Compare commits

...

5 Commits

134 changed files with 8937 additions and 751 deletions
+5 -2
View File
@@ -5,7 +5,8 @@ use crate::app::context_clues::{
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS,
ADD_ARTIST_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock,
EDIT_ARTIST_BLOCKS,
};
#[cfg(test)]
@@ -73,7 +74,9 @@ impl ContextClueProvider for LidarrContextClueProvider {
ActiveLidarrBlock::AddArtistSearchInput | ActiveLidarrBlock::AddArtistEmptySearchResults => {
Some(&BARE_POPUP_CONTEXT_CLUES)
}
_ if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block) => {
_ if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block)
|| ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) =>
{
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
ActiveLidarrBlock::AddArtistPrompt
+5 -2
View File
@@ -10,7 +10,7 @@ mod tests {
LidarrContextClueProvider,
};
use crate::models::servarr_data::lidarr::lidarr_data::{
ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, LidarrData,
ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, LidarrData,
};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use rstest::rstest;
@@ -218,7 +218,10 @@ mod tests {
#[test]
fn test_lidarr_context_clue_provider_confirmation_prompt_popup_clues_edit_indexer_blocks() {
for active_lidarr_block in EDIT_ARTIST_BLOCKS {
let mut blocks = EDIT_ARTIST_BLOCKS.to_vec();
blocks.extend(ADD_ROOT_FOLDER_BLOCKS);
for active_lidarr_block in blocks {
let mut app = App::test_default();
app.push_navigation_stack(active_lidarr_block.into());
+41
View File
@@ -12,6 +12,7 @@ mod tests {
async fn test_dispatch_by_lidarr_block_artists() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
@@ -37,6 +38,7 @@ mod tests {
async fn test_dispatch_by_lidarr_block_artist_details() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.data.lidarr_data.artists.set_items(vec![artist()]);
app.network_tx = Some(tx);
@@ -50,10 +52,31 @@ mod tests {
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_downloads_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(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::Downloads)
.await;
assert!(app.is_loading);
assert_eq!(
rx.recv().await.unwrap(),
LidarrEvent::GetDownloads(500).into()
);
assert!(!app.data.lidarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_lidarr_block_add_artist_search_results() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app.data.lidarr_data.add_artist_search = Some("test artist".into());
@@ -74,6 +97,7 @@ mod tests {
async fn test_dispatch_by_history_block() {
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App::test_default();
app.data.lidarr_data.prompt_confirm = true;
app.network_tx = Some(tx);
app
@@ -89,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::<NetworkEvent>(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();
+10
View File
@@ -28,6 +28,11 @@ impl App<'_> {
.dispatch_network_event(LidarrEvent::ListArtists.into())
.await;
}
ActiveLidarrBlock::Downloads => {
self
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
.await;
}
ActiveLidarrBlock::ArtistDetails => {
self
.dispatch_network_event(LidarrEvent::GetAlbums(self.extract_artist_id().await).into())
@@ -45,6 +50,11 @@ impl App<'_> {
.dispatch_network_event(LidarrEvent::GetHistory(500).into())
.await
}
ActiveLidarrBlock::RootFolders => {
self
.dispatch_network_event(LidarrEvent::GetRootFolders.into())
.await;
}
_ => (),
}
+5
View File
@@ -62,6 +62,11 @@ impl App<'_> {
.dispatch_network_event(RadarrEvent::GetDownloads(500).into())
.await;
}
ActiveRadarrBlock::History => {
self
.dispatch_network_event(RadarrEvent::GetHistory(500).into())
.await;
}
ActiveRadarrBlock::Indexers => {
self
.dispatch_network_event(RadarrEvent::GetTags.into())
+6 -5
View File
@@ -3,8 +3,8 @@ mod tests {
use crate::app::App;
use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::radarr::radarr_context_clues::{
@@ -459,9 +459,10 @@ mod tests {
#[case(1, ActiveRadarrBlock::Collections, &COLLECTIONS_CONTEXT_CLUES)]
#[case(2, ActiveRadarrBlock::Downloads, &DOWNLOADS_CONTEXT_CLUES)]
#[case(3, ActiveRadarrBlock::Blocklist, &BLOCKLIST_CONTEXT_CLUES)]
#[case(4, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)]
#[case(5, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)]
#[case(6, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)]
#[case(4, ActiveRadarrBlock::History, &HISTORY_CONTEXT_CLUES)]
#[case(5, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)]
#[case(6, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)]
#[case(7, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)]
fn test_radarr_context_clue_provider_radarr_blocks_context_clues(
#[case] index: usize,
#[case] active_radarr_block: ActiveRadarrBlock,
+17
View File
@@ -147,6 +147,23 @@ mod tests {
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_history_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_radarr_block(&ActiveRadarrBlock::History)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetHistory(500).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_root_folders_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
+68 -1
View File
@@ -8,7 +8,9 @@ use super::LidarrCommand;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::lidarr_models::{AddArtistBody, AddArtistOptions, MonitorType, NewItemMonitorType},
models::lidarr_models::{
AddArtistBody, AddArtistOptions, AddLidarrRootFolderBody, MonitorType, NewItemMonitorType,
},
network::{NetworkTrait, lidarr_network::LidarrEvent},
};
@@ -75,6 +77,46 @@ pub enum LidarrAddCommand {
)]
no_search_for_missing_albums: bool,
},
#[command(about = "Add a new root folder")]
RootFolder {
#[arg(long, help = "The name of the root folder", required = true)]
name: String,
#[arg(long, help = "The path of the new root folder", required = true)]
root_folder_path: String,
#[arg(
long,
help = "The ID of the default quality profile for artists in this root folder",
required = true
)]
quality_profile_id: i64,
#[arg(
long,
help = "The ID of the default metadata profile for artists in this root folder",
required = true
)]
metadata_profile_id: i64,
#[arg(
long,
help = "The default monitor option for artists in this root folder",
value_enum,
default_value_t = MonitorType::default()
)]
monitor: MonitorType,
#[arg(
long,
help = "The default monitor new items option for artists in this root folder",
value_enum,
default_value_t = NewItemMonitorType::default()
)]
monitor_new_items: NewItemMonitorType,
#[arg(
long,
help = "Default tag IDs for artists in this root folder",
value_parser,
action = ArgAction::Append
)]
tag: Vec<i64>,
},
#[command(about = "Add new tag")]
Tag {
#[arg(long, help = "The name of the tag to be added", required = true)]
@@ -142,6 +184,31 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrAddCommand> for LidarrAddCommandHan
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrAddCommand::RootFolder {
name,
root_folder_path,
quality_profile_id,
metadata_profile_id,
monitor,
monitor_new_items,
tag: tags,
} => {
let add_root_folder_body = AddLidarrRootFolderBody {
name,
path: root_folder_path,
default_quality_profile_id: quality_profile_id,
default_metadata_profile_id: metadata_profile_id,
default_monitor_option: monitor,
default_new_item_monitor_option: monitor_new_items,
default_tags: tags,
tag_input_string: None,
};
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
+93 -1
View File
@@ -27,6 +27,53 @@ 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 {
name: "Music".to_owned(),
root_folder_path: "/nfs/test".to_owned(),
quality_profile_id: 1,
metadata_profile_id: 1,
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
tag: vec![],
};
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"add",
"root-folder",
"--name",
"Music",
"--root-folder-path",
"/nfs/test",
"--quality-profile-id",
"1",
"--metadata-profile-id",
"1",
]);
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"]);
@@ -381,7 +428,8 @@ mod tests {
use crate::cli::lidarr::add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler};
use crate::models::Serdeable;
use crate::models::lidarr_models::{
AddArtistBody, AddArtistOptions, LidarrSerdeable, MonitorType, NewItemMonitorType,
AddArtistBody, AddArtistOptions, AddLidarrRootFolderBody, LidarrSerdeable, MonitorType,
NewItemMonitorType,
};
use crate::network::lidarr_network::LidarrEvent;
use crate::{
@@ -389,6 +437,50 @@ mod tests {
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 = AddLidarrRootFolderBody {
name: "Music".to_owned(),
path: expected_root_folder_path.clone(),
default_quality_profile_id: 1,
default_metadata_profile_id: 1,
default_monitor_option: MonitorType::All,
default_new_item_monitor_option: NewItemMonitorType::All,
default_tags: vec![1, 2],
tag_input_string: None,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
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 {
name: "Music".to_owned(),
root_folder_path: expected_root_folder_path,
quality_profile_id: 1,
metadata_profile_id: 1,
monitor: MonitorType::All,
monitor_new_items: NewItemMonitorType::All,
tag: vec![1, 2],
};
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();
+24
View File
@@ -37,6 +37,16 @@ pub enum LidarrDeleteCommand {
#[arg(long, help = "Add a list exclusion for this artist")]
add_list_exclusion: bool,
},
#[command(about = "Delete the specified download")]
Download {
#[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)]
@@ -103,6 +113,20 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteComm
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrDeleteCommand::Download { download_id } => {
let resp = self
.network
.handle_network_event(LidarrEvent::DeleteDownload(download_id).into())
.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
@@ -145,6 +145,74 @@ mod tests {
assert_eq!(delete_command, expected_args);
}
#[test]
fn test_delete_download_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "download"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_download_success() {
let expected_args = LidarrDeleteCommand::Download { download_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"lidarr",
"delete",
"download",
"--download-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_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"]);
@@ -260,6 +328,58 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_delete_download_command() {
let expected_download_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::DeleteDownload(expected_download_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_download_command = LidarrDeleteCommand::Download { download_id: 1 };
let result =
LidarrDeleteCommandHandler::with(&app_arc, delete_download_command, &mut mock_network)
.handle()
.await;
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::<NetworkEvent>(
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;
+21
View File
@@ -29,6 +29,11 @@ pub enum LidarrListCommand {
},
#[command(about = "List all artists in your Lidarr library")]
Artists,
#[command(about = "List all active downloads in Lidarr")]
Downloads {
#[arg(long, help = "How many downloads to fetch", default_value_t = 500)]
count: u64,
},
#[command(about = "Fetch all Lidarr history events")]
History {
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
@@ -38,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,
}
@@ -83,6 +90,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::Downloads { count } => {
let resp = self
.network
.handle_network_event(LidarrEvent::GetDownloads(count).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrListCommand::History { events: items } => {
let resp = self
.network
@@ -104,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
+58 -1
View File
@@ -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]);
@@ -58,6 +65,29 @@ mod tests {
assert_eq!(album_command, expected_args);
}
#[test]
fn test_list_downloads_count_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "downloads", "--count"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_downloads_default_values() {
let expected_args = LidarrListCommand::Downloads { count: 500 };
let result = Cli::try_parse_from(["managarr", "lidarr", "list", "downloads"]);
assert_ok!(&result);
let Some(Command::Lidarr(LidarrCommand::List(downloads_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(downloads_command, expected_args);
}
#[test]
fn test_list_history_events_flag_requires_arguments() {
let result =
@@ -104,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(
@@ -151,6 +182,32 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_downloads_command() {
let expected_count = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
LidarrEvent::GetDownloads(expected_count).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_downloads_command = LidarrListCommand::Downloads { count: 1000 };
let result =
LidarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_history_command() {
let expected_events = 1000;
@@ -28,6 +28,8 @@ pub enum LidarrRefreshCommand {
)]
artist_id: i64,
},
#[command(about = "Refresh all downloads in Lidarr")]
Downloads,
}
impl From<LidarrRefreshCommand> for Command {
@@ -73,6 +75,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrRefreshCommand>
.await?;
serde_json::to_string_pretty(&resp)?
}
LidarrRefreshCommand::Downloads => {
let resp = self
.network
.handle_network_event(LidarrEvent::UpdateDownloads.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
@@ -22,11 +22,14 @@ mod tests {
use super::*;
use clap::{Parser, error::ErrorKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn test_refresh_all_artists_has_no_arg_requirements() {
#[rstest]
fn test_refresh_commands_have_no_arg_requirements(
#[values("all-artists", "downloads")] subcommand: &str,
) {
let result =
Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", "all-artists"]);
Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", subcommand]);
assert_ok!(&result);
}
@@ -67,6 +70,7 @@ mod tests {
use std::sync::Arc;
use mockall::predicate::eq;
use rstest::rstest;
use serde_json::json;
use tokio::sync::Mutex;
@@ -80,12 +84,18 @@ mod tests {
network::{MockNetworkTrait, NetworkEvent},
};
#[rstest]
#[case(LidarrRefreshCommand::AllArtists, LidarrEvent::UpdateAllArtists)]
#[case(LidarrRefreshCommand::Downloads, LidarrEvent::UpdateDownloads)]
#[tokio::test]
async fn test_handle_refresh_all_artists_command() {
async fn test_handle_refresh_command(
#[case] refresh_command: LidarrRefreshCommand,
#[case] expected_sonarr_event: LidarrEvent,
) {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(LidarrEvent::UpdateAllArtists.into()))
.with(eq::<NetworkEvent>(expected_sonarr_event.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Lidarr(LidarrSerdeable::Value(
@@ -93,7 +103,6 @@ mod tests {
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_command = LidarrRefreshCommand::AllArtists;
let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle()
+12
View File
@@ -29,6 +29,11 @@ pub enum RadarrListCommand {
},
#[command(about = "List disk space details for all provisioned root folders in Radarr")]
DiskSpace,
#[command(about = "Fetch all Radarr history events")]
History {
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
events: u64,
},
#[command(about = "List all Radarr indexers")]
Indexers,
#[command(about = "Fetch Radarr logs")]
@@ -121,6 +126,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::History { events: items } => {
let resp = self
.network
.handle_network_event(RadarrEvent::GetHistory(items).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::Indexers => {
let resp = self
.network
@@ -111,6 +111,29 @@ mod tests {
assert_eq!(refresh_command, expected_args);
}
#[test]
fn test_list_history_events_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "list", "history", "--events"]);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_history_default_values() {
let expected_args = RadarrListCommand::History { events: 500 };
let result = Cli::try_parse_from(["managarr", "radarr", "list", "history"]);
assert_ok!(&result);
let Some(Command::Radarr(RadarrCommand::List(history_command))) = result.unwrap().command
else {
panic!("Unexpected command type");
};
assert_eq!(history_command, expected_args);
}
#[test]
fn test_list_logs_default_values() {
let expected_args = RadarrListCommand::Logs {
@@ -233,6 +256,32 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_history_command() {
let expected_events = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::GetHistory(expected_events).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_history_command = RadarrListCommand::History { events: 1000 };
let result =
RadarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_handle_list_logs_command() {
let expected_events = 1000;
+18
View File
@@ -64,6 +64,15 @@ pub enum RadarrCommand {
Refresh(RadarrRefreshCommand),
#[command(about = "Clear the blocklist")]
ClearBlocklist,
#[command(about = "Mark the Radarr history item with the given ID as 'failed'")]
MarkHistoryItemAsFailed {
#[arg(
long,
help = "The Radarr ID of the history item you wish to mark as 'failed'",
required = true
)]
history_item_id: i64,
},
#[command(about = "Manually download the given release for the specified movie ID")]
DownloadRelease {
#[arg(long, help = "The GUID of the release to download", required = true)]
@@ -208,6 +217,15 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
let _ = self
.network
.handle_network_event(RadarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
.await?;
serde_json::to_string_pretty(
&serde_json::json!({"message": "Radarr history item marked as 'failed'"}),
)?
}
RadarrCommand::DownloadRelease {
guid,
indexer_id,
+55
View File
@@ -31,6 +31,31 @@ mod tests {
assert_ok!(&result);
}
#[test]
fn test_mark_history_item_as_failed_requires_history_item_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "mark-history-item-as-failed"]);
assert_err!(&result);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_mark_history_item_as_failed_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"mark-history-item-as-failed",
"--history-item-id",
"1",
]);
assert_ok!(&result);
}
#[test]
fn test_download_release_requires_movie_id() {
let result = Cli::command().try_get_matches_from([
@@ -327,6 +352,36 @@ mod tests {
assert_ok!(&result);
}
#[tokio::test]
async fn test_mark_history_item_as_failed_command() {
let expected_history_item_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let mark_history_item_as_failed_command =
RadarrCommand::MarkHistoryItemAsFailed { history_item_id: 1 };
let result = RadarrCliHandler::with(
&app_arc,
mark_history_item_as_failed_command,
&mut mock_network,
)
.handle()
.await;
assert_ok!(&result);
}
#[tokio::test]
async fn test_download_release_command() {
let expected_release_download_body = RadarrReleaseDownloadBody {
+1 -84
View File
@@ -330,90 +330,7 @@ mod test_utils {
#[macro_export]
macro_rules! test_handler_delegation {
($handler:ident, $base:expr, $active_block:expr) => {
let mut app = App::test_default();
app.data.sonarr_data.history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
]);
app
.data
.sonarr_data
.root_folders
.set_items(vec![$crate::models::servarr_models::RootFolder::default()]);
app
.data
.sonarr_data
.indexers
.set_items(vec![$crate::models::servarr_models::Indexer::default()]);
app
.data
.sonarr_data
.blocklist
.set_items(vec![$crate::models::sonarr_models::BlocklistItem::default()]);
app.data.sonarr_data.add_searched_series =
Some($crate::models::stateful_table::StatefulTable::default());
app
.data
.radarr_data
.movies
.set_items(vec![$crate::models::radarr_models::Movie::default()]);
app
.data
.radarr_data
.collections
.set_items(vec![$crate::models::radarr_models::Collection::default()]);
app.data.radarr_data.collection_movies.set_items(vec![
$crate::models::radarr_models::CollectionMovie::default(),
]);
app
.data
.radarr_data
.indexers
.set_items(vec![$crate::models::servarr_models::Indexer::default()]);
app
.data
.radarr_data
.root_folders
.set_items(vec![$crate::models::servarr_models::RootFolder::default()]);
app
.data
.radarr_data
.blocklist
.set_items(vec![$crate::models::radarr_models::BlocklistItem::default()]);
app.data.radarr_data.add_searched_movies =
Some($crate::models::stateful_table::StatefulTable::default());
let mut movie_details_modal =
$crate::models::servarr_data::radarr::modals::MovieDetailsModal::default();
movie_details_modal.movie_history.set_items(vec![
$crate::models::radarr_models::MovieHistoryItem::default(),
]);
movie_details_modal
.movie_cast
.set_items(vec![$crate::models::radarr_models::Credit::default()]);
movie_details_modal
.movie_crew
.set_items(vec![$crate::models::radarr_models::Credit::default()]);
movie_details_modal
.movie_releases
.set_items(vec![$crate::models::radarr_models::RadarrRelease::default()]);
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
let mut season_details_modal =
$crate::models::servarr_data::sonarr::modals::SeasonDetailsModal::default();
season_details_modal.season_history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
]);
season_details_modal.episode_details_modal =
Some($crate::models::servarr_data::sonarr::modals::EpisodeDetailsModal::default());
app.data.sonarr_data.season_details_modal = Some(season_details_modal);
let mut series_history = $crate::models::stateful_table::StatefulTable::default();
series_history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
]);
app.data.sonarr_data.series_history = Some(series_history);
app
.data
.sonarr_data
.series
.set_items(vec![$crate::models::sonarr_models::Series::default()]);
let mut app = App::test_default_fully_populated();
app.push_navigation_stack($base.into());
app.push_navigation_stack($active_block.into());
@@ -0,0 +1,481 @@
#[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_navigation_pushed;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::downloads::DownloadsHandler;
use crate::models::lidarr_models::DownloadRecord;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS};
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record;
mod test_handle_delete {
use pretty_assertions::assert_eq;
use super::*;
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
#[test]
fn test_delete_download_prompt() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteDownloadPrompt.into());
}
#[test]
fn test_delete_download_prompt_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into());
}
}
mod test_handle_left_right_action {
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
use crate::assert_navigation_pushed;
#[rstest]
fn test_downloads_tab_left(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(1);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.left.key,
&mut app,
ActiveLidarrBlock::Downloads,
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_downloads_tab_right(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(1);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.right.key,
&mut app,
ActiveLidarrBlock::Downloads,
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_downloads_left_right_prompt_toggle(
#[values(
ActiveLidarrBlock::DeleteDownloadPrompt,
ActiveLidarrBlock::UpdateDownloadsPrompt
)]
active_lidarr_block: ActiveLidarrBlock,
#[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key,
) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
DownloadsHandler::new(key, &mut app, active_lidarr_block, None).handle();
assert!(app.data.lidarr_data.prompt_confirm);
DownloadsHandler::new(key, &mut app, active_lidarr_block, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
}
}
mod test_handle_submit {
use rstest::rstest;
use crate::network::lidarr_network::LidarrEvent;
use super::*;
use crate::assert_navigation_popped;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[rstest]
#[case(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
LidarrEvent::DeleteDownload(1)
)]
#[case(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::UpdateDownloadsPrompt,
LidarrEvent::UpdateDownloads
)]
fn test_downloads_prompt_confirm_submit(
#[case] base_route: ActiveLidarrBlock,
#[case] prompt_block: ActiveLidarrBlock,
#[case] expected_action: LidarrEvent,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![download_record()]);
app.data.lidarr_data.prompt_confirm = true;
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&expected_action
);
assert_navigation_popped!(app, base_route.into());
}
#[rstest]
#[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::DeleteDownloadPrompt)]
#[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::UpdateDownloadsPrompt)]
fn test_downloads_prompt_decline_submit(
#[case] base_route: ActiveLidarrBlock,
#[case] prompt_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(!app.data.lidarr_data.prompt_confirm);
assert_none!(app.data.lidarr_data.prompt_confirm_action);
assert_navigation_popped!(app, base_route.into());
}
}
mod test_handle_esc {
use rstest::rstest;
use super::*;
use crate::assert_navigation_popped;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[rstest]
#[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::DeleteDownloadPrompt)]
#[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::UpdateDownloadsPrompt)]
fn test_downloads_prompt_blocks_esc(
#[case] base_block: ActiveLidarrBlock,
#[case] prompt_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(base_block.into());
app.push_navigation_stack(prompt_block.into());
app.data.lidarr_data.prompt_confirm = true;
DownloadsHandler::new(ESC_KEY, &mut app, prompt_block, None).handle();
assert_navigation_popped!(app, base_block.into());
assert!(!app.data.lidarr_data.prompt_confirm);
}
#[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::Downloads.into());
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
DownloadsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle();
assert_navigation_popped!(app, ActiveLidarrBlock::Downloads.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::download_record;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn test_update_downloads_key() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateDownloadsPrompt.into());
}
#[test]
fn test_update_downloads_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.update.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into());
}
#[test]
fn test_refresh_downloads_key() {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveLidarrBlock::Downloads.into());
assert!(app.should_refresh);
}
#[test]
fn test_refresh_downloads_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into());
assert!(!app.should_refresh);
}
#[rstest]
#[case(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
LidarrEvent::DeleteDownload(1)
)]
#[case(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::UpdateDownloadsPrompt,
LidarrEvent::UpdateDownloads
)]
fn test_downloads_prompt_confirm_submit(
#[case] base_route: ActiveLidarrBlock,
#[case] prompt_block: ActiveLidarrBlock,
#[case] expected_action: LidarrEvent,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![download_record()]);
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::new(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
prompt_block,
None,
)
.handle();
assert!(app.data.lidarr_data.prompt_confirm);
assert_some_eq_x!(
&app.data.lidarr_data.prompt_confirm_action,
&expected_action
);
assert_navigation_popped!(app, base_route.into());
}
}
#[test]
fn test_downloads_handler_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if DOWNLOADS_BLOCKS.contains(&active_lidarr_block) {
assert!(DownloadsHandler::accepts(active_lidarr_block));
} else {
assert!(!DownloadsHandler::accepts(active_lidarr_block));
}
})
}
#[rstest]
fn test_downloads_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 = DownloadsHandler::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_download_id() {
let mut app = App::test_default();
app
.data
.lidarr_data
.downloads
.set_items(vec![download_record()]);
let download_id = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
)
.extract_download_id();
assert_eq!(download_id, 1);
}
#[test]
fn test_downloads_handler_not_ready_when_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = true;
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_downloads_handler_not_ready_when_downloads_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = false;
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_downloads_handler_ready_when_not_loading_and_downloads_is_not_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
app.is_loading = false;
app
.data
.lidarr_data
.downloads
.set_items(vec![DownloadRecord::default()]);
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveLidarrBlock::Downloads,
None,
);
assert!(handler.is_ready());
}
}
@@ -0,0 +1,171 @@
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::matches_key;
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS};
use crate::network::lidarr_network::LidarrEvent;
#[cfg(test)]
#[path = "downloads_handler_tests.rs"]
mod downloads_handler_tests;
pub(super) struct DownloadsHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
}
impl DownloadsHandler<'_, '_> {
fn extract_download_id(&self) -> i64 {
self.app.data.lidarr_data.downloads.current_selection().id
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DownloadsHandler<'a, 'b> {
fn handle(&mut self) {
let download_table_handling_config =
TableHandlingConfig::new(ActiveLidarrBlock::Downloads.into());
if !handle_table(
self,
|app| &mut app.data.lidarr_data.downloads,
download_table_handling_config,
) {
self.handle_key_event();
}
}
fn accepts(active_block: ActiveLidarrBlock) -> bool {
DOWNLOADS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
_context: Option<ActiveLidarrBlock>,
) -> DownloadsHandler<'a, 'b> {
DownloadsHandler {
key,
app,
active_lidarr_block: active_block,
_context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
!self.app.is_loading && !self.app.data.lidarr_data.downloads.is_empty()
}
fn handle_scroll_up(&mut self) {}
fn handle_scroll_down(&mut self) {}
fn handle_home(&mut self) {}
fn handle_end(&mut self) {}
fn handle_delete(&mut self) {
if self.active_lidarr_block == ActiveLidarrBlock::Downloads {
self
.app
.push_navigation_stack(ActiveLidarrBlock::DeleteDownloadPrompt.into())
}
}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::Downloads => handle_change_tab_left_right_keys(self.app, self.key),
ActiveLidarrBlock::DeleteDownloadPrompt | ActiveLidarrBlock::UpdateDownloadsPrompt => {
handle_prompt_toggle(self.app, self.key)
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::DeleteDownloadPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteDownload(self.extract_download_id()));
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::UpdateDownloadsPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateDownloads);
}
self.app.pop_navigation_stack();
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::DeleteDownloadPrompt | ActiveLidarrBlock::UpdateDownloadsPrompt => {
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::Downloads => match self.key {
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveLidarrBlock::UpdateDownloadsPrompt.into());
}
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
},
ActiveLidarrBlock::DeleteDownloadPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action =
Some(LidarrEvent::DeleteDownload(self.extract_download_id()));
self.app.pop_navigation_stack();
}
}
ActiveLidarrBlock::UpdateDownloadsPrompt => {
if matches_key!(confirm, key) {
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateDownloads);
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()
}
}
@@ -29,7 +29,7 @@ mod tests {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(1);
app.data.lidarr_data.main_tabs.set_index(2);
HistoryHandler::new(
DEFAULT_KEYBINDINGS.left.key,
@@ -41,9 +41,9 @@ mod tests {
assert_eq!(
app.data.lidarr_data.main_tabs.get_active_route(),
ActiveLidarrBlock::Artists.into()
ActiveLidarrBlock::Downloads.into()
);
assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into());
assert_navigation_pushed!(app, ActiveLidarrBlock::Downloads.into());
}
#[rstest]
@@ -51,7 +51,7 @@ mod tests {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app.is_loading = is_ready;
app.data.lidarr_data.main_tabs.set_index(1);
app.data.lidarr_data.main_tabs.set_index(2);
HistoryHandler::new(
DEFAULT_KEYBINDINGS.right.key,
@@ -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());
}
}
@@ -2,13 +2,10 @@
mod tests {
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_pushed;
use crate::handlers::KeyEventHandler;
use crate::handlers::lidarr_handlers::{LidarrHandler, handle_change_tab_left_right_keys};
use crate::models::lidarr_models::Artist;
use crate::models::lidarr_models::LidarrHistoryItem;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::lidarr::modals::EditArtistModal;
use crate::{assert_navigation_pushed, test_handler_delegation};
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
@@ -55,8 +52,10 @@ mod tests {
}
#[rstest]
#[case(0, ActiveLidarrBlock::History, ActiveLidarrBlock::History)]
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::Artists)]
#[case(0, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::Downloads)]
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)]
#[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,
@@ -85,8 +84,10 @@ mod tests {
}
#[rstest]
#[case(0, ActiveLidarrBlock::History, ActiveLidarrBlock::History)]
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::Artists)]
#[case(0, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::Downloads)]
#[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)]
#[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,
@@ -116,7 +117,9 @@ mod tests {
#[rstest]
#[case(0, ActiveLidarrBlock::Artists)]
#[case(1, ActiveLidarrBlock::History)]
#[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,
@@ -165,25 +168,27 @@ mod tests {
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.artists
.set_items(vec![Artist::default()]);
app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default());
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
app.push_navigation_stack(active_lidarr_block.into());
test_handler_delegation!(
LidarrHandler,
ActiveLidarrBlock::Artists,
active_lidarr_block
);
}
LidarrHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into());
#[rstest]
fn test_delegates_downloads_blocks_to_downloads_handler(
#[values(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
ActiveLidarrBlock::UpdateDownloadsPrompt
)]
active_lidarr_block: ActiveLidarrBlock,
) {
test_handler_delegation!(
LidarrHandler,
ActiveLidarrBlock::Downloads,
active_lidarr_block
);
}
#[rstest]
@@ -199,23 +204,26 @@ mod tests {
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default();
app
.data
.lidarr_data
.history
.set_items(vec![LidarrHistoryItem::default()]);
app.push_navigation_stack(ActiveLidarrBlock::History.into());
app.push_navigation_stack(active_lidarr_block.into());
test_handler_delegation!(
LidarrHandler,
ActiveLidarrBlock::History,
active_lidarr_block
);
}
LidarrHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
active_lidarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveLidarrBlock::History.into());
#[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
);
}
}
+11
View File
@@ -2,6 +2,8 @@ use history::HistoryHandler;
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,
@@ -10,9 +12,11 @@ use crate::{
mod history;
mod library;
mod downloads;
#[cfg(test)]
#[path = "lidarr_handler_tests.rs"]
mod lidarr_handler_tests;
mod root_folders;
pub(super) struct LidarrHandler<'a, 'b> {
key: Key,
@@ -27,9 +31,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b
_ if LibraryHandler::accepts(self.active_lidarr_block) => {
LibraryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
}
_ if DownloadsHandler::accepts(self.active_lidarr_block) => {
DownloadsHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
}
_ 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(),
}
}
@@ -0,0 +1,533 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::models::lidarr_models::AddLidarrRootFolderBody;
use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock};
use crate::models::servarr_data::lidarr::modals::AddRootFolderModal;
use crate::models::{Route, Scrollable};
use crate::network::lidarr_network::LidarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
#[cfg(test)]
#[path = "add_root_folder_handler_tests.rs"]
mod add_root_folder_handler_tests;
pub(super) struct AddRootFolderHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_lidarr_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
}
impl AddRootFolderHandler<'_, '_> {
fn build_add_root_folder_body(&mut self) -> AddLidarrRootFolderBody {
let add_root_folder_modal = self
.app
.data
.lidarr_data
.add_root_folder_modal
.take()
.expect("AddRootFolderModal is None");
let tags = add_root_folder_modal.tags.text.clone();
let AddRootFolderModal {
name,
path,
monitor_list,
monitor_new_items_list,
quality_profile_list,
metadata_profile_list,
..
} = add_root_folder_modal;
let quality_profile = quality_profile_list.current_selection();
let quality_profile_id = *self
.app
.data
.lidarr_data
.quality_profile_map
.iter()
.filter(|(_, value)| *value == quality_profile)
.map(|(key, _)| key)
.next()
.unwrap();
let metadata_profile = metadata_profile_list.current_selection();
let metadata_profile_id = *self
.app
.data
.lidarr_data
.metadata_profile_map
.iter()
.filter(|(_, value)| *value == metadata_profile)
.map(|(key, _)| key)
.next()
.unwrap();
AddLidarrRootFolderBody {
name: name.text,
path: path.text,
default_quality_profile_id: quality_profile_id,
default_metadata_profile_id: metadata_profile_id,
default_monitor_option: *monitor_list.current_selection(),
default_new_item_monitor_option: *monitor_new_items_list.current_selection(),
default_tags: Vec::new(),
tag_input_string: Some(tags),
}
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddRootFolderHandler<'a, 'b> {
fn accepts(active_block: ActiveLidarrBlock) -> bool {
ADD_ROOT_FOLDER_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
) -> AddRootFolderHandler<'a, 'b> {
AddRootFolderHandler {
key,
app,
active_lidarr_block: active_block,
context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
!self.app.is_loading && self.app.data.lidarr_data.add_root_folder_modal.is_some()
}
fn handle_scroll_up(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderSelectMonitor => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_up(),
ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_up(),
ActiveLidarrBlock::AddRootFolderSelectQualityProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_up(),
ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_up(),
ActiveLidarrBlock::AddRootFolderPrompt => self.app.data.lidarr_data.selected_block.up(),
_ => (),
}
}
fn handle_scroll_down(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderSelectMonitor => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_down(),
ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_down(),
ActiveLidarrBlock::AddRootFolderSelectQualityProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_down(),
ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_down(),
ActiveLidarrBlock::AddRootFolderPrompt => self.app.data.lidarr_data.selected_block.down(),
_ => (),
}
}
fn handle_home(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderSelectMonitor => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_to_top(),
ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_to_top(),
ActiveLidarrBlock::AddRootFolderSelectQualityProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_to_top(),
ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_to_top(),
ActiveLidarrBlock::AddRootFolderNameInput => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.name
.scroll_home(),
ActiveLidarrBlock::AddRootFolderPathInput => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.path
.scroll_home(),
ActiveLidarrBlock::AddRootFolderTagsInput => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.tags
.scroll_home(),
_ => (),
}
}
fn handle_end(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderSelectMonitor => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.monitor_new_items_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddRootFolderSelectQualityProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.quality_profile_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.metadata_profile_list
.scroll_to_bottom(),
ActiveLidarrBlock::AddRootFolderNameInput => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.name
.reset_offset(),
ActiveLidarrBlock::AddRootFolderPathInput => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.path
.reset_offset(),
ActiveLidarrBlock::AddRootFolderTagsInput => self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.tags
.reset_offset(),
_ => (),
}
}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderPrompt => handle_prompt_toggle(self.app, self.key),
ActiveLidarrBlock::AddRootFolderNameInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.name
)
}
ActiveLidarrBlock::AddRootFolderPathInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.path
)
}
ActiveLidarrBlock::AddRootFolderTagsInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.tags
)
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderPrompt => {
match self.app.data.lidarr_data.selected_block.get_active_block() {
ActiveLidarrBlock::AddRootFolderConfirmPrompt => {
if self.app.data.lidarr_data.prompt_confirm {
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::AddRootFolder(
self.build_add_root_folder_body(),
));
self.app.should_refresh = true;
}
self.app.pop_navigation_stack();
}
ActiveLidarrBlock::AddRootFolderSelectMonitor
| ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems
| ActiveLidarrBlock::AddRootFolderSelectQualityProfile
| ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => {
self.app.push_navigation_stack(
(
self.app.data.lidarr_data.selected_block.get_active_block(),
self.context,
)
.into(),
)
}
ActiveLidarrBlock::AddRootFolderNameInput
| ActiveLidarrBlock::AddRootFolderPathInput
| ActiveLidarrBlock::AddRootFolderTagsInput => {
self.app.push_navigation_stack(
(
self.app.data.lidarr_data.selected_block.get_active_block(),
self.context,
)
.into(),
);
self.app.ignore_special_keys_for_textbox_input = true;
}
_ => (),
}
}
ActiveLidarrBlock::AddRootFolderSelectMonitor
| ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems
| ActiveLidarrBlock::AddRootFolderSelectQualityProfile
| ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self.app.pop_navigation_stack(),
ActiveLidarrBlock::AddRootFolderNameInput
| ActiveLidarrBlock::AddRootFolderPathInput
| ActiveLidarrBlock::AddRootFolderTagsInput => {
self.app.pop_navigation_stack();
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderNameInput
| ActiveLidarrBlock::AddRootFolderPathInput
| ActiveLidarrBlock::AddRootFolderTagsInput => {
self.app.pop_navigation_stack();
self.app.ignore_special_keys_for_textbox_input = false;
}
ActiveLidarrBlock::AddRootFolderPrompt => {
self.app.pop_navigation_stack();
self.app.data.lidarr_data.add_root_folder_modal = None;
self.app.data.lidarr_data.prompt_confirm = false;
}
ActiveLidarrBlock::AddRootFolderSelectMonitor
| ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems
| ActiveLidarrBlock::AddRootFolderSelectQualityProfile
| ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self.app.pop_navigation_stack(),
_ => (),
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
match self.active_lidarr_block {
ActiveLidarrBlock::AddRootFolderNameInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.name
)
}
ActiveLidarrBlock::AddRootFolderPathInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.path
)
}
ActiveLidarrBlock::AddRootFolderTagsInput => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.unwrap()
.tags
)
}
ActiveLidarrBlock::AddRootFolderPrompt => {
if self.app.data.lidarr_data.selected_block.get_active_block()
== ActiveLidarrBlock::AddRootFolderConfirmPrompt
&& matches_key!(confirm, key)
{
self.app.data.lidarr_data.prompt_confirm = true;
self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::AddRootFolder(
self.build_add_root_folder_body(),
));
self.app.should_refresh = true;
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()
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,173 @@
use add_root_folder_handler::AddRootFolderHandler;
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::matches_key;
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ROOT_FOLDER_BLOCKS, ADD_ROOT_FOLDER_SELECTION_BLOCKS, ActiveLidarrBlock, ROOT_FOLDERS_BLOCKS,
};
use crate::models::servarr_data::lidarr::modals::AddRootFolderModal;
use crate::models::{BlockSelectionState, Route};
use crate::network::lidarr_network::LidarrEvent;
mod add_root_folder_handler;
#[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<ActiveLidarrBlock>,
}
impl RootFoldersHandler<'_, '_> {
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 AddRootFolderHandler::accepts(self.active_lidarr_block) {
return AddRootFolderHandler::new(self.key, self.app, self.active_lidarr_block, self.context)
.handle();
}
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) || ADD_ROOT_FOLDER_BLOCKS.contains(&active_block)
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveLidarrBlock,
context: Option<ActiveLidarrBlock>,
) -> 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) {}
fn handle_end(&mut self) {}
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),
_ => (),
}
}
fn handle_submit(&mut self) {
if 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();
}
}
fn handle_esc(&mut self) {
match self.active_lidarr_block {
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.data.lidarr_data.add_root_folder_modal =
Some(AddRootFolderModal::from(&self.app.data.lidarr_data));
self.app.data.lidarr_data.selected_block =
BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS);
self
.app
.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into());
}
_ => (),
},
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()
}
}
@@ -0,0 +1,480 @@
#[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_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::servarr_data::lidarr::lidarr_data::{
ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, ROOT_FOLDERS_BLOCKS,
};
use crate::models::servarr_models::RootFolder;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::root_folder;
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 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);
}
}
mod test_handle_submit {
use crate::network::lidarr_network::LidarrEvent;
use super::*;
use crate::assert_navigation_popped;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::root_folder;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[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);
}
#[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;
#[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_modal_present!(app.data.lidarr_data.add_root_folder_modal);
}
#[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_none!(app.data.lidarr_data.add_root_folder_modal);
}
#[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_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)
|| ADD_ROOT_FOLDER_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_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());
}
}
@@ -91,9 +91,9 @@ mod tests {
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
ActiveRadarrBlock::RootFolders.into()
ActiveRadarrBlock::History.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::RootFolders.into());
assert_navigation_pushed!(app, ActiveRadarrBlock::History.into());
}
#[rstest]
@@ -0,0 +1,438 @@
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use chrono::DateTime;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::App;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::assert_navigation_pushed;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::radarr_handlers::history::{HistoryHandler, history_sorting_options};
use crate::models::radarr_models::{RadarrHistoryEventType, RadarrHistoryItem};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, HISTORY_BLOCKS};
use crate::models::servarr_models::{Language, Quality, QualityWrapper};
mod test_handle_left_right_action {
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
use crate::assert_navigation_pushed;
#[rstest]
fn test_history_tab_left(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::History.into());
app.is_loading = is_ready;
app.data.radarr_data.main_tabs.set_index(4);
HistoryHandler::new(
DEFAULT_KEYBINDINGS.left.key,
&mut app,
ActiveRadarrBlock::History,
None,
)
.handle();
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
ActiveRadarrBlock::Blocklist.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::Blocklist.into());
}
#[rstest]
fn test_history_tab_right(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::History.into());
app.is_loading = is_ready;
app.data.radarr_data.main_tabs.set_index(4);
HistoryHandler::new(
DEFAULT_KEYBINDINGS.right.key,
&mut app,
ActiveRadarrBlock::History,
None,
)
.handle();
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
ActiveRadarrBlock::RootFolders.into()
);
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::RootFolders.into()
);
}
}
mod test_handle_submit {
use pretty_assertions::assert_eq;
use super::*;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[test]
fn test_history_submit() {
let mut app = App::test_default();
app.data.radarr_data.history.set_items(history_vec());
app.push_navigation_stack(ActiveRadarrBlock::History.into());
HistoryHandler::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::History, None).handle();
assert_navigation_pushed!(app, ActiveRadarrBlock::HistoryItemDetails.into());
}
#[test]
fn test_history_submit_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.data.radarr_data.history.set_items(history_vec());
app.push_navigation_stack(ActiveRadarrBlock::History.into());
HistoryHandler::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::History, None).handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::History.into());
}
}
mod test_handle_esc {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
use super::*;
use crate::assert_navigation_popped;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[test]
fn test_esc_history_item_details() {
let mut app = App::test_default();
app
.data
.radarr_data
.history
.set_items(vec![RadarrHistoryItem::default()]);
app.push_navigation_stack(ActiveRadarrBlock::History.into());
app.push_navigation_stack(ActiveRadarrBlock::HistoryItemDetails.into());
HistoryHandler::new(
ESC_KEY,
&mut app,
ActiveRadarrBlock::HistoryItemDetails,
None,
)
.handle();
assert_navigation_popped!(app, ActiveRadarrBlock::History.into());
}
#[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(ActiveRadarrBlock::History.into());
app.push_navigation_stack(ActiveRadarrBlock::History.into());
app.data.radarr_data = create_test_radarr_data();
app
.data
.radarr_data
.history
.set_items(vec![RadarrHistoryItem::default()]);
HistoryHandler::new(ESC_KEY, &mut app, ActiveRadarrBlock::History, None).handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::History.into());
assert_is_empty!(app.error.text);
}
}
mod test_handle_key_char {
use pretty_assertions::assert_eq;
use super::*;
use crate::assert_navigation_pushed;
#[test]
fn test_refresh_history_key() {
let mut app = App::test_default();
app.data.radarr_data.history.set_items(history_vec());
app.push_navigation_stack(ActiveRadarrBlock::History.into());
HistoryHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveRadarrBlock::History,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveRadarrBlock::History.into());
assert!(app.should_refresh);
}
#[test]
fn test_refresh_history_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.data.radarr_data.history.set_items(history_vec());
app.push_navigation_stack(ActiveRadarrBlock::History.into());
HistoryHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveRadarrBlock::History,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::History.into());
assert!(!app.should_refresh);
}
}
#[test]
fn test_history_sorting_options_source_title() {
let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = |a, b| {
a.source_title
.text
.to_lowercase()
.cmp(&b.source_title.text.to_lowercase())
};
let mut expected_history_vec = history_vec();
expected_history_vec.sort_by(expected_cmp_fn);
let sort_option = history_sorting_options()[0].clone();
let mut sorted_history_vec = history_vec();
sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_history_vec, expected_history_vec);
assert_str_eq!(sort_option.name, "Source Title");
}
#[test]
fn test_history_sorting_options_event_type() {
let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = |a, b| {
a.event_type
.to_string()
.to_lowercase()
.cmp(&b.event_type.to_string().to_lowercase())
};
let mut expected_history_vec = history_vec();
expected_history_vec.sort_by(expected_cmp_fn);
let sort_option = history_sorting_options()[1].clone();
let mut sorted_history_vec = history_vec();
sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_history_vec, expected_history_vec);
assert_str_eq!(sort_option.name, "Event Type");
}
#[test]
fn test_history_sorting_options_language() {
let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = |a, b| {
let default_language = Language {
id: 1,
name: "_".to_owned(),
};
let language_a = a.languages.first().unwrap_or(&default_language);
let language_b = b.languages.first().unwrap_or(&default_language);
language_a.cmp(language_b)
};
let mut expected_history_vec = history_vec();
expected_history_vec.sort_by(expected_cmp_fn);
let sort_option = history_sorting_options()[2].clone();
let mut sorted_history_vec = history_vec();
sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_history_vec, expected_history_vec);
assert_str_eq!(sort_option.name, "Language");
}
#[test]
fn test_history_sorting_options_quality() {
let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = |a, b| {
a.quality
.quality
.name
.to_lowercase()
.cmp(&b.quality.quality.name.to_lowercase())
};
let mut expected_history_vec = history_vec();
expected_history_vec.sort_by(expected_cmp_fn);
let sort_option = history_sorting_options()[3].clone();
let mut sorted_history_vec = history_vec();
sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_history_vec, expected_history_vec);
assert_str_eq!(sort_option.name, "Quality");
}
#[test]
fn test_history_sorting_options_date() {
let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering =
|a, b| a.date.cmp(&b.date);
let mut expected_history_vec = history_vec();
expected_history_vec.sort_by(expected_cmp_fn);
let sort_option = history_sorting_options()[4].clone();
let mut sorted_history_vec = history_vec();
sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_history_vec, expected_history_vec);
assert_str_eq!(sort_option.name, "Date");
}
#[test]
fn test_history_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if HISTORY_BLOCKS.contains(&active_radarr_block) {
assert!(HistoryHandler::accepts(active_radarr_block));
} else {
assert!(!HistoryHandler::accepts(active_radarr_block));
}
})
}
#[rstest]
fn test_history_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 = HistoryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_history_handler_not_ready_when_loading() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::History.into());
app.is_loading = true;
let handler = HistoryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::History,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_history_handler_not_ready_when_history_is_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::History.into());
app.is_loading = false;
let handler = HistoryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::History,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_history_handler_ready_when_not_loading_and_history_is_not_empty() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::History.into());
app.is_loading = false;
app
.data
.radarr_data
.history
.set_items(vec![RadarrHistoryItem::default()]);
let handler = HistoryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::History,
None,
);
assert!(handler.is_ready());
}
fn history_vec() -> Vec<RadarrHistoryItem> {
vec![
RadarrHistoryItem {
id: 3,
source_title: "test 1".into(),
movie_id: 1,
event_type: RadarrHistoryEventType::Grabbed,
languages: vec![Language {
id: 1,
name: "telgu".to_owned(),
}],
quality: QualityWrapper {
quality: Quality {
name: "HD - 1080p".to_owned(),
},
},
date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()),
..RadarrHistoryItem::default()
},
RadarrHistoryItem {
id: 2,
source_title: "test 2".into(),
movie_id: 2,
event_type: RadarrHistoryEventType::DownloadFolderImported,
languages: vec![Language {
id: 3,
name: "chinese".to_owned(),
}],
quality: QualityWrapper {
quality: Quality {
name: "SD - 720p".to_owned(),
},
},
date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()),
..RadarrHistoryItem::default()
},
RadarrHistoryItem {
id: 1,
source_title: "test 3".into(),
movie_id: 3,
event_type: RadarrHistoryEventType::MovieFileDeleted,
languages: vec![Language {
id: 1,
name: "english".to_owned(),
}],
quality: QualityWrapper {
quality: Quality {
name: "HD - 1080p".to_owned(),
},
},
date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()),
..RadarrHistoryItem::default()
},
]
}
}
+179
View File
@@ -0,0 +1,179 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_clear_errors};
use crate::matches_key;
use crate::models::Route;
use crate::models::radarr_models::RadarrHistoryItem;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, HISTORY_BLOCKS};
use crate::models::servarr_models::Language;
use crate::models::stateful_table::SortOption;
#[cfg(test)]
#[path = "history_handler_tests.rs"]
mod history_handler_tests;
pub(super) struct HistoryHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_radarr_block: ActiveRadarrBlock,
_context: Option<ActiveRadarrBlock>,
}
impl HistoryHandler<'_, '_> {}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for HistoryHandler<'a, 'b> {
fn handle(&mut self) {
let history_table_handling_config = TableHandlingConfig::new(ActiveRadarrBlock::History.into())
.sorting_block(ActiveRadarrBlock::HistorySortPrompt.into())
.sort_options(history_sorting_options())
.searching_block(ActiveRadarrBlock::SearchHistory.into())
.search_error_block(ActiveRadarrBlock::SearchHistoryError.into())
.search_field_fn(|history| &history.source_title.text)
.filtering_block(ActiveRadarrBlock::FilterHistory.into())
.filter_error_block(ActiveRadarrBlock::FilterHistoryError.into())
.filter_field_fn(|history| &history.source_title.text);
if !handle_table(
self,
|app| &mut app.data.radarr_data.history,
history_table_handling_config,
) {
self.handle_key_event();
}
}
fn accepts(active_block: ActiveRadarrBlock) -> bool {
HISTORY_BLOCKS.contains(&active_block)
}
fn new(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveRadarrBlock,
context: Option<ActiveRadarrBlock>,
) -> Self {
HistoryHandler {
key,
app,
active_radarr_block: active_block,
_context: 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.radarr_data.history.is_empty()
}
fn handle_scroll_up(&mut self) {}
fn handle_scroll_down(&mut self) {}
fn handle_home(&mut self) {}
fn handle_end(&mut self) {}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
if self.active_radarr_block == ActiveRadarrBlock::History {
handle_change_tab_left_right_keys(self.app, self.key)
}
}
fn handle_submit(&mut self) {
if self.active_radarr_block == ActiveRadarrBlock::History {
self
.app
.push_navigation_stack(ActiveRadarrBlock::HistoryItemDetails.into());
}
}
fn handle_esc(&mut self) {
if self.active_radarr_block == ActiveRadarrBlock::HistoryItemDetails {
self.app.pop_navigation_stack();
} else {
handle_clear_errors(self.app);
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
if self.active_radarr_block == ActiveRadarrBlock::History {
match self.key {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
}
}
}
fn app_mut(&mut self) -> &mut App<'b> {
self.app
}
fn current_route(&self) -> Route {
self.app.get_current_route()
}
}
pub(in crate::handlers::radarr_handlers) fn history_sorting_options()
-> Vec<SortOption<RadarrHistoryItem>> {
vec![
SortOption {
name: "Source Title",
cmp_fn: Some(|a, b| {
a.source_title
.text
.to_lowercase()
.cmp(&b.source_title.text.to_lowercase())
}),
},
SortOption {
name: "Event Type",
cmp_fn: Some(|a, b| {
a.event_type
.to_string()
.to_lowercase()
.cmp(&b.event_type.to_string().to_lowercase())
}),
},
SortOption {
name: "Language",
cmp_fn: Some(|a, b| {
let default_language = Language {
id: 1,
name: "_".to_owned(),
};
let language_a = a.languages.first().unwrap_or(&default_language);
let language_b = b.languages.first().unwrap_or(&default_language);
language_a.cmp(language_b)
}),
},
SortOption {
name: "Quality",
cmp_fn: Some(|a, b| {
a.quality
.quality
.name
.to_lowercase()
.cmp(&b.quality.quality.name.to_lowercase())
}),
},
SortOption {
name: "Date",
cmp_fn: Some(|a, b| a.date.cmp(&b.date)),
},
]
}
@@ -65,7 +65,7 @@ mod tests {
fn test_indexers_tab_left(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.data.radarr_data.main_tabs.set_index(5);
app.data.radarr_data.main_tabs.set_index(6);
IndexersHandler::new(
DEFAULT_KEYBINDINGS.left.key,
@@ -86,7 +86,7 @@ mod tests {
fn test_indexers_tab_right(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.data.radarr_data.main_tabs.set_index(5);
app.data.radarr_data.main_tabs.set_index(6);
IndexersHandler::new(
DEFAULT_KEYBINDINGS.right.key,
+5
View File
@@ -2,6 +2,7 @@ use crate::handlers::KeyEventHandler;
use crate::handlers::radarr_handlers::blocklist::BlocklistHandler;
use crate::handlers::radarr_handlers::collections::CollectionsHandler;
use crate::handlers::radarr_handlers::downloads::DownloadsHandler;
use crate::handlers::radarr_handlers::history::HistoryHandler;
use crate::handlers::radarr_handlers::indexers::IndexersHandler;
use crate::handlers::radarr_handlers::library::LibraryHandler;
use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler;
@@ -13,6 +14,7 @@ use crate::{App, Key, matches_key};
mod blocklist;
mod collections;
mod downloads;
mod history;
mod indexers;
mod library;
mod root_folders;
@@ -51,6 +53,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
_ if DownloadsHandler::accepts(self.active_radarr_block) => {
DownloadsHandler::new(self.key, self.app, self.active_radarr_block, self.context).handle()
}
_ if HistoryHandler::accepts(self.active_radarr_block) => {
HistoryHandler::new(self.key, self.app, self.active_radarr_block, self.context).handle()
}
_ if RootFoldersHandler::accepts(self.active_radarr_block) => {
RootFoldersHandler::new(self.key, self.app, self.active_radarr_block, self.context).handle()
}
@@ -16,10 +16,11 @@ mod tests {
#[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Collections)]
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Downloads)]
#[case(2, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Blocklist)]
#[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)]
#[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Indexers)]
#[case(5, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)]
#[case(6, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)]
#[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::History)]
#[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::RootFolders)]
#[case(5, ActiveRadarrBlock::History, ActiveRadarrBlock::Indexers)]
#[case(6, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)]
#[case(7, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)]
fn test_radarr_handler_change_tab_left_right_keys(
#[case] index: usize,
#[case] left_block: ActiveRadarrBlock,
@@ -51,10 +52,11 @@ mod tests {
#[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Collections)]
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Downloads)]
#[case(2, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Blocklist)]
#[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)]
#[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Indexers)]
#[case(5, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)]
#[case(6, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)]
#[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::History)]
#[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::RootFolders)]
#[case(5, ActiveRadarrBlock::History, ActiveRadarrBlock::Indexers)]
#[case(6, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)]
#[case(7, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)]
fn test_radarr_handler_change_tab_left_right_keys_alt_navigation(
#[case] index: usize,
#[case] left_block: ActiveRadarrBlock,
@@ -88,9 +90,10 @@ mod tests {
#[case(1, ActiveRadarrBlock::Collections)]
#[case(2, ActiveRadarrBlock::Downloads)]
#[case(3, ActiveRadarrBlock::Blocklist)]
#[case(4, ActiveRadarrBlock::RootFolders)]
#[case(5, ActiveRadarrBlock::Indexers)]
#[case(6, ActiveRadarrBlock::System)]
#[case(4, ActiveRadarrBlock::History)]
#[case(5, ActiveRadarrBlock::RootFolders)]
#[case(6, ActiveRadarrBlock::Indexers)]
#[case(7, ActiveRadarrBlock::System)]
fn test_radarr_handler_change_tab_left_right_keys_alt_navigation_no_op_when_ignoring_quit_key(
#[case] index: usize,
#[case] block: ActiveRadarrBlock,
@@ -281,6 +284,26 @@ mod tests {
);
}
#[rstest]
fn test_delegates_history_blocks_to_history_handler(
#[values(
ActiveRadarrBlock::History,
ActiveRadarrBlock::HistoryItemDetails,
ActiveRadarrBlock::HistorySortPrompt,
ActiveRadarrBlock::FilterHistory,
ActiveRadarrBlock::FilterHistoryError,
ActiveRadarrBlock::SearchHistory,
ActiveRadarrBlock::SearchHistoryError
)]
active_radarr_block: ActiveRadarrBlock,
) {
test_handler_delegation!(
RadarrHandler,
ActiveRadarrBlock::History,
active_radarr_block
);
}
#[test]
fn test_radarr_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
@@ -130,7 +130,7 @@ mod tests {
fn test_root_folders_tab_left(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.data.radarr_data.main_tabs.set_index(4);
app.data.radarr_data.main_tabs.set_index(5);
RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.left.key,
@@ -142,16 +142,16 @@ mod tests {
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
ActiveRadarrBlock::Blocklist.into()
ActiveRadarrBlock::History.into()
);
assert_navigation_pushed!(app, ActiveRadarrBlock::Blocklist.into());
assert_navigation_pushed!(app, ActiveRadarrBlock::History.into());
}
#[rstest]
fn test_root_folders_tab_right(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.data.radarr_data.main_tabs.set_index(4);
app.data.radarr_data.main_tabs.set_index(5);
RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.right.key,
@@ -26,7 +26,7 @@ mod tests {
fn test_system_tab_left(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.data.radarr_data.main_tabs.set_index(6);
app.data.radarr_data.main_tabs.set_index(7);
SystemHandler::new(
DEFAULT_KEYBINDINGS.left.key,
@@ -47,7 +47,7 @@ mod tests {
fn test_system_tab_right(#[values(true, false)] is_ready: bool) {
let mut app = App::test_default();
app.is_loading = is_ready;
app.data.radarr_data.main_tabs.set_index(6);
app.data.radarr_data.main_tabs.set_index(7);
SystemHandler::new(
DEFAULT_KEYBINDINGS.right.key,
+14
View File
@@ -301,6 +301,20 @@ pub struct EditArtistParams {
pub clear_tags: bool,
}
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AddLidarrRootFolderBody {
pub name: String,
pub path: String,
pub default_quality_profile_id: i64,
pub default_metadata_profile_id: i64,
pub default_monitor_option: MonitorType,
pub default_new_item_monitor_option: NewItemMonitorType,
pub default_tags: Vec<i64>,
#[serde(skip_serializing, skip_deserializing)]
pub tag_input_string: Option<String>,
}
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Album {
+64
View File
@@ -408,6 +408,69 @@ pub struct SystemStatus {
pub start_time: DateTime<Utc>,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RadarrHistoryWrapper {
pub records: Vec<RadarrHistoryItem>,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RadarrHistoryData {
pub indexer: Option<String>,
pub release_group: Option<String>,
pub nzb_info_url: Option<String>,
pub download_client: Option<String>,
pub download_client_name: Option<String>,
pub age: Option<String>,
pub published_date: Option<DateTime<Utc>>,
pub message: Option<String>,
pub reason: Option<String>,
pub dropped_path: Option<String>,
pub imported_path: Option<String>,
pub source_path: Option<String>,
pub path: Option<String>,
}
#[derive(
Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Display, EnumDisplayStyle,
)]
#[serde(rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum RadarrHistoryEventType {
#[default]
Unknown,
Grabbed,
#[display_style(name = "Download Folder Imported")]
DownloadFolderImported,
#[display_style(name = "Download Failed")]
DownloadFailed,
#[display_style(name = "Movie File Deleted")]
MovieFileDeleted,
#[display_style(name = "Movie Folder Imported")]
MovieFolderImported,
#[display_style(name = "Movie File Renamed")]
MovieFileRenamed,
#[display_style(name = "Download Ignored")]
DownloadIgnored,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RadarrHistoryItem {
#[serde(deserialize_with = "super::from_i64")]
pub id: i64,
pub source_title: HorizontallyScrollableText,
#[serde(deserialize_with = "super::from_i64")]
pub movie_id: i64,
pub quality: QualityWrapper,
pub languages: Vec<Language>,
pub date: DateTime<Utc>,
pub event_type: RadarrHistoryEventType,
#[serde(default)]
pub data: RadarrHistoryData,
}
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RadarrTask {
@@ -461,6 +524,7 @@ serde_enum_from!(
Credits(Vec<Credit>),
DiskSpaces(Vec<DiskSpace>),
DownloadsResponse(DownloadsResponse),
HistoryWrapper(RadarrHistoryWrapper),
HostConfig(HostConfig),
Indexers(Vec<Indexer>),
IndexerSettings(IndexerSettings),
+80
View File
@@ -3,6 +3,9 @@ mod tests {
use pretty_assertions::{assert_eq, assert_str_eq};
use serde_json::json;
use crate::models::radarr_models::{
RadarrHistoryEventType, RadarrHistoryItem, RadarrHistoryWrapper,
};
use crate::models::{
Serdeable,
radarr_models::{
@@ -61,6 +64,66 @@ mod tests {
assert_str_eq!(MovieMonitor::None.to_display_str(), "None");
}
#[test]
fn test_radarr_history_event_type_display() {
assert_str_eq!(RadarrHistoryEventType::Unknown.to_string(), "unknown");
assert_str_eq!(RadarrHistoryEventType::Grabbed.to_string(), "grabbed");
assert_str_eq!(
RadarrHistoryEventType::DownloadFolderImported.to_string(),
"downloadFolderImported"
);
assert_str_eq!(
RadarrHistoryEventType::DownloadFailed.to_string(),
"downloadFailed"
);
assert_str_eq!(
RadarrHistoryEventType::MovieFileDeleted.to_string(),
"movieFileDeleted"
);
assert_str_eq!(
RadarrHistoryEventType::MovieFolderImported.to_string(),
"movieFolderImported"
);
assert_str_eq!(
RadarrHistoryEventType::MovieFileRenamed.to_string(),
"movieFileRenamed"
);
assert_str_eq!(
RadarrHistoryEventType::DownloadIgnored.to_string(),
"downloadIgnored"
);
}
#[test]
fn test_radarr_history_event_type_to_display_str() {
assert_str_eq!(RadarrHistoryEventType::Unknown.to_display_str(), "Unknown");
assert_str_eq!(RadarrHistoryEventType::Grabbed.to_display_str(), "Grabbed");
assert_str_eq!(
RadarrHistoryEventType::DownloadFolderImported.to_display_str(),
"Download Folder Imported"
);
assert_str_eq!(
RadarrHistoryEventType::DownloadFailed.to_display_str(),
"Download Failed"
);
assert_str_eq!(
RadarrHistoryEventType::MovieFileDeleted.to_display_str(),
"Movie File Deleted"
);
assert_str_eq!(
RadarrHistoryEventType::MovieFolderImported.to_display_str(),
"Movie Folder Imported"
);
assert_str_eq!(
RadarrHistoryEventType::MovieFileRenamed.to_display_str(),
"Movie File Renamed"
);
assert_str_eq!(
RadarrHistoryEventType::DownloadIgnored.to_display_str(),
"Download Ignored"
);
}
#[test]
fn test_download_record_default_indexer_value() {
let json = r#"{
@@ -235,6 +298,23 @@ mod tests {
);
}
#[test]
fn test_radarr_serdeable_from_history_wrapper() {
let history_wrapper = RadarrHistoryWrapper {
records: vec![RadarrHistoryItem {
id: 1,
..RadarrHistoryItem::default()
}],
};
let radarr_serdeable: RadarrSerdeable = history_wrapper.clone().into();
assert_eq!(
radarr_serdeable,
RadarrSerdeable::HistoryWrapper(history_wrapper)
);
}
#[test]
fn test_radarr_serdeable_from_log_response() {
let log_response = LogResponse {
+86 -2
View File
@@ -1,7 +1,9 @@
use serde_json::Number;
use super::modals::{AddArtistModal, EditArtistModal};
use crate::app::context_clues::HISTORY_CONTEXT_CLUES;
use super::modals::{AddArtistModal, AddRootFolderModal, EditArtistModal};
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,
};
@@ -37,6 +39,7 @@ pub struct LidarrData<'a> {
pub add_artist_modal: Option<AddArtistModal>,
pub add_artist_search: Option<HorizontallyScrollableText>,
pub add_import_list_exclusion: bool,
pub add_root_folder_modal: Option<AddRootFolderModal>,
pub add_searched_artists: Option<StatefulTable<AddArtistSearchResult>>,
pub albums: StatefulTable<Album>,
pub artist_info_tabs: TabState,
@@ -107,6 +110,7 @@ impl<'a> Default for LidarrData<'a> {
add_artist_modal: None,
add_artist_search: None,
add_import_list_exclusion: false,
add_root_folder_modal: None,
add_searched_artists: None,
albums: StatefulTable::default(),
artists: StatefulTable::default(),
@@ -131,12 +135,24 @@ impl<'a> Default for LidarrData<'a> {
contextual_help: Some(&ARTISTS_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "Downloads".to_string(),
route: ActiveLidarrBlock::Downloads.into(),
contextual_help: Some(&DOWNLOADS_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "History".to_string(),
route: ActiveLidarrBlock::History.into(),
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(),
@@ -186,12 +202,32 @@ impl LidarrData<'_> {
.metadata_profile_list
.set_items(vec![metadata_profile().name]);
let mut add_root_folder_modal = AddRootFolderModal {
name: "Test Root Folder".into(),
path: "/nfs/music".into(),
tags: "test".into(),
..AddRootFolderModal::default()
};
add_root_folder_modal
.monitor_list
.set_items(Vec::from_iter(MonitorType::iter()));
add_root_folder_modal
.monitor_new_items_list
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
add_root_folder_modal
.quality_profile_list
.set_items(vec![quality_profile().name]);
add_root_folder_modal
.metadata_profile_list
.set_items(vec![metadata_profile().name]);
let mut lidarr_data = LidarrData {
delete_files: true,
disk_space_vec: vec![diskspace()],
quality_profile_map: quality_profile_map(),
metadata_profile_map: metadata_profile_map(),
edit_artist_modal: Some(edit_artist_modal),
add_root_folder_modal: Some(add_root_folder_modal),
add_artist_modal: Some(add_artist_modal),
tags_map: tags_map(),
..LidarrData::default()
@@ -243,6 +279,15 @@ pub enum ActiveLidarrBlock {
AddArtistSelectQualityProfile,
AddArtistSelectRootFolder,
AddArtistTagsInput,
AddRootFolderPrompt,
AddRootFolderConfirmPrompt,
AddRootFolderNameInput,
AddRootFolderPathInput,
AddRootFolderSelectMonitor,
AddRootFolderSelectMonitorNewItems,
AddRootFolderSelectQualityProfile,
AddRootFolderSelectMetadataProfile,
AddRootFolderTagsInput,
AutomaticallySearchArtistPrompt,
DeleteAlbumPrompt,
DeleteAlbumConfirmPrompt,
@@ -252,6 +297,9 @@ pub enum ActiveLidarrBlock {
DeleteArtistConfirmPrompt,
DeleteArtistToggleDeleteFile,
DeleteArtistToggleAddListExclusion,
DeleteDownloadPrompt,
DeleteRootFolderPrompt,
Downloads,
EditArtistPrompt,
EditArtistConfirmPrompt,
EditArtistPathInput,
@@ -267,6 +315,7 @@ pub enum ActiveLidarrBlock {
History,
HistoryItemDetails,
HistorySortPrompt,
RootFolders,
SearchAlbums,
SearchAlbumsError,
SearchArtists,
@@ -275,6 +324,7 @@ pub enum ActiveLidarrBlock {
SearchHistoryError,
UpdateAllArtistsPrompt,
UpdateAndScanArtistPrompt,
UpdateDownloadsPrompt,
}
pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [
@@ -295,6 +345,12 @@ pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [
ActiveLidarrBlock::UpdateAndScanArtistPrompt,
];
pub static DOWNLOADS_BLOCKS: [ActiveLidarrBlock; 3] = [
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
ActiveLidarrBlock::UpdateDownloadsPrompt,
];
pub static HISTORY_BLOCKS: [ActiveLidarrBlock; 7] = [
ActiveLidarrBlock::History,
ActiveLidarrBlock::HistoryItemDetails,
@@ -377,6 +433,34 @@ pub const EDIT_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
&[ActiveLidarrBlock::EditArtistConfirmPrompt],
];
pub const ROOT_FOLDERS_BLOCKS: [ActiveLidarrBlock; 2] = [
ActiveLidarrBlock::RootFolders,
ActiveLidarrBlock::DeleteRootFolderPrompt,
];
pub static ADD_ROOT_FOLDER_BLOCKS: [ActiveLidarrBlock; 9] = [
ActiveLidarrBlock::AddRootFolderPrompt,
ActiveLidarrBlock::AddRootFolderConfirmPrompt,
ActiveLidarrBlock::AddRootFolderNameInput,
ActiveLidarrBlock::AddRootFolderPathInput,
ActiveLidarrBlock::AddRootFolderSelectMonitor,
ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems,
ActiveLidarrBlock::AddRootFolderSelectQualityProfile,
ActiveLidarrBlock::AddRootFolderSelectMetadataProfile,
ActiveLidarrBlock::AddRootFolderTagsInput,
];
pub const ADD_ROOT_FOLDER_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[
&[ActiveLidarrBlock::AddRootFolderNameInput],
&[ActiveLidarrBlock::AddRootFolderPathInput],
&[ActiveLidarrBlock::AddRootFolderSelectMonitor],
&[ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems],
&[ActiveLidarrBlock::AddRootFolderSelectQualityProfile],
&[ActiveLidarrBlock::AddRootFolderSelectMetadataProfile],
&[ActiveLidarrBlock::AddRootFolderTagsInput],
&[ActiveLidarrBlock::AddRootFolderConfirmPrompt],
];
impl From<ActiveLidarrBlock> for Route {
fn from(active_lidarr_block: ActiveLidarrBlock) -> Route {
Route::Lidarr(active_lidarr_block, None)
@@ -1,6 +1,8 @@
#[cfg(test)]
mod tests {
use crate::app::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,
};
@@ -8,7 +10,8 @@ mod tests {
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ARTIST_DETAILS_BLOCKS, DELETE_ALBUM_BLOCKS,
DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS,
EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, HISTORY_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.add_root_folder_modal);
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(), 2);
assert_eq!(lidarr_data.main_tabs.tabs.len(), 4);
assert_str_eq!(lidarr_data.main_tabs.tabs[0].title, "Library");
assert_eq!(
@@ -159,17 +163,39 @@ mod tests {
);
assert_none!(lidarr_data.main_tabs.tabs[0].config);
assert_str_eq!(lidarr_data.main_tabs.tabs[1].title, "History");
assert_str_eq!(lidarr_data.main_tabs.tabs[1].title, "Downloads");
assert_eq!(
lidarr_data.main_tabs.tabs[1].route,
ActiveLidarrBlock::History.into()
ActiveLidarrBlock::Downloads.into()
);
assert_some_eq_x!(
&lidarr_data.main_tabs.tabs[1].contextual_help,
&HISTORY_CONTEXT_CLUES
&DOWNLOADS_CONTEXT_CLUES
);
assert_none!(lidarr_data.main_tabs.tabs[1].config);
assert_str_eq!(lidarr_data.main_tabs.tabs[2].title, "History");
assert_eq!(
lidarr_data.main_tabs.tabs[2].route,
ActiveLidarrBlock::History.into()
);
assert_some_eq_x!(
&lidarr_data.main_tabs.tabs[2].contextual_help,
&HISTORY_CONTEXT_CLUES
);
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!(
@@ -205,6 +231,14 @@ mod tests {
assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::UpdateAndScanArtistPrompt));
}
#[test]
fn test_downloads_blocks_contains_expected_blocks() {
assert_eq!(DOWNLOADS_BLOCKS.len(), 3);
assert!(DOWNLOADS_BLOCKS.contains(&ActiveLidarrBlock::Downloads));
assert!(DOWNLOADS_BLOCKS.contains(&ActiveLidarrBlock::DeleteDownloadPrompt));
assert!(DOWNLOADS_BLOCKS.contains(&ActiveLidarrBlock::UpdateDownloadsPrompt));
}
#[test]
fn test_history_blocks_contains_expected_blocks() {
assert_eq!(HISTORY_BLOCKS.len(), 7);
@@ -372,4 +406,30 @@ mod tests {
);
assert_none!(edit_artist_block_iter.next());
}
#[test]
fn test_root_folders_blocks_contents() {
assert_eq!(ROOT_FOLDERS_BLOCKS.len(), 2);
assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveLidarrBlock::RootFolders));
assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveLidarrBlock::DeleteRootFolderPrompt));
}
#[test]
fn test_add_root_folder_blocks_contents() {
use crate::models::servarr_data::lidarr::lidarr_data::ADD_ROOT_FOLDER_BLOCKS;
assert_eq!(ADD_ROOT_FOLDER_BLOCKS.len(), 9);
assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderPrompt));
assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderConfirmPrompt));
assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderNameInput));
assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderPathInput));
assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderSelectMonitor));
assert!(
ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems)
);
assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderSelectQualityProfile));
assert!(
ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderSelectMetadataProfile)
);
assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderTagsInput));
}
}
+32
View File
@@ -113,3 +113,35 @@ impl From<&LidarrData<'_>> for EditArtistModal {
edit_artist_modal
}
}
#[derive(Default)]
#[cfg_attr(test, derive(Debug))]
pub struct AddRootFolderModal {
pub name: HorizontallyScrollableText,
pub path: HorizontallyScrollableText,
pub monitor_list: StatefulList<MonitorType>,
pub monitor_new_items_list: StatefulList<NewItemMonitorType>,
pub quality_profile_list: StatefulList<String>,
pub metadata_profile_list: StatefulList<String>,
pub tags: HorizontallyScrollableText,
}
impl From<&LidarrData<'_>> for AddRootFolderModal {
fn from(lidarr_data: &LidarrData<'_>) -> AddRootFolderModal {
let mut add_root_folder_modal = AddRootFolderModal::default();
add_root_folder_modal
.monitor_list
.set_items(Vec::from_iter(MonitorType::iter()));
add_root_folder_modal
.monitor_new_items_list
.set_items(Vec::from_iter(NewItemMonitorType::iter()));
add_root_folder_modal
.quality_profile_list
.set_items(lidarr_data.sorted_quality_profile_names());
add_root_folder_modal
.metadata_profile_list
.set_items(lidarr_data.sorted_metadata_profile_names());
add_root_folder_modal
}
}
+4
View File
@@ -15,6 +15,7 @@ use crate::models::{HorizontallyScrollableText, ScrollableText};
mod modals_tests;
#[derive(Default)]
#[cfg_attr(test, derive(Debug))]
pub struct MovieDetailsModal {
pub movie_details: ScrollableText,
pub file_details: String,
@@ -97,6 +98,7 @@ impl From<&RadarrData<'_>> for EditIndexerModal {
}
#[derive(Default)]
#[cfg_attr(test, derive(Debug))]
pub struct EditMovieModal {
pub minimum_availability_list: StatefulList<MinimumAvailability>,
pub quality_profile_list: StatefulList<String>,
@@ -157,6 +159,7 @@ impl From<&RadarrData<'_>> for EditMovieModal {
}
#[derive(Default)]
#[cfg_attr(test, derive(Debug))]
pub struct AddMovieModal {
pub root_folder_list: StatefulList<RootFolder>,
pub monitor_list: StatefulList<MovieMonitor>,
@@ -186,6 +189,7 @@ impl From<&RadarrData<'_>> for AddMovieModal {
}
#[derive(Default)]
#[cfg_attr(test, derive(Debug))]
pub struct EditCollectionModal {
pub monitored: Option<bool>,
pub minimum_availability_list: StatefulList<MinimumAvailability>,
+32 -3
View File
@@ -1,5 +1,5 @@
use crate::app::context_clues::{
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::radarr::radarr_context_clues::{
@@ -8,7 +8,7 @@ use crate::app::radarr::radarr_context_clues::{
};
use crate::models::radarr_models::{
AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DownloadRecord,
IndexerSettings, Movie, RadarrTask,
IndexerSettings, Movie, RadarrHistoryItem, RadarrTask,
};
use crate::models::servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem};
use crate::models::servarr_data::radarr::modals::{
@@ -34,7 +34,8 @@ use {
crate::network::radarr_network::radarr_network_test_utils::test_utils::{
add_movie_search_result, blocklist_item, cast_credit, collection, collection_movie,
crew_credit, download_record, indexer, log_line, movie, movie_history_item,
quality_profile_map, tags_map, task, torrent_release, updates, usenet_release,
quality_profile_map, radarr_history_item, tags_map, task, torrent_release, updates,
usenet_release,
},
crate::network::servarr_test_utils::diskspace,
crate::network::servarr_test_utils::indexer_test_result,
@@ -62,6 +63,7 @@ pub struct RadarrData<'a> {
pub downloads: StatefulTable<DownloadRecord>,
pub indexers: StatefulTable<Indexer>,
pub blocklist: StatefulTable<BlocklistItem>,
pub history: StatefulTable<RadarrHistoryItem>,
pub quality_profile_map: BiMap<i64, String>,
pub tags_map: BiMap<i64, String>,
pub collections: StatefulTable<Collection>,
@@ -135,6 +137,7 @@ impl<'a> Default for RadarrData<'a> {
downloads: StatefulTable::default(),
indexers: StatefulTable::default(),
blocklist: StatefulTable::default(),
history: StatefulTable::default(),
quality_profile_map: BiMap::default(),
tags_map: BiMap::default(),
collections: StatefulTable::default(),
@@ -184,6 +187,12 @@ impl<'a> Default for RadarrData<'a> {
contextual_help: Some(&BLOCKLIST_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "History".to_string(),
route: ActiveRadarrBlock::History.into(),
contextual_help: Some(&HISTORY_CONTEXT_CLUES),
config: None,
},
TabRoute {
title: "Root Folders".to_string(),
route: ActiveRadarrBlock::RootFolders.into(),
@@ -387,6 +396,10 @@ impl RadarrData<'_> {
radarr_data.downloads.set_items(vec![download_record()]);
radarr_data.blocklist.set_items(vec![blocklist_item()]);
radarr_data.blocklist.sorting(vec![sort_option!(id)]);
radarr_data.history.set_items(vec![radarr_history_item()]);
radarr_data.history.sorting(vec![sort_option!(id)]);
radarr_data.history.search = Some("Something".into());
radarr_data.history.filter = Some("Something".into());
radarr_data.indexers.set_items(vec![indexer()]);
radarr_data.indexers.sorting(vec![sort_option!(id)]);
radarr_data.indexers.search = Some("Something".into());
@@ -420,6 +433,13 @@ pub enum ActiveRadarrBlock {
BlocklistClearAllItemsPrompt,
BlocklistItemDetails,
BlocklistSortPrompt,
History,
HistoryItemDetails,
HistorySortPrompt,
FilterHistory,
FilterHistoryError,
SearchHistory,
SearchHistoryError,
Collections,
CollectionsSortPrompt,
CollectionDetails,
@@ -538,6 +558,15 @@ pub static BLOCKLIST_BLOCKS: [ActiveRadarrBlock; 5] = [
ActiveRadarrBlock::BlocklistClearAllItemsPrompt,
ActiveRadarrBlock::BlocklistSortPrompt,
];
pub static HISTORY_BLOCKS: [ActiveRadarrBlock; 7] = [
ActiveRadarrBlock::History,
ActiveRadarrBlock::HistoryItemDetails,
ActiveRadarrBlock::HistorySortPrompt,
ActiveRadarrBlock::FilterHistory,
ActiveRadarrBlock::FilterHistoryError,
ActiveRadarrBlock::SearchHistory,
ActiveRadarrBlock::SearchHistoryError,
];
pub static ADD_MOVIE_BLOCKS: [ActiveRadarrBlock; 10] = [
ActiveRadarrBlock::AddMovieSearchInput,
ActiveRadarrBlock::AddMovieSearchResults,
@@ -2,8 +2,8 @@
mod tests {
mod radarr_data_tests {
use crate::app::context_clues::{
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::radarr::radarr_context_clues::{
COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
@@ -105,41 +105,42 @@ mod tests {
fn test_radarr_data_defaults() {
let radarr_data = RadarrData::default();
assert!(radarr_data.root_folders.items.is_empty());
assert_is_empty!(radarr_data.root_folders.items);
assert_eq!(radarr_data.disk_space_vec, Vec::new());
assert!(radarr_data.version.is_empty());
assert_is_empty!(radarr_data.version);
assert_eq!(radarr_data.start_time, <DateTime<Utc>>::default());
assert!(radarr_data.movies.is_empty());
assert_is_empty!(radarr_data.movies);
assert_eq!(radarr_data.selected_block, BlockSelectionState::default());
assert!(radarr_data.downloads.items.is_empty());
assert!(radarr_data.indexers.items.is_empty());
assert!(radarr_data.blocklist.items.is_empty());
assert!(radarr_data.quality_profile_map.is_empty());
assert!(radarr_data.tags_map.is_empty());
assert!(radarr_data.collections.items.is_empty());
assert!(radarr_data.collection_movies.items.is_empty());
assert!(radarr_data.logs.items.is_empty());
assert!(radarr_data.log_details.items.is_empty());
assert!(radarr_data.tasks.items.is_empty());
assert!(radarr_data.queued_events.items.is_empty());
assert!(radarr_data.updates.get_text().is_empty());
assert!(radarr_data.add_movie_search.is_none());
assert!(radarr_data.add_movie_modal.is_none());
assert!(radarr_data.add_searched_movies.is_none());
assert!(radarr_data.edit_movie_modal.is_none());
assert!(radarr_data.edit_collection_modal.is_none());
assert!(radarr_data.edit_root_folder.is_none());
assert!(radarr_data.edit_indexer_modal.is_none());
assert!(radarr_data.indexer_settings.is_none());
assert!(radarr_data.indexer_test_errors.is_none());
assert!(radarr_data.indexer_test_all_results.is_none());
assert!(radarr_data.movie_details_modal.is_none());
assert!(radarr_data.prompt_confirm_action.is_none());
assert_is_empty!(radarr_data.downloads.items);
assert_is_empty!(radarr_data.indexers.items);
assert_is_empty!(radarr_data.blocklist.items);
assert_is_empty!(radarr_data.history.items);
assert_is_empty!(radarr_data.quality_profile_map);
assert_is_empty!(radarr_data.tags_map);
assert_is_empty!(radarr_data.collections.items);
assert_is_empty!(radarr_data.collection_movies.items);
assert_is_empty!(radarr_data.logs.items);
assert_is_empty!(radarr_data.log_details.items);
assert_is_empty!(radarr_data.tasks.items);
assert_is_empty!(radarr_data.queued_events.items);
assert_is_empty!(radarr_data.updates.get_text());
assert_none!(&radarr_data.add_movie_search);
assert_none!(&radarr_data.add_movie_modal);
assert_none!(&radarr_data.add_searched_movies);
assert_none!(&radarr_data.edit_movie_modal);
assert_none!(&radarr_data.edit_collection_modal);
assert_none!(&radarr_data.edit_root_folder);
assert_none!(&radarr_data.edit_indexer_modal);
assert_none!(&radarr_data.indexer_settings);
assert_none!(&radarr_data.indexer_test_errors);
assert_none!(&radarr_data.indexer_test_all_results);
assert_none!(&radarr_data.movie_details_modal);
assert_none!(&radarr_data.prompt_confirm_action);
assert!(!radarr_data.prompt_confirm);
assert!(!radarr_data.delete_movie_files);
assert!(!radarr_data.add_list_exclusion);
assert_eq!(radarr_data.main_tabs.tabs.len(), 7);
assert_eq!(radarr_data.main_tabs.tabs.len(), 8);
assert_str_eq!(radarr_data.main_tabs.tabs[0].title, "Library");
assert_eq!(
@@ -189,42 +190,54 @@ mod tests {
);
assert_eq!(radarr_data.main_tabs.tabs[3].config, None);
assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "Root Folders");
assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "History");
assert_eq!(
radarr_data.main_tabs.tabs[4].route,
ActiveRadarrBlock::RootFolders.into()
ActiveRadarrBlock::History.into()
);
assert!(radarr_data.main_tabs.tabs[4].contextual_help.is_some());
assert_eq!(
radarr_data.main_tabs.tabs[4].contextual_help.unwrap(),
&ROOT_FOLDERS_CONTEXT_CLUES
&HISTORY_CONTEXT_CLUES
);
assert_eq!(radarr_data.main_tabs.tabs[4].config, None);
assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "Indexers");
assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "Root Folders");
assert_eq!(
radarr_data.main_tabs.tabs[5].route,
ActiveRadarrBlock::Indexers.into()
ActiveRadarrBlock::RootFolders.into()
);
assert!(radarr_data.main_tabs.tabs[5].contextual_help.is_some());
assert_eq!(
radarr_data.main_tabs.tabs[5].contextual_help.unwrap(),
&INDEXERS_CONTEXT_CLUES
&ROOT_FOLDERS_CONTEXT_CLUES
);
assert_eq!(radarr_data.main_tabs.tabs[5].config, None);
assert_str_eq!(radarr_data.main_tabs.tabs[6].title, "System");
assert_str_eq!(radarr_data.main_tabs.tabs[6].title, "Indexers");
assert_eq!(
radarr_data.main_tabs.tabs[6].route,
ActiveRadarrBlock::System.into()
ActiveRadarrBlock::Indexers.into()
);
assert!(radarr_data.main_tabs.tabs[6].contextual_help.is_some());
assert_eq!(
radarr_data.main_tabs.tabs[6].contextual_help.unwrap(),
&SYSTEM_CONTEXT_CLUES
&INDEXERS_CONTEXT_CLUES
);
assert_eq!(radarr_data.main_tabs.tabs[6].config, None);
assert_str_eq!(radarr_data.main_tabs.tabs[7].title, "System");
assert_eq!(
radarr_data.main_tabs.tabs[7].route,
ActiveRadarrBlock::System.into()
);
assert!(radarr_data.main_tabs.tabs[7].contextual_help.is_some());
assert_eq!(
radarr_data.main_tabs.tabs[7].contextual_help.unwrap(),
&SYSTEM_CONTEXT_CLUES
);
assert_eq!(radarr_data.main_tabs.tabs[7].config, None);
assert_eq!(radarr_data.movie_info_tabs.tabs.len(), 6);
assert_str_eq!(radarr_data.movie_info_tabs.tabs[0].title, "Details");
@@ -334,8 +347,8 @@ mod tests {
DELETE_MOVIE_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_COLLECTION_BLOCKS,
EDIT_COLLECTION_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS,
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, EDIT_MOVIE_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS,
INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, LIBRARY_BLOCKS,
MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS,
HISTORY_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS,
LIBRARY_BLOCKS, MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS,
};
#[test]
@@ -388,6 +401,18 @@ mod tests {
assert!(BLOCKLIST_BLOCKS.contains(&ActiveRadarrBlock::BlocklistSortPrompt));
}
#[test]
fn test_history_blocks_contents() {
assert_eq!(HISTORY_BLOCKS.len(), 7);
assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::History));
assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::HistoryItemDetails));
assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::HistorySortPrompt));
assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::FilterHistory));
assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::FilterHistoryError));
assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::SearchHistory));
assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::SearchHistoryError));
}
#[test]
fn test_add_movie_blocks_contents() {
assert_eq!(ADD_MOVIE_BLOCKS.len(), 10);
@@ -2,10 +2,37 @@
mod tests {
use crate::models::lidarr_models::{DownloadsResponse, LidarrSerdeable};
use crate::network::lidarr_network::LidarrEvent;
use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record;
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_delete_lidarr_download_event() {
let (mock, app, _server) = MockServarrApi::delete()
.path("/1")
.build_for(LidarrEvent::DeleteDownload(1))
.await;
app
.lock()
.await
.data
.lidarr_data
.downloads
.set_items(vec![download_record()]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
network
.handle_lidarr_event(LidarrEvent::DeleteDownload(1))
.await
.is_ok()
);
mock.assert_async().await;
}
#[tokio::test]
async fn test_handle_get_downloads_event() {
let downloads_json = json!({
@@ -40,4 +67,26 @@ mod tests {
assert_eq!(downloads_response, response);
assert!(!app.lock().await.data.lidarr_data.downloads.is_empty());
}
#[tokio::test]
async fn test_handle_update_lidarr_downloads_event() {
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(json!({
"name": "RefreshMonitoredDownloads"
}))
.returns(json!({}))
.build_for(LidarrEvent::UpdateDownloads)
.await;
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert!(
network
.handle_lidarr_event(LidarrEvent::UpdateDownloads)
.await
.is_ok()
);
mock.assert_async().await;
}
}
+44 -3
View File
@@ -1,15 +1,38 @@
use anyhow::Result;
use log::info;
use crate::models::lidarr_models::DownloadsResponse;
use crate::models::servarr_models::CommandBody;
use crate::network::lidarr_network::LidarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
use log::info;
use serde_json::Value;
#[cfg(test)]
#[path = "lidarr_downloads_network_tests.rs"]
mod lidarr_downloads_network_tests;
impl Network<'_, '_> {
pub(in crate::network::lidarr_network) async fn delete_lidarr_download(
&mut self,
download_id: i64,
) -> Result<()> {
let event = LidarrEvent::DeleteDownload(download_id);
info!("Deleting Lidarr download for download with id: {download_id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Delete,
None::<()>,
Some(format!("/{download_id}")),
None,
)
.await;
self
.handle_request::<(), ()>(request_props, |_, _| ())
.await
}
pub(in crate::network::lidarr_network) async fn get_lidarr_downloads(
&mut self,
count: u64,
@@ -37,4 +60,22 @@ impl Network<'_, '_> {
})
.await
}
pub(in crate::network::lidarr_network) async fn update_lidarr_downloads(
&mut self,
) -> Result<Value> {
info!("Updating Lidarr downloads");
let event = LidarrEvent::UpdateDownloads;
let body = CommandBody {
name: "RefreshMonitoredDownloads".to_owned(),
};
let request_props = self
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
.await;
self
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
.await
}
}
@@ -173,7 +173,7 @@ mod tests {
panic!("Expected LidarrHistoryWrapper")
};
mock.assert_async().await;
assert!(app.lock().await.data.lidarr_data.history.is_empty());
assert_is_empty!(app.lock().await.data.lidarr_data.history);
assert!(app.lock().await.data.lidarr_data.history.sort_asc);
assert_eq!(history, response);
}
@@ -194,6 +194,6 @@ mod tests {
.await;
mock.assert_async().await;
assert!(result.is_ok());
assert_ok!(result);
}
}
@@ -30,6 +30,13 @@ mod tests {
assert_str_eq!(event.resource(), "/artist");
}
#[rstest]
fn test_resource_downloads(
#[values(LidarrEvent::GetDownloads(0), LidarrEvent::DeleteDownload(0))] event: LidarrEvent,
) {
assert_str_eq!(event.resource(), "/queue");
}
#[rstest]
fn test_resource_history(#[values(LidarrEvent::GetHistory(0))] event: LidarrEvent) {
assert_str_eq!(event.resource(), "/history");
@@ -59,7 +66,8 @@ mod tests {
#[values(
LidarrEvent::UpdateAllArtists,
LidarrEvent::TriggerAutomaticArtistSearch(0),
LidarrEvent::UpdateAndScanArtist(0)
LidarrEvent::UpdateAndScanArtist(0),
LidarrEvent::UpdateDownloads
)]
event: LidarrEvent,
) {
@@ -79,12 +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::GetDownloads(500), "/queue")]
#[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")]
+28 -4
View File
@@ -3,7 +3,8 @@ use log::info;
use super::{NetworkEvent, NetworkResource};
use crate::models::lidarr_models::{
AddArtistBody, DeleteParams, EditArtistParams, LidarrSerdeable, MetadataProfile,
AddArtistBody, AddLidarrRootFolderBody, DeleteParams, EditArtistParams, LidarrSerdeable,
MetadataProfile,
};
use crate::models::servarr_models::{QualityProfile, Tag};
use crate::network::{Network, RequestMethod};
@@ -25,9 +26,12 @@ pub mod lidarr_network_test_utils;
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum LidarrEvent {
AddArtist(AddArtistBody),
AddRootFolder(AddLidarrRootFolderBody),
AddTag(String),
DeleteAlbum(DeleteParams),
DeleteArtist(DeleteParams),
DeleteDownload(i64),
DeleteRootFolder(i64),
DeleteTag(i64),
EditArtist(EditArtistParams),
GetAlbums(i64),
@@ -52,6 +56,7 @@ pub enum LidarrEvent {
TriggerAutomaticArtistSearch(i64),
UpdateAllArtists,
UpdateAndScanArtist(i64),
UpdateDownloads,
}
impl NetworkResource for LidarrEvent {
@@ -69,16 +74,19 @@ impl NetworkResource for LidarrEvent {
| LidarrEvent::GetAlbumDetails(_)
| LidarrEvent::DeleteAlbum(_) => "/album",
LidarrEvent::GetDiskSpace => "/diskspace",
LidarrEvent::GetDownloads(_) => "/queue",
LidarrEvent::GetDownloads(_) | LidarrEvent::DeleteDownload(_) => "/queue",
LidarrEvent::GetHistory(_) => "/history",
LidarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed",
LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host",
LidarrEvent::TriggerAutomaticArtistSearch(_)
| LidarrEvent::UpdateAllArtists
| LidarrEvent::UpdateAndScanArtist(_) => "/command",
| LidarrEvent::UpdateAndScanArtist(_)
| 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",
@@ -99,12 +107,24 @@ impl Network<'_, '_> {
) -> Result<LidarrSerdeable> {
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)
}
LidarrEvent::DeleteArtist(params) => {
self.delete_artist(params).await.map(LidarrSerdeable::from)
}
LidarrEvent::DeleteDownload(download_id) => self
.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
@@ -182,6 +202,10 @@ impl Network<'_, '_> {
.map(LidarrSerdeable::from),
LidarrEvent::EditArtist(params) => self.edit_artist(params).await.map(LidarrSerdeable::from),
LidarrEvent::AddArtist(body) => self.add_artist(body).await.map(LidarrSerdeable::from),
LidarrEvent::UpdateDownloads => self
.update_lidarr_downloads()
.await
.map(LidarrSerdeable::from),
}
}
@@ -1,19 +1,125 @@
#[cfg(test)]
mod tests {
use crate::models::lidarr_models::LidarrSerdeable;
use crate::models::lidarr_models::{
AddLidarrRootFolderBody, LidarrSerdeable, MonitorType, NewItemMonitorType,
};
use crate::models::servarr_models::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 bimap::BiMap;
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 = AddLidarrRootFolderBody {
name: "Music".to_owned(),
path: "/nfs/test".to_owned(),
default_quality_profile_id: 1,
default_metadata_profile_id: 1,
default_monitor_option: MonitorType::All,
default_new_item_monitor_option: NewItemMonitorType::All,
default_tags: vec![],
tag_input_string: Some("usenet, testing".to_owned()),
};
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(json!({
"name": "Music",
"path": "/nfs/test",
"defaultQualityProfileId": 1,
"defaultMetadataProfileId": 1,
"defaultMonitorOption": "all",
"defaultNewItemMonitorOption": "all",
"defaultTags": [1, 2]
}))
.returns(json!({}))
.build_for(LidarrEvent::AddRootFolder(
expected_add_root_folder_body.clone(),
))
.await;
app.lock().await.data.lidarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
app.lock().await.server_tabs.set_index(2);
let mut network = test_network(&app);
assert_ok!(
network
.handle_lidarr_event(LidarrEvent::AddRootFolder(expected_add_root_folder_body))
.await
);
mock.assert_async().await;
assert_none!(app.lock().await.data.lidarr_data.add_root_folder_modal);
}
#[tokio::test]
async fn test_handle_add_lidarr_root_folder_event_does_not_overwrite_default_tags_vec_when_tag_input_string_is_none()
{
let expected_add_root_folder_body = AddLidarrRootFolderBody {
name: "Music".to_owned(),
path: "/nfs/test".to_owned(),
default_quality_profile_id: 1,
default_metadata_profile_id: 1,
default_monitor_option: MonitorType::All,
default_new_item_monitor_option: NewItemMonitorType::All,
default_tags: vec![1, 2],
tag_input_string: None,
};
let (mock, app, _server) = MockServarrApi::post()
.with_request_body(json!({
"name": "Music",
"path": "/nfs/test",
"defaultQualityProfileId": 1,
"defaultMetadataProfileId": 1,
"defaultMonitorOption": "all",
"defaultNewItemMonitorOption": "all",
"defaultTags": [1, 2]
}))
.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_ok!(
network
.handle_lidarr_event(LidarrEvent::AddRootFolder(expected_add_root_folder_body))
.await
);
mock.assert_async().await;
assert_none!(app.lock().await.data.lidarr_data.add_root_folder_modal);
}
#[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<RootFolder> = serde_json::from_value(root_folders_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
@@ -34,6 +140,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()]
);
}
}
+54 -3
View File
@@ -1,15 +1,66 @@
use anyhow::Result;
use log::info;
use crate::models::lidarr_models::AddLidarrRootFolderBody;
use crate::models::servarr_models::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,
mut add_root_folder_body: AddLidarrRootFolderBody,
) -> Result<Value> {
info!("Adding new root folder to Lidarr");
if let Some(tag_input_str) = add_root_folder_body.tag_input_string.as_ref() {
let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await;
add_root_folder_body.default_tags = tag_ids_vec;
}
let event = LidarrEvent::AddRootFolder(AddLidarrRootFolderBody::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::<AddLidarrRootFolderBody, Value>(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<Vec<RootFolder>> {
+63
View File
@@ -0,0 +1,63 @@
use crate::models::Route;
use crate::models::radarr_models::RadarrHistoryWrapper;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::network::radarr_network::RadarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
use log::info;
use serde_json::Value;
#[cfg(test)]
#[path = "radarr_history_network_tests.rs"]
mod radarr_history_network_tests;
impl Network<'_, '_> {
pub(in crate::network::radarr_network) async fn get_radarr_history(
&mut self,
events: u64,
) -> Result<RadarrHistoryWrapper> {
info!("Fetching all Radarr history events");
let event = RadarrEvent::GetHistory(events);
let params = format!("pageSize={events}&sortDirection=descending&sortKey=date");
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params))
.await;
self
.handle_request::<(), RadarrHistoryWrapper>(request_props, |history_response, mut app| {
if !matches!(
app.get_current_route(),
Route::Radarr(ActiveRadarrBlock::HistorySortPrompt, _)
) {
let mut history_vec = history_response.records;
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
app.data.radarr_data.history.set_items(history_vec);
app.data.radarr_data.history.apply_sorting_toggle(false);
}
})
.await
}
pub(in crate::network::radarr_network) async fn mark_radarr_history_item_as_failed(
&mut self,
history_item_id: i64,
) -> Result<Value> {
info!("Marking the Radarr history item with ID: {history_item_id} as 'failed'");
let event = RadarrEvent::MarkHistoryItemAsFailed(history_item_id);
let request_props = self
.request_props_from(
event,
RequestMethod::Post,
None,
Some(format!("/{history_item_id}")),
None,
)
.await;
self
.handle_request::<(), Value>(request_props, |_, _| ())
.await
}
}
@@ -0,0 +1,196 @@
#[cfg(test)]
mod tests {
use crate::models::radarr_models::{RadarrHistoryItem, RadarrHistoryWrapper, RadarrSerdeable};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::stateful_table::SortOption;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use crate::network::radarr_network::RadarrEvent;
use crate::network::radarr_network::radarr_network_test_utils::test_utils::radarr_history_item;
use pretty_assertions::assert_eq;
use rstest::rstest;
use serde_json::json;
#[rstest]
#[tokio::test]
async fn test_handle_get_radarr_history_event(#[values(true, false)] use_custom_sorting: bool) {
let history_json = json!({"records": [{
"id": 123,
"sourceTitle": "z movie",
"movieId": 1007,
"quality": { "quality": { "name": "HD - 1080p" } },
"languages": [{ "id": 1, "name": "English" }],
"date": "2022-12-30T07:37:56Z",
"eventType": "grabbed",
"data": {
"indexer": "DrunkenSlug (Prowlarr)",
"releaseGroup": "SPARKS",
"downloadClient": "transmission",
}
},
{
"id": 456,
"sourceTitle": "A Movie",
"movieId": 2001,
"quality": { "quality": { "name": "HD - 1080p" } },
"languages": [{ "id": 1, "name": "English" }],
"date": "2022-12-30T07:37:56Z",
"eventType": "grabbed",
"data": {
"indexer": "DrunkenSlug (Prowlarr)",
"releaseGroup": "SPARKS",
"downloadClient": "transmission",
}
}]});
let response: RadarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap();
let mut expected_history_items = vec![
RadarrHistoryItem {
id: 123,
movie_id: 1007,
source_title: "z movie".into(),
..radarr_history_item()
},
RadarrHistoryItem {
id: 456,
movie_id: 2001,
source_title: "A Movie".into(),
..radarr_history_item()
},
];
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("pageSize=500&sortDirection=descending&sortKey=date")
.build_for(RadarrEvent::GetHistory(500))
.await;
app.lock().await.data.radarr_data.history.sort_asc = true;
if use_custom_sorting {
let cmp_fn = |a: &RadarrHistoryItem, b: &RadarrHistoryItem| {
a.source_title
.text
.to_lowercase()
.cmp(&b.source_title.text.to_lowercase())
};
expected_history_items.sort_by(cmp_fn);
let history_sort_option = SortOption {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
app
.lock()
.await
.data
.radarr_data
.history
.sorting(vec![history_sort_option]);
}
let mut network = test_network(&app);
let RadarrSerdeable::HistoryWrapper(history) = network
.handle_radarr_event(RadarrEvent::GetHistory(500))
.await
.unwrap()
else {
panic!("Expected HistoryWrapper")
};
mock.assert_async().await;
assert_eq!(
app.lock().await.data.radarr_data.history.items,
expected_history_items
);
assert!(app.lock().await.data.radarr_data.history.sort_asc);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_get_radarr_history_event_no_op_when_user_is_selecting_sort_options() {
let history_json = json!({"records": [{
"id": 123,
"sourceTitle": "z movie",
"movieId": 1007,
"quality": { "quality": { "name": "Bluray-1080p" } },
"languages": [{ "id": 1, "name": "English" }],
"date": "2024-02-10T07:28:45Z",
"eventType": "grabbed",
"data": {
"indexer": "DrunkenSlug (Prowlarr)",
"releaseGroup": "SPARKS"
}
},
{
"id": 456,
"sourceTitle": "A Movie",
"movieId": 2001,
"quality": { "quality": { "name": "Bluray-1080p" } },
"languages": [{ "id": 1, "name": "English" }],
"date": "2024-02-10T07:28:45Z",
"eventType": "grabbed",
"data": {
"indexer": "DrunkenSlug (Prowlarr)",
"releaseGroup": "SPARKS"
}
}]});
let response: RadarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("pageSize=500&sortDirection=descending&sortKey=date")
.build_for(RadarrEvent::GetHistory(500))
.await;
app.lock().await.data.radarr_data.history.sort_asc = true;
app
.lock()
.await
.push_navigation_stack(ActiveRadarrBlock::HistorySortPrompt.into());
let cmp_fn = |a: &RadarrHistoryItem, b: &RadarrHistoryItem| {
a.source_title
.text
.to_lowercase()
.cmp(&b.source_title.text.to_lowercase())
};
let history_sort_option = SortOption {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
app
.lock()
.await
.data
.radarr_data
.history
.sorting(vec![history_sort_option]);
let mut network = test_network(&app);
let RadarrSerdeable::HistoryWrapper(history) = network
.handle_radarr_event(RadarrEvent::GetHistory(500))
.await
.unwrap()
else {
panic!("Expected HistoryWrapper")
};
mock.assert_async().await;
assert_is_empty!(app.lock().await.data.radarr_data.history);
assert!(app.lock().await.data.radarr_data.history.sort_asc);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_mark_radarr_history_item_as_failed_event() {
let expected_history_item_id = 1;
let (mock, app, _server) = MockServarrApi::post()
.returns(json!({}))
.path("/1")
.build_for(RadarrEvent::MarkHistoryItemAsFailed(
expected_history_item_id,
))
.await;
let mut network = test_network(&app);
let result = network
.handle_radarr_event(RadarrEvent::MarkHistoryItemAsFailed(
expected_history_item_id,
))
.await;
mock.assert_async().await;
assert_ok!(result);
}
}
+13
View File
@@ -16,6 +16,7 @@ use super::NetworkResource;
mod blocklist;
mod collections;
mod downloads;
mod history;
mod indexers;
mod library;
mod root_folders;
@@ -47,10 +48,12 @@ pub enum RadarrEvent {
GetBlocklist,
GetCollections,
GetDownloads(u64),
GetHistory(u64),
GetHostConfig,
GetIndexers,
GetAllIndexerSettings,
GetLogs(u64),
MarkHistoryItemAsFailed(i64),
GetMovieCredits(i64),
GetMovieDetails(i64),
GetMovieHistory(i64),
@@ -86,7 +89,9 @@ impl NetworkResource for RadarrEvent {
RadarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000",
RadarrEvent::GetCollections | RadarrEvent::EditCollection(_) => "/collection",
RadarrEvent::GetDownloads(_) | RadarrEvent::DeleteDownload(_) => "/queue",
RadarrEvent::GetHistory(_) => "/history",
RadarrEvent::GetHostConfig | RadarrEvent::GetSecurityConfig => "/config/host",
RadarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed",
RadarrEvent::GetIndexers | RadarrEvent::EditIndexer(_) | RadarrEvent::DeleteIndexer(_) => {
"/indexer"
}
@@ -199,6 +204,10 @@ impl Network<'_, '_> {
.get_radarr_downloads(count)
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetHistory(events) => self
.get_radarr_history(events)
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetHostConfig => self
.get_radarr_host_config()
.await
@@ -208,6 +217,10 @@ impl Network<'_, '_> {
.get_radarr_logs(events)
.await
.map(RadarrSerdeable::from),
RadarrEvent::MarkHistoryItemAsFailed(history_item_id) => self
.mark_radarr_history_item_as_failed(history_item_id)
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetMovieCredits(movie_id) => {
self.get_credits(movie_id).await.map(RadarrSerdeable::from)
}
@@ -3,8 +3,8 @@ pub mod test_utils {
use crate::models::radarr_models::{
AddMovieSearchResult, BlocklistItem, BlocklistItemMovie, Collection, CollectionMovie, Credit,
CreditType, DownloadRecord, DownloadsResponse, IndexerSettings, MediaInfo, MinimumAvailability,
Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName,
Rating, RatingsList,
Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrHistoryData, RadarrHistoryEventType,
RadarrHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName, Rating, RatingsList,
};
use crate::models::servarr_models::{
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
@@ -313,6 +313,24 @@ pub mod test_utils {
}
}
pub fn radarr_history_item() -> RadarrHistoryItem {
RadarrHistoryItem {
id: 1,
source_title: HorizontallyScrollableText::from("Test"),
movie_id: 1,
quality: quality_wrapper(),
languages: vec![language()],
date: DateTime::from(DateTime::parse_from_rfc3339("2022-12-30T07:37:56Z").unwrap()),
event_type: RadarrHistoryEventType::Grabbed,
data: RadarrHistoryData {
indexer: Some("DrunkenSlug (Prowlarr)".to_owned()),
release_group: Some("SPARKS".to_owned()),
download_client: Some("transmission".to_owned()),
..RadarrHistoryData::default()
},
}
}
pub fn download_record() -> DownloadRecord {
DownloadRecord {
title: "Test Download Title".to_owned(),
@@ -136,7 +136,9 @@ mod test {
#[case(RadarrEvent::ClearBlocklist, "/blocklist/bulk")]
#[case(RadarrEvent::DeleteBlocklistItem(1), "/blocklist")]
#[case(RadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")]
#[case(RadarrEvent::GetHistory(500), "/history")]
#[case(RadarrEvent::GetLogs(500), "/log")]
#[case(RadarrEvent::MarkHistoryItemAsFailed(1), "/history/failed")]
#[case(RadarrEvent::SearchNewMovie(String::new()), "/movie/lookup")]
#[case(RadarrEvent::GetMovieCredits(0), "/credit")]
#[case(RadarrEvent::GetMovieHistory(0), "/history/movie")]
@@ -0,0 +1,72 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::app::App;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS};
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::downloads::DownloadsUi;
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
#[test]
fn test_downloads_ui_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if DOWNLOADS_BLOCKS.contains(&active_lidarr_block) {
assert!(DownloadsUi::accepts(active_lidarr_block.into()));
} else {
assert!(!DownloadsUi::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_downloads_ui_renders_loading() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
DownloadsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_downloads_ui_renders_empty_downloads() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveLidarrBlock::Downloads.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
DownloadsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[rstest]
fn test_downloads_ui_renders(
#[values(
ActiveLidarrBlock::Downloads,
ActiveLidarrBlock::DeleteDownloadPrompt,
ActiveLidarrBlock::UpdateDownloadsPrompt
)]
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| {
DownloadsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(format!("downloads_ui_{active_lidarr_block}"), output);
}
}
}
+146
View File
@@ -0,0 +1,146 @@
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect};
use ratatui::widgets::{Cell, Row};
use crate::app::App;
use crate::models::lidarr_models::DownloadRecord;
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS};
use crate::models::{HorizontallyScrollableText, Route};
use crate::ui::DrawUi;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{get_width_from_percentage, 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::utils::convert_f64_to_gb;
#[cfg(test)]
#[path = "downloads_ui_tests.rs"]
mod downloads_ui_tests;
pub(super) struct DownloadsUi;
impl DrawUi for DownloadsUi {
fn accepts(route: Route) -> bool {
if let Route::Lidarr(active_lidarr_block, _) = route {
return DOWNLOADS_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_downloads(f, app, area);
match active_lidarr_block {
ActiveLidarrBlock::DeleteDownloadPrompt => {
let prompt = format!(
"Do you really want to delete this download: \n{}?",
app.data.lidarr_data.downloads.current_selection().title
);
let confirmation_prompt = ConfirmationPrompt::new()
.title("Cancel Download")
.prompt(&prompt)
.yes_no_value(app.data.lidarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
ActiveLidarrBlock::UpdateDownloadsPrompt => {
let confirmation_prompt = ConfirmationPrompt::new()
.title("Update Downloads")
.prompt("Do you want to update your downloads?")
.yes_no_value(app.data.lidarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
_ => (),
}
}
}
}
fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let current_selection = if app.data.lidarr_data.downloads.items.is_empty() {
DownloadRecord::default()
} else {
app.data.lidarr_data.downloads.current_selection().clone()
};
let downloads_row_mapping = |download_record: &DownloadRecord| {
let DownloadRecord {
title,
size,
sizeleft,
download_client,
indexer,
output_path,
..
} = download_record;
if output_path.is_some() {
output_path.as_ref().unwrap().scroll_left_or_reset(
get_width_from_percentage(area, 18),
current_selection == *download_record,
app.ui_scroll_tick_count == 0,
);
}
let percent = if *size == 0.0 {
0.0
} else {
1f64 - (*sizeleft / *size)
};
let file_size: f64 = convert_f64_to_gb(*size);
Row::new(vec![
Cell::from(title.to_owned()),
Cell::from(format!("{:.0}%", percent * 100.0)),
Cell::from(format!("{file_size:.2} GB")),
Cell::from(
output_path
.as_ref()
.unwrap_or(&HorizontallyScrollableText::default())
.to_string(),
),
Cell::from(indexer.to_owned()),
Cell::from(
download_client
.as_ref()
.unwrap_or(&String::new())
.to_owned(),
),
])
.primary()
};
let downloads_table = ManagarrTable::new(
Some(&mut app.data.lidarr_data.downloads),
downloads_row_mapping,
)
.block(layout_block_top_border())
.loading(app.is_loading)
.headers([
"Title",
"Percent Complete",
"Size",
"Output Path",
"Indexer",
"Download Client",
])
.constraints([
Constraint::Percentage(30),
Constraint::Percentage(11),
Constraint::Percentage(11),
Constraint::Percentage(18),
Constraint::Percentage(17),
Constraint::Percentage(13),
]);
f.render_widget(downloads_table, area);
}
@@ -0,0 +1,38 @@
---
source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Title Percent Complete Size Output Path Indexer Download Client
=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission
╭──────────────────── Cancel Download ────────────────────╮
│ Do you really want to delete this download: │
│ Test download title? │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -0,0 +1,7 @@
---
source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Title Percent Complete Size Output Path Indexer Download Client
=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission
@@ -0,0 +1,38 @@
---
source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Title Percent Complete Size Output Path Indexer Download Client
=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission
╭─────────────────── Update Downloads ────────────────────╮
│ Do you want to update your downloads? │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭────────────────────────────╮╭───────────────────────────╮│
││ Yes ││ No ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -0,0 +1,5 @@
---
source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,8 @@
---
source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Loading ...
+27
View File
@@ -13,4 +13,31 @@ mod tests {
assert!(LidarrUi::accepts(Route::Lidarr(lidarr_block, None)));
}
}
mod snapshot_tests {
use super::*;
use crate::app::App;
use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app};
use rstest::rstest;
#[rstest]
#[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,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
app.data.lidarr_data.main_tabs.set_index(index);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
LidarrUi::draw(f, app, f.area());
});
insta::assert_snapshot!(format!("lidarr_tabs_{active_lidarr_block}"), output);
}
}
}
+33 -24
View File
@@ -32,9 +32,9 @@ pub(super) fn create_history_event_details(history_item: LidarrHistoryItem) -> V
} = data;
let mut lines = vec![
Line::from(format!("Source Title: {}", source_title.text)),
Line::from(format!("Source Title: {}", source_title.text.trim_start())),
Line::from(format!("Event Type: {event_type}")),
Line::from(format!("Quality: {}", quality.quality.name)),
Line::from(format!("Quality: {}", quality.quality.name.trim_start())),
Line::from(format!("Date: {date}")),
];
@@ -42,19 +42,19 @@ pub(super) fn create_history_event_details(history_item: LidarrHistoryItem) -> V
LidarrHistoryEventType::Grabbed => {
lines.push(Line::from(format!(
"Indexer: {}",
indexer.unwrap_or_default()
indexer.unwrap_or_default().trim_start()
)));
lines.push(Line::from(format!(
"NZB Info URL: {}",
nzb_info_url.unwrap_or_default()
nzb_info_url.unwrap_or_default().trim_start()
)));
lines.push(Line::from(format!(
"Release Group: {}",
release_group.unwrap_or_default()
release_group.unwrap_or_default().trim_start()
)));
lines.push(Line::from(format!(
"Age: {} days",
age.unwrap_or("0".to_owned())
age.unwrap_or("0".to_owned()).trim_start()
)));
lines.push(Line::from(format!(
"Published Date: {}",
@@ -62,86 +62,95 @@ pub(super) fn create_history_event_details(history_item: LidarrHistoryItem) -> V
)));
lines.push(Line::from(format!(
"Download Client: {}",
download_client_name.unwrap_or(download_client.unwrap_or_default())
download_client_name
.unwrap_or(download_client.unwrap_or_default())
.trim_start()
)));
}
LidarrHistoryEventType::DownloadImported => {
lines.push(Line::from(format!(
"Release Group: {}",
release_group.unwrap_or_default()
release_group.unwrap_or_default().trim_start()
)));
}
LidarrHistoryEventType::DownloadFailed => {
lines.push(Line::from(format!(
"Download Client: {}",
download_client_name.unwrap_or(download_client.unwrap_or_default())
download_client_name
.unwrap_or(download_client.unwrap_or_default())
.trim_start()
)));
lines.push(Line::from(format!(
"Message: {}",
message.unwrap_or_default()
message.unwrap_or_default().trim_start()
)));
lines.push(Line::from(format!(
"Release Group: {}",
release_group.unwrap_or_default()
release_group.unwrap_or_default().trim_start()
)));
lines.push(Line::from(format!(
"Indexer: {}",
indexer.unwrap_or_default()
indexer.unwrap_or_default().trim_start()
)));
}
LidarrHistoryEventType::TrackFileDeleted => {
lines.push(Line::from(format!(
"Reason: {}",
reason.unwrap_or_default()
reason.unwrap_or_default().trim_start()
)));
lines.push(Line::from(format!(
"Release Group: {}",
release_group.unwrap_or_default()
release_group.unwrap_or_default().trim_start()
)));
}
LidarrHistoryEventType::TrackFileImported => {
lines.push(Line::from(format!(
"Dropped Path: {}",
dropped_path.unwrap_or_default()
dropped_path.unwrap_or_default().trim_start()
)));
lines.push(Line::from(format!(
"Imported Path: {}",
imported_path.unwrap_or_default()
imported_path.unwrap_or_default().trim_start()
)));
lines.push(Line::from(format!(
"Download Client: {}",
download_client_name.unwrap_or(download_client.unwrap_or_default())
download_client_name
.unwrap_or(download_client.unwrap_or_default())
.trim_start()
)));
lines.push(Line::from(format!(
"Release Group: {}",
release_group.unwrap_or_default()
release_group.unwrap_or_default().trim_start()
)));
}
LidarrHistoryEventType::TrackFileRenamed => {
lines.push(Line::from(format!(
"Source Path: {}",
source_path.unwrap_or_default()
source_path.unwrap_or_default().trim_start()
)));
lines.push(Line::from(format!(
"Path: {}",
path.unwrap_or_default().trim_start()
)));
lines.push(Line::from(format!("Path: {}", path.unwrap_or_default())));
lines.push(Line::from(format!(
"Release Group: {}",
release_group.unwrap_or_default()
release_group.unwrap_or_default().trim_start()
)));
}
LidarrHistoryEventType::TrackFileRetagged => {
lines.push(Line::from(format!(
"Release Group: {}",
release_group.unwrap_or_default()
release_group.unwrap_or_default().trim_start()
)));
}
LidarrHistoryEventType::AlbumImportIncomplete => {
lines.push(Line::from(format!(
"Status Messages: {}",
status_messages.unwrap_or_default()
status_messages.unwrap_or_default().trim_start()
)));
lines.push(Line::from(format!(
"Release Group: {}",
release_group.unwrap_or_default()
release_group.unwrap_or_default().trim_start()
)));
}
_ => {
+92 -56
View File
@@ -26,29 +26,35 @@ mod tests {
assert_eq!(
result[0],
Line::from(format!("Source Title: {}", source_title.text))
Line::from(format!("Source Title: {}", source_title.text.trim_start()))
);
assert_eq!(result[1], Line::from(format!("Event Type: {event_type}")));
assert_eq!(
result[2],
Line::from(format!("Quality: {}", quality.quality.name))
Line::from(format!("Quality: {}", quality.quality.name.trim_start()))
);
assert_eq!(result[3], Line::from(format!("Date: {date}")));
assert_eq!(
result[4],
Line::from(format!("Indexer: {}", data.indexer.unwrap()))
Line::from(format!("Indexer: {}", data.indexer.unwrap().trim_start()))
);
assert_eq!(
result[5],
Line::from(format!("NZB Info URL: {}", data.nzb_info_url.unwrap()))
Line::from(format!(
"NZB Info URL: {}",
data.nzb_info_url.unwrap().trim_start()
))
);
assert_eq!(
result[6],
Line::from(format!("Release Group: {}", data.release_group.unwrap()))
Line::from(format!(
"Release Group: {}",
data.release_group.unwrap().trim_start()
))
);
assert_eq!(
result[7],
Line::from(format!("Age: {} days", data.age.unwrap()))
Line::from(format!("Age: {} days", data.age.unwrap().trim_start()))
);
assert_eq!(
result[8],
@@ -58,7 +64,7 @@ mod tests {
result[9],
Line::from(format!(
"Download Client: {}",
data.download_client_name.unwrap()
data.download_client_name.unwrap().trim_start()
))
);
assert_eq!(result.len(), 10);
@@ -68,7 +74,7 @@ mod tests {
fn test_create_history_event_details_grabbed_uses_download_client_as_fallback() {
let mut history_item = lidarr_history_item(LidarrHistoryEventType::Grabbed);
history_item.data.download_client_name = None;
history_item.data.download_client = Some("Fallback Client".to_owned());
history_item.data.download_client = Some("\nFallback Client".to_owned());
let result = create_history_event_details(history_item);
@@ -91,17 +97,20 @@ mod tests {
assert_eq!(
result[0],
Line::from(format!("Source Title: {}", source_title.text))
Line::from(format!("Source Title: {}", source_title.text.trim_start()))
);
assert_eq!(result[1], Line::from(format!("Event Type: {event_type}")));
assert_eq!(
result[2],
Line::from(format!("Quality: {}", quality.quality.name))
Line::from(format!("Quality: {}", quality.quality.name.trim_start()))
);
assert_eq!(result[3], Line::from(format!("Date: {date}")));
assert_eq!(
result[4],
Line::from(format!("Release Group: {}", data.release_group.unwrap()))
Line::from(format!(
"Release Group: {}",
data.release_group.unwrap().trim_start()
))
);
assert_eq!(result.len(), 5);
}
@@ -122,32 +131,35 @@ mod tests {
assert_eq!(
result[0],
Line::from(format!("Source Title: {}", source_title.text))
Line::from(format!("Source Title: {}", source_title.text.trim_start()))
);
assert_eq!(result[1], Line::from(format!("Event Type: {event_type}")));
assert_eq!(
result[2],
Line::from(format!("Quality: {}", quality.quality.name))
Line::from(format!("Quality: {}", quality.quality.name.trim_start()))
);
assert_eq!(result[3], Line::from(format!("Date: {date}")));
assert_eq!(
result[4],
Line::from(format!(
"Download Client: {}",
data.download_client_name.unwrap()
data.download_client_name.unwrap().trim_start()
))
);
assert_eq!(
result[5],
Line::from(format!("Message: {}", data.message.unwrap()))
Line::from(format!("Message: {}", data.message.unwrap().trim_start()))
);
assert_eq!(
result[6],
Line::from(format!("Release Group: {}", data.release_group.unwrap()))
Line::from(format!(
"Release Group: {}",
data.release_group.unwrap().trim_start()
))
);
assert_eq!(
result[7],
Line::from(format!("Indexer: {}", data.indexer.unwrap()))
Line::from(format!("Indexer: {}", data.indexer.unwrap().trim_start()))
);
assert_eq!(result.len(), 8);
}
@@ -168,21 +180,24 @@ mod tests {
assert_eq!(
result[0],
Line::from(format!("Source Title: {}", source_title.text))
Line::from(format!("Source Title: {}", source_title.text.trim_start()))
);
assert_eq!(result[1], Line::from(format!("Event Type: {event_type}")));
assert_eq!(
result[2],
Line::from(format!("Quality: {}", quality.quality.name))
Line::from(format!("Quality: {}", quality.quality.name.trim_start()))
);
assert_eq!(result[3], Line::from(format!("Date: {date}")));
assert_eq!(
result[4],
Line::from(format!("Reason: {}", data.reason.unwrap()))
Line::from(format!("Reason: {}", data.reason.unwrap().trim_start()))
);
assert_eq!(
result[5],
Line::from(format!("Release Group: {}", data.release_group.unwrap()))
Line::from(format!(
"Release Group: {}",
data.release_group.unwrap().trim_start()
))
);
assert_eq!(result.len(), 6);
}
@@ -203,32 +218,41 @@ mod tests {
assert_eq!(
result[0],
Line::from(format!("Source Title: {}", source_title.text))
Line::from(format!("Source Title: {}", source_title.text.trim_start()))
);
assert_eq!(result[1], Line::from(format!("Event Type: {event_type}")));
assert_eq!(
result[2],
Line::from(format!("Quality: {}", quality.quality.name))
Line::from(format!("Quality: {}", quality.quality.name.trim_start()))
);
assert_eq!(result[3], Line::from(format!("Date: {date}")));
assert_eq!(
result[4],
Line::from(format!("Dropped Path: {}", data.dropped_path.unwrap()))
Line::from(format!(
"Dropped Path: {}",
data.dropped_path.unwrap().trim_start()
))
);
assert_eq!(
result[5],
Line::from(format!("Imported Path: {}", data.imported_path.unwrap()))
Line::from(format!(
"Imported Path: {}",
data.imported_path.unwrap().trim_start()
))
);
assert_eq!(
result[6],
Line::from(format!(
"Download Client: {}",
data.download_client_name.unwrap()
data.download_client_name.unwrap().trim_start()
))
);
assert_eq!(
result[7],
Line::from(format!("Release Group: {}", data.release_group.unwrap()))
Line::from(format!(
"Release Group: {}",
data.release_group.unwrap().trim_start()
))
);
assert_eq!(result.len(), 8);
}
@@ -249,25 +273,31 @@ mod tests {
assert_eq!(
result[0],
Line::from(format!("Source Title: {}", source_title.text))
Line::from(format!("Source Title: {}", source_title.text.trim_start()))
);
assert_eq!(result[1], Line::from(format!("Event Type: {event_type}")));
assert_eq!(
result[2],
Line::from(format!("Quality: {}", quality.quality.name))
Line::from(format!("Quality: {}", quality.quality.name.trim_start()))
);
assert_eq!(result[3], Line::from(format!("Date: {date}")));
assert_eq!(
result[4],
Line::from(format!("Source Path: {}", data.source_path.unwrap()))
Line::from(format!(
"Source Path: {}",
data.source_path.unwrap().trim_start()
))
);
assert_eq!(
result[5],
Line::from(format!("Path: {}", data.path.unwrap()))
Line::from(format!("Path: {}", data.path.unwrap().trim_start()))
);
assert_eq!(
result[6],
Line::from(format!("Release Group: {}", data.release_group.unwrap()))
Line::from(format!(
"Release Group: {}",
data.release_group.unwrap().trim_start()
))
);
assert_eq!(result.len(), 7);
}
@@ -288,17 +318,20 @@ mod tests {
assert_eq!(
result[0],
Line::from(format!("Source Title: {}", source_title.text))
Line::from(format!("Source Title: {}", source_title.text.trim_start()))
);
assert_eq!(result[1], Line::from(format!("Event Type: {event_type}")));
assert_eq!(
result[2],
Line::from(format!("Quality: {}", quality.quality.name))
Line::from(format!("Quality: {}", quality.quality.name.trim_start()))
);
assert_eq!(result[3], Line::from(format!("Date: {date}")));
assert_eq!(
result[4],
Line::from(format!("Release Group: {}", data.release_group.unwrap()))
Line::from(format!(
"Release Group: {}",
data.release_group.unwrap().trim_start()
))
);
assert_eq!(result.len(), 5);
}
@@ -319,24 +352,27 @@ mod tests {
assert_eq!(
result[0],
Line::from(format!("Source Title: {}", source_title.text))
Line::from(format!("Source Title: {}", source_title.text.trim_start()))
);
assert_eq!(result[1], Line::from(format!("Event Type: {event_type}")));
assert_eq!(
result[2],
Line::from(format!("Quality: {}", quality.quality.name))
Line::from(format!("Quality: {}", quality.quality.name.trim_start()))
);
assert_eq!(result[3], Line::from(format!("Date: {date}")));
assert_eq!(
result[4],
Line::from(format!(
"Status Messages: {}",
data.status_messages.unwrap()
data.status_messages.unwrap().trim_start()
))
);
assert_eq!(
result[5],
Line::from(format!("Release Group: {}", data.release_group.unwrap()))
Line::from(format!(
"Release Group: {}",
data.release_group.unwrap().trim_start()
))
);
assert_eq!(result.len(), 6);
}
@@ -356,12 +392,12 @@ mod tests {
assert_eq!(
result[0],
Line::from(format!("Source Title: {}", source_title.text))
Line::from(format!("Source Title: {}", source_title.text.trim_start()))
);
assert_eq!(result[1], Line::from(format!("Event Type: {event_type}")));
assert_eq!(
result[2],
Line::from(format!("Quality: {}", quality.quality.name))
Line::from(format!("Quality: {}", quality.quality.name.trim_start()))
);
assert_eq!(result[3], Line::from(format!("Date: {date}")));
assert_eq!(result[4], Line::from("No additional details available."));
@@ -386,13 +422,13 @@ mod tests {
fn lidarr_history_item(event_type: LidarrHistoryEventType) -> LidarrHistoryItem {
LidarrHistoryItem {
id: 1,
source_title: "Test Album - Artist Name".into(),
source_title: "\nTest Album - Artist Name".into(),
album_id: 100,
artist_id: 10,
event_type,
quality: QualityWrapper {
quality: Quality {
name: "FLAC".to_owned(),
name: "\nFLAC".to_owned(),
},
},
date: Utc::now(),
@@ -402,20 +438,20 @@ mod tests {
fn lidarr_history_data() -> LidarrHistoryData {
LidarrHistoryData {
indexer: Some("Test Indexer".to_owned()),
release_group: Some("Test Release Group".to_owned()),
nzb_info_url: Some("https://test.url".to_owned()),
download_client_name: Some("Test Download Client".to_owned()),
download_client: Some("Fallback Download Client".to_owned()),
age: Some("7".to_owned()),
indexer: Some("\nTest Indexer".to_owned()),
release_group: Some("\nTest Release Group".to_owned()),
nzb_info_url: Some("\nhttps://test.url".to_owned()),
download_client_name: Some("\nTest Download Client".to_owned()),
download_client: Some("\nFallback Download Client".to_owned()),
age: Some("\n7".to_owned()),
published_date: Some(Utc::now()),
message: Some("Test failure message".to_owned()),
reason: Some("Test deletion reason".to_owned()),
dropped_path: Some("/downloads/completed/album".to_owned()),
imported_path: Some("/music/artist/album".to_owned()),
source_path: Some("/music/artist/old_album_name".to_owned()),
path: Some("/music/artist/new_album_name".to_owned()),
status_messages: Some("Missing tracks: 1, 2, 3".to_owned()),
message: Some("\nTest failure message".to_owned()),
reason: Some("\nTest deletion reason".to_owned()),
dropped_path: Some("\n/downloads/completed/album".to_owned()),
imported_path: Some("\n/music/artist/album".to_owned()),
source_path: Some("\n/music/artist/old_album_name".to_owned()),
path: Some("\n/music/artist/new_album_name".to_owned()),
status_messages: Some("\nMissing tracks: 1, 2, 3".to_owned()),
}
}
}
+14 -9
View File
@@ -15,6 +15,16 @@ use ratatui::{
widgets::Paragraph,
};
use super::{
DrawUi, draw_tabs,
styles::ManagarrStyle,
utils::{
borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block,
},
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,
@@ -27,15 +37,7 @@ use crate::{
utils::convert_to_gb,
};
use super::{
DrawUi, draw_tabs,
styles::ManagarrStyle,
utils::{
borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block,
},
widgets::loading_block::LoadingBlock,
};
mod downloads;
mod history;
mod library;
mod lidarr_ui_utils;
@@ -43,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;
@@ -57,7 +60,9 @@ impl DrawUi for LidarrUi {
match route {
_ 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),
_ => (),
}
}
@@ -0,0 +1,245 @@
use std::sync::atomic::Ordering;
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect};
use ratatui::prelude::Layout;
use ratatui::widgets::ListItem;
use crate::app::App;
use crate::models::Route;
use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock};
use crate::models::servarr_data::lidarr::modals::AddRootFolderModal;
use crate::render_selectable_input_box;
use crate::ui::utils::title_block_centered;
use crate::ui::widgets::button::Button;
use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::widgets::selectable_list::SelectableList;
use crate::ui::{DrawUi, draw_popup};
#[cfg(test)]
#[path = "add_root_folder_ui_tests.rs"]
mod add_root_folder_ui_tests;
pub(super) struct AddRootFolderUi;
impl DrawUi for AddRootFolderUi {
fn accepts(route: Route) -> bool {
let Route::Lidarr(active_lidarr_block, _) = route else {
return false;
};
ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block)
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
let draw_add_root_folder_prompt =
|f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| {
draw_add_root_folder_confirmation_prompt(f, app, prompt_area);
match active_lidarr_block {
ActiveLidarrBlock::AddRootFolderSelectMonitor => {
draw_add_root_folder_select_monitor_popup(f, app);
}
ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => {
draw_add_root_folder_select_monitor_new_items_popup(f, app);
}
ActiveLidarrBlock::AddRootFolderSelectQualityProfile => {
draw_add_root_folder_select_quality_profile_popup(f, app);
}
ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => {
draw_add_root_folder_select_metadata_profile_popup(f, app);
}
_ => (),
}
};
draw_popup(f, app, draw_add_root_folder_prompt, Size::Long);
}
}
}
fn draw_add_root_folder_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let title = "Add Root Folder";
f.render_widget(title_block_centered(title), area);
let yes_no_value = app.data.lidarr_data.prompt_confirm;
let selected_block = app.data.lidarr_data.selected_block.get_active_block();
let highlight_yes_no = selected_block == ActiveLidarrBlock::AddRootFolderConfirmPrompt;
let AddRootFolderModal {
name,
path,
monitor_list,
monitor_new_items_list,
quality_profile_list,
metadata_profile_list,
tags,
} = app
.data
.lidarr_data
.add_root_folder_modal
.as_ref()
.expect("add_root_folder_modal must exist in this context");
let selected_monitor = monitor_list.current_selection();
let selected_monitor_new_items = monitor_new_items_list.current_selection();
let selected_quality_profile = quality_profile_list.current_selection();
let selected_metadata_profile = metadata_profile_list.current_selection();
let [
_,
name_area,
path_area,
monitor_area,
monitor_new_items_area,
quality_profile_area,
metadata_profile_area,
tags_area,
_,
buttons_area,
] = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(3),
])
.margin(1)
.areas(area);
let [save_area, cancel_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(buttons_area);
let monitor_drop_down_button = Button::default()
.title(selected_monitor.to_display_str())
.label("Monitor")
.icon("")
.selected(selected_block == ActiveLidarrBlock::AddRootFolderSelectMonitor);
let monitor_new_items_drop_down_button = Button::default()
.title(selected_monitor_new_items.to_display_str())
.label("Monitor New Albums")
.icon("")
.selected(selected_block == ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems);
let quality_profile_drop_down_button = Button::default()
.title(selected_quality_profile)
.label("Quality Profile")
.icon("")
.selected(selected_block == ActiveLidarrBlock::AddRootFolderSelectQualityProfile);
let metadata_profile_drop_down_button = Button::default()
.title(selected_metadata_profile)
.label("Metadata Profile")
.icon("")
.selected(selected_block == ActiveLidarrBlock::AddRootFolderSelectMetadataProfile);
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
let name_input_box = InputBox::new(&name.text)
.offset(name.offset.load(Ordering::SeqCst))
.label("Name")
.highlighted(selected_block == ActiveLidarrBlock::AddRootFolderNameInput)
.selected(active_lidarr_block == ActiveLidarrBlock::AddRootFolderNameInput);
let path_input_box = InputBox::new(&path.text)
.offset(path.offset.load(Ordering::SeqCst))
.label("Path")
.highlighted(selected_block == ActiveLidarrBlock::AddRootFolderPathInput)
.selected(active_lidarr_block == ActiveLidarrBlock::AddRootFolderPathInput);
let tags_input_box = InputBox::new(&tags.text)
.offset(tags.offset.load(Ordering::SeqCst))
.label("Tags")
.highlighted(selected_block == ActiveLidarrBlock::AddRootFolderTagsInput)
.selected(active_lidarr_block == ActiveLidarrBlock::AddRootFolderTagsInput);
match active_lidarr_block {
ActiveLidarrBlock::AddRootFolderNameInput => name_input_box.show_cursor(f, name_area),
ActiveLidarrBlock::AddRootFolderPathInput => path_input_box.show_cursor(f, path_area),
ActiveLidarrBlock::AddRootFolderTagsInput => tags_input_box.show_cursor(f, tags_area),
_ => (),
}
render_selectable_input_box!(name_input_box, f, name_area);
render_selectable_input_box!(path_input_box, f, path_area);
render_selectable_input_box!(tags_input_box, f, tags_area);
}
let save_button = Button::default()
.title("Save")
.selected(yes_no_value && highlight_yes_no);
let cancel_button = Button::default()
.title("Cancel")
.selected(!yes_no_value && highlight_yes_no);
f.render_widget(monitor_drop_down_button, monitor_area);
f.render_widget(monitor_new_items_drop_down_button, monitor_new_items_area);
f.render_widget(quality_profile_drop_down_button, quality_profile_area);
f.render_widget(metadata_profile_drop_down_button, metadata_profile_area);
f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area);
}
fn draw_add_root_folder_select_monitor_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let monitor_list = SelectableList::new(
&mut app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.expect("add_root_folder_modal must exist in this context")
.monitor_list,
|monitor_type| ListItem::new(monitor_type.to_display_str().to_owned()),
);
let popup = Popup::new(monitor_list).size(Size::Dropdown);
f.render_widget(popup, f.area());
}
fn draw_add_root_folder_select_monitor_new_items_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let monitor_new_items_list = SelectableList::new(
&mut app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.expect("add_root_folder_modal must exist in this context")
.monitor_new_items_list,
|monitor_type| ListItem::new(monitor_type.to_display_str().to_owned()),
);
let popup = Popup::new(monitor_new_items_list).size(Size::Dropdown);
f.render_widget(popup, f.area());
}
fn draw_add_root_folder_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let quality_profile_list = SelectableList::new(
&mut app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.expect("add_root_folder_modal must exist in this context")
.quality_profile_list,
|quality_profile| ListItem::new(quality_profile.clone()),
);
let popup = Popup::new(quality_profile_list).size(Size::Dropdown);
f.render_widget(popup, f.area());
}
fn draw_add_root_folder_select_metadata_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let metadata_profile_list = SelectableList::new(
&mut app
.data
.lidarr_data
.add_root_folder_modal
.as_mut()
.expect("add_root_folder_modal must exist in this context")
.metadata_profile_list,
|metadata_profile| ListItem::new(metadata_profile.clone()),
);
let popup = Popup::new(metadata_profile_list).size(Size::Dropdown);
f.render_widget(popup, f.area());
}
@@ -0,0 +1,51 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::app::App;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ROOT_FOLDER_BLOCKS, ADD_ROOT_FOLDER_SELECTION_BLOCKS, ActiveLidarrBlock,
};
use crate::ui::DrawUi;
use crate::ui::lidarr_ui::root_folders::add_root_folder_ui::AddRootFolderUi;
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
#[test]
fn test_add_root_folder_ui_accepts() {
ActiveLidarrBlock::iter().for_each(|active_lidarr_block| {
if ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) {
assert!(AddRootFolderUi::accepts(active_lidarr_block.into()));
} else {
assert!(!AddRootFolderUi::accepts(active_lidarr_block.into()));
}
});
}
mod snapshot_tests {
use crate::ui::ui_test_utils::test_utils::TerminalSize;
use rstest::rstest;
use super::*;
#[rstest]
#[case(ActiveLidarrBlock::AddRootFolderPrompt)]
#[case(ActiveLidarrBlock::AddRootFolderConfirmPrompt)]
#[case(ActiveLidarrBlock::AddRootFolderSelectMonitor)]
#[case(ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems)]
#[case(ActiveLidarrBlock::AddRootFolderSelectQualityProfile)]
#[case(ActiveLidarrBlock::AddRootFolderSelectMetadataProfile)]
fn test_add_root_folder_ui_renders(#[case] active_lidarr_block: ActiveLidarrBlock) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_lidarr_block.into());
app.data.lidarr_data.selected_block =
BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS);
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
AddRootFolderUi::draw(f, app, f.area());
});
insta::assert_snapshot!(format!("add_root_folder_{active_lidarr_block}"), output);
}
}
}
+102
View File
@@ -0,0 +1,102 @@
use add_root_folder_ui::AddRootFolderUi;
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::{
ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, ROOT_FOLDERS_BLOCKS,
};
use crate::models::servarr_models::RootFolder;
use crate::ui::DrawUi;
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::utils::convert_to_gb;
mod add_root_folder_ui;
#[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)
|| ADD_ROOT_FOLDER_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);
if ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) {
AddRootFolderUi::draw(f, app, area);
} else if active_lidarr_block == 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);
}
@@ -0,0 +1,101 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::app::App;
use crate::models::servarr_data::lidarr::lidarr_data::{
ADD_ROOT_FOLDER_BLOCKS, 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)
|| ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block)
{
assert!(RootFoldersUi::accepts(active_lidarr_block.into()));
} else {
assert!(!RootFoldersUi::accepts(active_lidarr_block.into()));
}
});
}
mod snapshot_tests {
use super::*;
use crate::models::BlockSelectionState;
use crate::models::servarr_data::lidarr::lidarr_data::ADD_ROOT_FOLDER_SELECTION_BLOCKS;
use crate::ui::ui_test_utils::test_utils::TerminalSize;
use rstest::rstest;
#[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::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);
}
#[rstest]
fn test_root_folders_ui_renders_add_root_folders_popup_over_root_folders_table(
#[values(
ActiveLidarrBlock::AddRootFolderPrompt,
ActiveLidarrBlock::AddRootFolderConfirmPrompt,
ActiveLidarrBlock::AddRootFolderSelectMonitor,
ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems,
ActiveLidarrBlock::AddRootFolderSelectQualityProfile,
ActiveLidarrBlock::AddRootFolderSelectMetadataProfile
)]
active_lidarr_block: ActiveLidarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.data.lidarr_data.selected_block =
BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS);
app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into());
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);
}
}
}
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs
expression: output
---
╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────────────────────────────────────────╮ │
│ Name: │Test Root Folder │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Path: │/nfs/music │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Monitor: │All Albums ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Monitor New Albums: │All Albums ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Quality Profile: │Lossless ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Metadata Profile: │Standard ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Tags: │test │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
││ Save ││ Cancel ││
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs
expression: output
---
╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────────────────────────────────────────╮ │
│ Name: │Test Root Folder │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Path: │/nfs/music │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Monitor: │All Albums ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Monitor New Albums: │All Albums ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Quality Profile: │Lossless ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Metadata Profile: │Standard ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Tags: │test │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
││ Save ││ Cancel ││
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs
expression: output
---
╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────────────────────────────────────────╮ │
│ Name: │Test Root Folder │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Path: │/nfs/music │ │
│ ╭───────────────────────────────╮──────────────────────────────╯ │
│ │Standard │──────────────────────────────╮ │
│ │ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Monitor│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Qual│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Metad│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ ╰───────────────────────────────╯ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
││ Save ││ Cancel ││
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs
expression: output
---
╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────────────────────────────────────────╮ │
│ Name: │Test Root Folder │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Path: │/nfs/music │ │
│ ╭───────────────────────────────╮──────────────────────────────╯ │
│ │All Albums │──────────────────────────────╮ │
│ │Future Albums │ ▼ │ │
│ │Missing Albums │──────────────────────────────╯ │
│ │Existing Albums │──────────────────────────────╮ │
│ Monitor│First Album │ ▼ │ │
│ │Latest Album │──────────────────────────────╯ │
│ │None │──────────────────────────────╮ │
│ Qual│Unknown │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Metad│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ ╰───────────────────────────────╯ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
││ Save ││ Cancel ││
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs
expression: output
---
╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────────────────────────────────────────╮ │
│ Name: │Test Root Folder │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Path: │/nfs/music │ │
│ ╭───────────────────────────────╮──────────────────────────────╯ │
│ │All Albums │──────────────────────────────╮ │
│ │No New Albums │ ▼ │ │
│ │New Albums │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Monitor│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Qual│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Metad│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ ╰───────────────────────────────╯ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
││ Save ││ Cancel ││
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs
expression: output
---
╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────────────────────────────────────────╮ │
│ Name: │Test Root Folder │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Path: │/nfs/music │ │
│ ╭───────────────────────────────╮──────────────────────────────╯ │
│ │Lossless │──────────────────────────────╮ │
│ │ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Monitor│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Qual│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Metad│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ ╰───────────────────────────────╯ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
││ Save ││ Cancel ││
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
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 ───────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────────────────────────────────────────╮ │
│ Name: │Test Root Folder │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Path: │/nfs/music │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Monitor: │All Albums ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Monitor New Albums: │All Albums ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Quality Profile: │Lossless ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Metadata Profile: │Standard ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Tags: │test │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
││ Save ││ Cancel ││
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
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 ───────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────────────────────────────────────────╮ │
│ Name: │Test Root Folder │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Path: │/nfs/music │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Monitor: │All Albums ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Monitor New Albums: │All Albums ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Quality Profile: │Lossless ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Metadata Profile: │Standard ▼ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Tags: │test │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
││ Save ││ Cancel ││
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
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 ───────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────────────────────────────────────────╮ │
│ Name: │Test Root Folder │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Path: │/nfs/music │ │
│ ╭───────────────────────────────╮──────────────────────────────╯ │
│ │Standard │──────────────────────────────╮ │
│ │ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Monitor│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Qual│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Metad│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ ╰───────────────────────────────╯ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
││ Save ││ Cancel ││
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
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 ───────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────────────────────────────────────────╮ │
│ Name: │Test Root Folder │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Path: │/nfs/music │ │
│ ╭───────────────────────────────╮──────────────────────────────╯ │
│ │All Albums │──────────────────────────────╮ │
│ │Future Albums │ ▼ │ │
│ │Missing Albums │──────────────────────────────╯ │
│ │Existing Albums │──────────────────────────────╮ │
│ Monitor│First Album │ ▼ │ │
│ │Latest Album │──────────────────────────────╯ │
│ │None │──────────────────────────────╮ │
│ Qual│Unknown │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Metad│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ ╰───────────────────────────────╯ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
││ Save ││ Cancel ││
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
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 ───────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────────────────────────────────────────╮ │
│ Name: │Test Root Folder │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Path: │/nfs/music │ │
│ ╭───────────────────────────────╮──────────────────────────────╯ │
│ │All Albums │──────────────────────────────╮ │
│ │No New Albums │ ▼ │ │
│ │New Albums │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Monitor│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Qual│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Metad│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ ╰───────────────────────────────╯ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
││ Save ││ Cancel ││
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
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 ───────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ ╭─────────────────────────────────────────────────╮ │
│ Name: │Test Root Folder │ │
│ ╰─────────────────────────────────────────────────╯ │
│ ╭─────────────────────────────────────────────────╮ │
│ Path: │/nfs/music │ │
│ ╭───────────────────────────────╮──────────────────────────────╯ │
│ │Lossless │──────────────────────────────╮ │
│ │ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Monitor│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Qual│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ Metad│ │ ▼ │ │
│ │ │──────────────────────────────╯ │
│ │ │──────────────────────────────╮ │
│ ╰───────────────────────────────╯ │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ │
│ │
│ │
│ │
│ │
│╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│
││ Save ││ Cancel ││
│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -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 ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -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
@@ -0,0 +1,5 @@
---
source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,8 @@
---
source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Loading ...
@@ -0,0 +1,54 @@
---
source: src/ui/lidarr_ui/lidarr_ui_tests.rs
expression: output
---
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ 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 │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,54 @@
---
source: src/ui/lidarr_ui/lidarr_ui_tests.rs
expression: output
---
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ 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 │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,54 @@
---
source: src/ui/lidarr_ui/lidarr_ui_tests.rs
expression: output
---
╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Downloads │ History │ Root Folders │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Source Title ▼ Event Type Quality Date │
│=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -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 │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,79 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::app::App;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, HISTORY_BLOCKS};
use crate::ui::DrawUi;
use crate::ui::radarr_ui::history::HistoryUi;
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
#[test]
fn test_history_ui_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if HISTORY_BLOCKS.contains(&active_radarr_block) {
assert!(HistoryUi::accepts(active_radarr_block.into()));
} else {
assert!(!HistoryUi::accepts(active_radarr_block.into()));
}
});
}
mod snapshot_tests {
use crate::ui::ui_test_utils::test_utils::TerminalSize;
use rstest::rstest;
use super::*;
#[test]
fn test_history_ui_renders_loading() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::History.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
HistoryUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[rstest]
fn test_history_ui_renders_empty(
#[values(ActiveRadarrBlock::History, ActiveRadarrBlock::HistoryItemDetails)]
active_radarr_block: ActiveRadarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_radarr_block.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
HistoryUi::draw(f, app, f.area());
});
insta::assert_snapshot!(format!("loading_history_tab_{active_radarr_block}"), output);
}
#[rstest]
fn test_history_ui_renders(
#[values(
ActiveRadarrBlock::History,
ActiveRadarrBlock::HistoryItemDetails,
ActiveRadarrBlock::HistorySortPrompt,
ActiveRadarrBlock::FilterHistory,
ActiveRadarrBlock::FilterHistoryError,
ActiveRadarrBlock::SearchHistory,
ActiveRadarrBlock::SearchHistoryError
)]
active_radarr_block: ActiveRadarrBlock,
) {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(active_radarr_block.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
HistoryUi::draw(f, app, f.area());
});
insta::assert_snapshot!(format!("history_tab_{active_radarr_block}"), output);
}
}
}
+129
View File
@@ -0,0 +1,129 @@
use crate::app::App;
use crate::models::Route;
use crate::models::radarr_models::RadarrHistoryItem;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, HISTORY_BLOCKS};
use crate::ui::DrawUi;
use crate::ui::styles::{ManagarrStyle, secondary_style};
use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size};
use ratatui::Frame;
use ratatui::layout::{Alignment, Constraint, Rect};
use ratatui::text::Text;
use ratatui::widgets::{Cell, Row};
use super::radarr_ui_utils::create_history_event_details;
#[cfg(test)]
#[path = "history_ui_tests.rs"]
mod history_ui_tests;
pub(super) struct HistoryUi;
impl DrawUi for HistoryUi {
fn accepts(route: Route) -> bool {
if let Route::Radarr(active_radarr_block, _) = route {
return HISTORY_BLOCKS.contains(&active_radarr_block);
}
false
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Radarr(active_radarr_block, _) = app.get_current_route() {
draw_history_table(f, app, area);
if active_radarr_block == ActiveRadarrBlock::HistoryItemDetails {
draw_history_item_details_popup(f, app);
}
}
}
}
fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let current_selection = if app.data.radarr_data.history.items.is_empty() {
RadarrHistoryItem::default()
} else {
app.data.radarr_data.history.current_selection().clone()
};
if let Route::Radarr(active_radarr_block, _) = app.get_current_route() {
let history_row_mapping = |history_item: &RadarrHistoryItem| {
let RadarrHistoryItem {
source_title,
languages,
quality,
event_type,
date,
..
} = history_item;
source_title.scroll_left_or_reset(
get_width_from_percentage(area, 40),
current_selection == *history_item,
app.ui_scroll_tick_count == 0,
);
Row::new(vec![
Cell::from(source_title.to_string()),
Cell::from(event_type.to_string()),
Cell::from(
languages
.iter()
.map(|language| language.name.to_owned())
.collect::<Vec<String>>()
.join(","),
),
Cell::from(quality.quality.name.to_owned()),
Cell::from(date.to_string()),
])
.primary()
};
let history_table =
ManagarrTable::new(Some(&mut app.data.radarr_data.history), history_row_mapping)
.block(layout_block_top_border())
.loading(app.is_loading)
.sorting(active_radarr_block == ActiveRadarrBlock::HistorySortPrompt)
.searching(active_radarr_block == ActiveRadarrBlock::SearchHistory)
.search_produced_empty_results(active_radarr_block == ActiveRadarrBlock::SearchHistoryError)
.filtering(active_radarr_block == ActiveRadarrBlock::FilterHistory)
.filter_produced_empty_results(active_radarr_block == ActiveRadarrBlock::FilterHistoryError)
.headers(["Source Title", "Event Type", "Language", "Quality", "Date"])
.constraints([
Constraint::Percentage(40),
Constraint::Percentage(15),
Constraint::Percentage(12),
Constraint::Percentage(13),
Constraint::Percentage(20),
]);
if [
ActiveRadarrBlock::SearchHistory,
ActiveRadarrBlock::FilterHistory,
]
.contains(&active_radarr_block)
{
history_table.show_cursor(f, area);
}
f.render_widget(history_table, area);
}
}
fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let current_selection = if app.data.radarr_data.history.items.is_empty() {
RadarrHistoryItem::default()
} else {
app.data.radarr_data.history.current_selection().clone()
};
let line_vec = create_history_event_details(current_selection);
let text = Text::from(line_vec);
let message = Message::new(text)
.title("Details")
.style(secondary_style())
.alignment(Alignment::Left);
f.render_widget(Popup::new(message).size(Size::NarrowLongMessage), f.area());
}
@@ -0,0 +1,28 @@
---
source: src/ui/radarr_ui/history/history_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Source Title ▼ Event Type Language Quality Date
=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC
╭───────────────── Filter ──────────────────╮
│Something │
╰─────────────────────────────────────────────╯
@@ -0,0 +1,31 @@
---
source: src/ui/radarr_ui/history/history_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Source Title ▼ Event Type Language Quality Date
=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC
╭─────────────── Error ───────────────╮
│The given filter produced empty results│
│ │
╰───────────────────────────────────────╯
@@ -0,0 +1,7 @@
---
source: src/ui/radarr_ui/history/history_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Source Title ▼ Event Type Language Quality Date
=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC
@@ -0,0 +1,39 @@
---
source: src/ui/radarr_ui/history/history_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Source Title ▼ Event Type Language Quality Date
=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC
╭─────────────────────────────────── Details ───────────────────────────────────╮
│Source Title: Test │
│Event Type: grabbed │
│Indexer: DrunkenSlug (Prowlarr) │
│Release Group: SPARKS │
│NZB Info URL: │
│Download Client: transmission │
│Age: 0 days │
│Published Date: 1970-01-01 00:00:00 UTC │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰─────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,42 @@
---
source: src/ui/radarr_ui/history/history_ui_tests.rs
expression: output
---
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Source Title Event Type Language Quality Date
=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC
╭───────────────────────────────╮
│Something │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────────────────────────╯

Some files were not shown because too many files have changed in this diff Show More