diff --git a/src/app/mod.rs b/src/app/mod.rs index 72f4759..a102471 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -55,7 +55,7 @@ impl App { } } - pub async fn dispatch(&mut self, action: NetworkEvent) { + pub async fn dispatch_network_event(&mut self, action: NetworkEvent) { if let Some(network_tx) = &self.network_tx { if let Err(e) = network_tx.send(action).await { self.is_loading = false; diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 6ceed16..3b75542 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -7,7 +7,8 @@ use strum::EnumIter; use crate::app::models::{ScrollableText, StatefulTable, TabRoute, TabState}; use crate::app::App; use crate::network::radarr_network::{ - Credit, DiskSpace, DownloadRecord, Movie, MovieHistoryItem, RadarrEvent, + Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord, Movie, MovieHistoryItem, + RadarrEvent, }; pub struct RadarrData { @@ -18,15 +19,27 @@ pub struct RadarrData { pub downloads: StatefulTable, pub quality_profile_map: HashMap, pub movie_details: ScrollableText, + pub file_details: String, + pub audio_details: String, + pub video_details: String, pub movie_history: StatefulTable, pub movie_cast: StatefulTable, pub movie_crew: StatefulTable, + pub collections: StatefulTable, + pub collection_movies: StatefulTable, pub main_tabs: TabState, pub movie_info_tabs: TabState, } impl RadarrData { + pub fn reset_movie_collection_table(&mut self) { + self.collection_movies = StatefulTable::default(); + } + pub fn reset_movie_info_tabs(&mut self) { + self.file_details = String::default(); + self.audio_details = String::default(); + self.video_details = String::default(); self.movie_details = ScrollableText::default(); self.movie_history = StatefulTable::default(); self.movie_cast = StatefulTable::default(); @@ -48,10 +61,15 @@ impl Default for RadarrData { movies: StatefulTable::default(), downloads: StatefulTable::default(), quality_profile_map: HashMap::default(), + file_details: String::default(), + audio_details: String::default(), + video_details: String::default(), movie_details: ScrollableText::default(), movie_history: StatefulTable::default(), movie_cast: StatefulTable::default(), movie_crew: StatefulTable::default(), + collections: StatefulTable::default(), + collection_movies: StatefulTable::default(), main_tabs: TabState::new(vec![ TabRoute { title: "Library".to_owned(), @@ -61,6 +79,10 @@ impl Default for RadarrData { title: "Downloads".to_owned(), route: ActiveRadarrBlock::Downloads.into(), }, + TabRoute { + title: "Collections".to_owned(), + route: ActiveRadarrBlock::Collections.into(), + }, ]), movie_info_tabs: TabState::new(vec![ TabRoute { @@ -71,6 +93,10 @@ impl Default for RadarrData { title: "History".to_owned(), route: ActiveRadarrBlock::MovieHistory.into(), }, + TabRoute { + title: "File".to_owned(), + route: ActiveRadarrBlock::FileInfo.into(), + }, TabRoute { title: "Cast".to_owned(), route: ActiveRadarrBlock::Cast.into(), @@ -89,9 +115,11 @@ pub enum ActiveRadarrBlock { AddMovie, Calendar, Collections, + CollectionDetails, Cast, Crew, Events, + FileInfo, Logs, Movies, MovieDetails, @@ -100,30 +128,57 @@ pub enum ActiveRadarrBlock { SearchMovie, SortOptions, Tasks, + ViewMovieOverview, } impl App { pub(super) async fn dispatch_by_radarr_block(&mut self, active_radarr_block: &ActiveRadarrBlock) { match active_radarr_block { - ActiveRadarrBlock::Downloads => self.dispatch(RadarrEvent::GetDownloads.into()).await, - ActiveRadarrBlock::Movies => { - self.dispatch(RadarrEvent::GetMovies.into()).await; - self.dispatch(RadarrEvent::GetDownloads.into()).await; - } - ActiveRadarrBlock::MovieDetails => { + ActiveRadarrBlock::Collections => { self.is_loading = true; - self.dispatch(RadarrEvent::GetMovieDetails.into()).await; + self + .dispatch_network_event(RadarrEvent::GetCollections.into()) + .await + } + ActiveRadarrBlock::CollectionDetails => { + self.is_loading = true; + self.populate_movie_collection_table().await; + self.is_loading = false; + } + ActiveRadarrBlock::Downloads => { + self.is_loading = true; + self + .dispatch_network_event(RadarrEvent::GetDownloads.into()) + .await + } + ActiveRadarrBlock::Movies => { + self + .dispatch_network_event(RadarrEvent::GetMovies.into()) + .await; + self + .dispatch_network_event(RadarrEvent::GetDownloads.into()) + .await; + } + ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::FileInfo => { + self.is_loading = true; + self + .dispatch_network_event(RadarrEvent::GetMovieDetails.into()) + .await; } ActiveRadarrBlock::MovieHistory => { self.is_loading = true; - self.dispatch(RadarrEvent::GetMovieHistory.into()).await; + self + .dispatch_network_event(RadarrEvent::GetMovieHistory.into()) + .await; } ActiveRadarrBlock::Cast | ActiveRadarrBlock::Crew => { if self.data.radarr_data.movie_cast.items.is_empty() || self.data.radarr_data.movie_crew.items.is_empty() { self.is_loading = true; - self.dispatch(RadarrEvent::GetMovieCredits.into()).await; + self + .dispatch_network_event(RadarrEvent::GetMovieCredits.into()) + .await; } } _ => (), @@ -138,9 +193,15 @@ impl App { is_first_render: bool, ) { if is_first_render { - self.dispatch(RadarrEvent::GetQualityProfiles.into()).await; - self.dispatch(RadarrEvent::GetOverview.into()).await; - self.dispatch(RadarrEvent::GetStatus.into()).await; + self + .dispatch_network_event(RadarrEvent::GetQualityProfiles.into()) + .await; + self + .dispatch_network_event(RadarrEvent::GetOverview.into()) + .await; + self + .dispatch_network_event(RadarrEvent::GetStatus.into()) + .await; self.dispatch_by_radarr_block(&active_radarr_block).await; } @@ -154,4 +215,16 @@ impl App { self.dispatch_by_radarr_block(&active_radarr_block).await; } } + + async fn populate_movie_collection_table(&mut self) { + self.data.radarr_data.collection_movies.set_items( + self + .data + .radarr_data + .collections + .current_selection_clone() + .movies + .unwrap_or_default(), + ); + } } diff --git a/src/handlers/radarr_handler.rs b/src/handlers/radarr_handler.rs index 0d39aaf..f6433ff 100644 --- a/src/handlers/radarr_handler.rs +++ b/src/handlers/radarr_handler.rs @@ -1,5 +1,5 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; -use crate::app::models::{Scrollable, ScrollableText, StatefulTable}; +use crate::app::models::Scrollable; use crate::app::radarr::ActiveRadarrBlock; use crate::handlers::handle_clear_errors; use crate::{App, Key}; @@ -23,21 +23,26 @@ pub async fn handle_radarr_key_events( async fn handle_tab_action(key: Key, app: &mut App, active_radarr_block: ActiveRadarrBlock) { match active_radarr_block { - ActiveRadarrBlock::Movies | ActiveRadarrBlock::Downloads => match key { - _ if key == DEFAULT_KEYBINDINGS.left.key => { - app.data.radarr_data.main_tabs.previous(); - app - .pop_and_push_navigation_stack(app.data.radarr_data.main_tabs.get_active_route().clone()); + ActiveRadarrBlock::Movies | ActiveRadarrBlock::Downloads | ActiveRadarrBlock::Collections => { + match key { + _ if key == DEFAULT_KEYBINDINGS.left.key => { + app.data.radarr_data.main_tabs.previous(); + app.pop_and_push_navigation_stack( + app.data.radarr_data.main_tabs.get_active_route().clone(), + ); + } + _ if key == DEFAULT_KEYBINDINGS.right.key => { + app.data.radarr_data.main_tabs.next(); + app.pop_and_push_navigation_stack( + app.data.radarr_data.main_tabs.get_active_route().clone(), + ); + } + _ => (), } - _ if key == DEFAULT_KEYBINDINGS.right.key => { - app.data.radarr_data.main_tabs.next(); - app - .pop_and_push_navigation_stack(app.data.radarr_data.main_tabs.get_active_route().clone()); - } - _ => (), - }, + } ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::MovieHistory + | ActiveRadarrBlock::FileInfo | ActiveRadarrBlock::Cast | ActiveRadarrBlock::Crew => match key { _ if key == DEFAULT_KEYBINDINGS.left.key => { @@ -70,6 +75,8 @@ async fn handle_tab_action(key: Key, app: &mut App, active_radarr_block: ActiveR async fn handle_scroll_up(app: &mut App, active_radarr_block: ActiveRadarrBlock) { match active_radarr_block { + ActiveRadarrBlock::Collections => app.data.radarr_data.collections.scroll_up(), + ActiveRadarrBlock::CollectionDetails => app.data.radarr_data.collection_movies.scroll_up(), ActiveRadarrBlock::Movies => app.data.radarr_data.movies.scroll_up(), ActiveRadarrBlock::MovieDetails => app.data.radarr_data.movie_details.scroll_up(), ActiveRadarrBlock::MovieHistory => app.data.radarr_data.movie_history.scroll_up(), @@ -82,6 +89,8 @@ async fn handle_scroll_up(app: &mut App, active_radarr_block: ActiveRadarrBlock) async fn handle_scroll_down(app: &mut App, active_radarr_block: ActiveRadarrBlock) { match active_radarr_block { + ActiveRadarrBlock::Collections => app.data.radarr_data.collections.scroll_down(), + ActiveRadarrBlock::CollectionDetails => app.data.radarr_data.collection_movies.scroll_down(), ActiveRadarrBlock::Movies => app.data.radarr_data.movies.scroll_down(), ActiveRadarrBlock::MovieDetails => app.data.radarr_data.movie_details.scroll_down(), ActiveRadarrBlock::MovieHistory => app.data.radarr_data.movie_history.scroll_down(), @@ -95,6 +104,12 @@ async fn handle_scroll_down(app: &mut App, active_radarr_block: ActiveRadarrBloc async fn handle_submit(app: &mut App, active_radarr_block: ActiveRadarrBlock) { match active_radarr_block { ActiveRadarrBlock::Movies => app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into()), + ActiveRadarrBlock::Collections => { + app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into()) + } + ActiveRadarrBlock::CollectionDetails => { + app.push_navigation_stack(ActiveRadarrBlock::ViewMovieOverview.into()) + } _ => (), } } @@ -103,11 +118,17 @@ async fn handle_esc(app: &mut App, active_radarr_block: ActiveRadarrBlock) { match active_radarr_block { ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::MovieHistory + | ActiveRadarrBlock::FileInfo | ActiveRadarrBlock::Cast | ActiveRadarrBlock::Crew => { app.pop_navigation_stack(); app.data.radarr_data.reset_movie_info_tabs(); } + ActiveRadarrBlock::CollectionDetails => { + app.pop_navigation_stack(); + app.data.radarr_data.reset_movie_collection_table(); + } + ActiveRadarrBlock::ViewMovieOverview => app.pop_navigation_stack(), _ => handle_clear_errors(app).await, } } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 783052e..22b9682 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -17,6 +17,7 @@ use crate::utils::{convert_runtime, convert_to_gb}; #[derive(Debug, Eq, PartialEq)] pub enum RadarrEvent { + GetCollections, GetDownloads, GetMovies, GetMovieCredits, @@ -31,6 +32,7 @@ pub enum RadarrEvent { impl RadarrEvent { const fn resource(self) -> &'static str { match self { + RadarrEvent::GetCollections => "/collection", RadarrEvent::GetDownloads => "/queue", RadarrEvent::GetMovies | RadarrEvent::GetMovieDetails => "/movie", RadarrEvent::GetMovieCredits => "/credit", @@ -88,6 +90,69 @@ pub struct Movie { pub quality_profile_id: Number, pub certification: Option, pub ratings: RatingsList, + pub movie_file: Option, + pub collection: Option, +} + +#[derive(Derivative, Deserialize, Debug, Clone)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct CollectionMovie { + pub title: String, + pub overview: String, + #[derivative(Default(value = "Number::from(0)"))] + pub year: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub runtime: Number, + pub genres: Vec, + pub ratings: RatingsList, +} + +#[derive(Deserialize, Derivative, Clone, Debug)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct Collection { + pub title: String, + pub root_folder_path: Option, + pub search_on_add: bool, + pub overview: Option, + #[derivative(Default(value = "Number::from(0)"))] + pub quality_profile_id: Number, + pub movies: Option>, +} + +#[derive(Deserialize, Derivative, Debug, Clone)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct MovieFile { + pub relative_path: String, + pub path: String, + pub date_added: DateTime, + pub media_info: MediaInfo, +} + +#[derive(Deserialize, Derivative, Debug, Clone)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct MediaInfo { + #[derivative(Default(value = "Number::from(0)"))] + pub audio_bitrate: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub audio_channels: Number, + pub audio_codec: Option, + pub audio_languages: Option, + #[derivative(Default(value = "Number::from(0)"))] + pub audio_stream_count: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub video_bit_depth: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub video_bitrate: Number, + pub video_codec: String, + #[derivative(Default(value = "Number::from(0)"))] + pub video_fps: Number, + pub resolution: String, + pub run_time: String, + pub scan_type: String, } #[derive(Default, Deserialize, Debug, Clone)] @@ -184,6 +249,11 @@ pub struct Credit { impl<'a> Network<'a> { pub async fn handle_radarr_event(&self, radarr_event: RadarrEvent) { match radarr_event { + RadarrEvent::GetCollections => { + self + .get_collections(RadarrEvent::GetCollections.resource()) + .await + } RadarrEvent::HealthCheck => { self .get_healthcheck(RadarrEvent::HealthCheck.resource()) @@ -275,6 +345,8 @@ impl<'a> Network<'a> { genres, runtime, ratings, + movie_file, + collection, .. } = movie_response; let (hours, minutes) = convert_runtime(runtime.as_u64().unwrap()); @@ -317,11 +389,13 @@ impl<'a> Network<'a> { }; let status = get_movie_status(has_file, &app.data.radarr_data.downloads.items, id); + let collection = collection.unwrap_or_default(); app.data.radarr_data.movie_details = ScrollableText::with_string(formatdoc!( "Title: {} Year: {} Runtime: {}h {}m + Collection: {} Status: {} Description: {} TMDB: {} @@ -336,6 +410,7 @@ impl<'a> Network<'a> { year, hours, minutes, + collection.title, status, overview, tmdb_rating, @@ -346,7 +421,52 @@ impl<'a> Network<'a> { path, studio, genres.join(", ") - )) + )); + + if let Some(file) = movie_file { + app.data.radarr_data.file_details = formatdoc!( + "Relative Path: {} + Absolute Path: {} + Size: {:.2} GB + Date Added: {}", + file.relative_path, + file.path, + size, + file.date_added + ); + + let media_info = file.media_info; + + app.data.radarr_data.audio_details = formatdoc!( + "Bitrate: {} + Channels: {:.1} + Codec: {} + Languages: {} + Stream Count: {}", + media_info.audio_bitrate.as_u64().unwrap(), + media_info.audio_channels.as_f64().unwrap(), + media_info.audio_codec.unwrap_or_default(), + media_info.audio_languages.unwrap_or_default(), + media_info.audio_stream_count.as_u64().unwrap() + ); + + app.data.radarr_data.video_details = formatdoc!( + "Bit Depth: {} + Bitrate: {} + Codec: {} + FPS: {} + Resolution: {} + Scan Type: {} + Runtime: {}", + media_info.video_bit_depth.as_u64().unwrap(), + media_info.video_bitrate.as_u64().unwrap(), + media_info.video_codec, + media_info.video_fps.as_f64().unwrap(), + media_info.resolution, + media_info.scan_type, + media_info.run_time + ); + } }, ) .await; @@ -369,6 +489,14 @@ impl<'a> Network<'a> { .await; } + async fn get_collections(&self, resource: &str) { + self + .handle_get_request::>(resource, |collections_vec, mut app| { + app.data.radarr_data.collections.set_items(collections_vec); + }) + .await; + } + async fn get_downloads(&self, resource: &str) { self .handle_get_request::(resource, |queue_response, mut app| { diff --git a/src/ui/radarr_ui.rs b/src/ui/radarr_ui.rs index 9110395..bbb79aa 100644 --- a/src/ui/radarr_ui.rs +++ b/src/ui/radarr_ui.rs @@ -5,7 +5,7 @@ use chrono::{Duration, Utc}; use tui::backend::Backend; use tui::layout::{Alignment, Constraint, Rect}; use tui::style::{Color, Style}; -use tui::text::Text; +use tui::text::{Spans, Text}; use tui::widgets::{Block, Cell, Paragraph, Row, Wrap}; use tui::Frame; @@ -14,26 +14,41 @@ use crate::app::{App, Route}; use crate::logos::RADARR_LOGO; use crate::network::radarr_network::{Credit, DiskSpace, DownloadRecord, Movie, MovieHistoryItem}; use crate::ui::utils::{ - horizontal_chunks, layout_block_top_border, line_gauge_with_label, line_gauge_with_title, - style_bold, style_failure, style_success, style_warning, title_block, - vertical_chunks_with_margin, + borderless_block, horizontal_chunks, layout_block_bottom_border, layout_block_top_border, + layout_block_top_border_with_title, layout_block_with_title, line_gauge_with_label, + line_gauge_with_title, spans_info_default, spans_info_primary, spans_info_with_style, style_bold, + style_default, style_default_bold, style_failure, style_primary, style_success, style_warning, + title_block, title_style, vertical_chunks, vertical_chunks_with_margin, +}; +use crate::ui::{ + draw_large_popup_over, draw_small_popup_over, draw_table, draw_tabs, loading, TableProps, }; -use crate::ui::{draw_large_popup_over, draw_table, draw_tabs, loading, TableProps}; use crate::utils::{convert_runtime, convert_to_gb}; pub(super) fn draw_radarr_ui(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let (content_rect, _) = draw_tabs(f, area, " Movies ", &app.data.radarr_data.main_tabs); - if let Route::Radarr(active_radarr_block) = app.get_current_route() { + if let Route::Radarr(active_radarr_block) = app.get_current_route().clone() { match active_radarr_block { ActiveRadarrBlock::Movies => draw_library(f, app, content_rect), ActiveRadarrBlock::Downloads => draw_downloads(f, app, content_rect), + ActiveRadarrBlock::Collections => draw_collections(f, app, content_rect), ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::MovieHistory + | ActiveRadarrBlock::FileInfo | ActiveRadarrBlock::Cast | ActiveRadarrBlock::Crew => { draw_large_popup_over(f, app, content_rect, draw_library, draw_movie_info) } + ActiveRadarrBlock::CollectionDetails | ActiveRadarrBlock::ViewMovieOverview => { + draw_large_popup_over( + f, + app, + content_rect, + draw_collections, + draw_collection_details_popup, + ) + } _ => (), } } @@ -189,12 +204,183 @@ fn draw_downloads(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { Cell::from(indexer.to_owned()), Cell::from(download_client.to_owned()), ]) - .style(style_success()) + .style(style_primary()) }, app.is_loading, ); } +fn draw_collections(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let quality_profile_map = &app.data.radarr_data.quality_profile_map; + draw_table( + f, + area, + layout_block_top_border(), + TableProps { + content: &mut app.data.radarr_data.collections, + table_headers: vec![ + "Collection", + "Search on Add?", + "Number of Movies", + "Root Folder Path", + "Quality Profile", + ], + constraints: iter::repeat(Constraint::Ratio(1, 5)).take(5).collect(), + }, + |collection| { + let number_of_movies = collection.movies.clone().unwrap_or_default().len(); + + Row::new(vec![ + Cell::from(collection.title.to_owned()), + Cell::from(collection.search_on_add.to_string()), + Cell::from(number_of_movies.to_string()), + Cell::from(collection.root_folder_path.clone().unwrap_or_default()), + Cell::from( + quality_profile_map + .get(&collection.quality_profile_id.as_u64().unwrap()) + .unwrap() + .to_owned(), + ), + ]) + .style(style_primary()) + }, + app.is_loading, + ); +} + +fn draw_collection_details(f: &mut Frame<'_, B>, app: &mut App, content_area: Rect) { + let chunks = vertical_chunks_with_margin( + vec![Constraint::Length(10), Constraint::Min(0)], + content_area, + 1, + ); + let collection_selection = app.data.radarr_data.collections.current_selection(); + let quality_profile = app + .data + .radarr_data + .quality_profile_map + .get(&collection_selection.quality_profile_id.as_u64().unwrap()) + .unwrap() + .to_owned(); + + let collection_description = Text::from(vec![ + spans_info_primary( + "Overview: ".to_owned(), + collection_selection.overview.clone().unwrap_or_default(), + ), + spans_info_primary( + "Root Folder Path: ".to_owned(), + collection_selection + .root_folder_path + .clone() + .unwrap_or_default(), + ), + spans_info_primary( + "Search on Add: ".to_owned(), + collection_selection.search_on_add.to_string(), + ), + spans_info_primary("Quality Profile: ".to_owned(), quality_profile), + ]); + + let description_paragraph = Paragraph::new(collection_description) + .block(borderless_block()) + .wrap(Wrap { trim: false }); + + f.render_widget( + layout_block_with_title(title_style(&collection_selection.title)), + content_area, + ); + + f.render_widget(description_paragraph, chunks[0]); + + draw_table( + f, + chunks[1], + layout_block_top_border_with_title(title_style("Movies")), + TableProps { + content: &mut app.data.radarr_data.collection_movies, + table_headers: vec![ + "Title", + "Year", + "Runtime", + "IMDB Rating", + "Rotten Tomatoes Rating", + "Genres", + ], + constraints: vec![ + Constraint::Percentage(20), + Constraint::Percentage(8), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(18), + Constraint::Percentage(30), + ], + }, + |movie| { + let (hours, minutes) = convert_runtime(movie.runtime.as_u64().unwrap()); + let imdb_rating = movie + .ratings + .imdb + .clone() + .unwrap_or_default() + .value + .as_f64() + .unwrap(); + let rotten_tomatoes_rating = movie + .ratings + .rotten_tomatoes + .clone() + .unwrap_or_default() + .value + .as_u64() + .unwrap(); + let imdb_rating = if imdb_rating == 0.0 { + String::default() + } else { + format!("{:.1}", imdb_rating) + }; + let rotten_tomatoes_rating = if rotten_tomatoes_rating == 0 { + String::default() + } else { + format!("{}%", rotten_tomatoes_rating) + }; + + Row::new(vec![ + Cell::from(movie.title.to_owned()), + Cell::from(movie.year.as_u64().unwrap().to_string()), + Cell::from(format!("{}h {}m", hours, minutes)), + Cell::from(imdb_rating), + Cell::from(rotten_tomatoes_rating), + Cell::from(movie.genres.join(", ")), + ]) + .style(style_primary()) + }, + app.is_loading, + ); +} + +fn draw_collection_details_popup( + f: &mut Frame<'_, B>, + app: &mut App, + content_area: Rect, +) { + if let Route::Radarr(active_radarr_block) = app.get_current_route() { + match active_radarr_block { + ActiveRadarrBlock::ViewMovieOverview => { + draw_small_popup_over( + f, + app, + content_area, + draw_collection_details, + draw_movie_overview, + ); + } + ActiveRadarrBlock::CollectionDetails => draw_collection_details(f, app, content_area), + _ => (), + } + } +} + fn draw_movie_info(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let (content_area, block) = draw_tabs(f, area, "Movie Info", &app.data.radarr_data.movie_info_tabs); @@ -203,6 +389,7 @@ fn draw_movie_info(f: &mut Frame<'_, B>, app: &mut App, area: Rect) app.data.radarr_data.movie_info_tabs.get_active_route() { match active_radarr_block { + ActiveRadarrBlock::FileInfo => draw_file_info(f, app, content_area, block), ActiveRadarrBlock::MovieDetails => draw_movie_details(f, app, content_area, block), ActiveRadarrBlock::MovieHistory => draw_movie_history(f, app, content_area, block), ActiveRadarrBlock::Cast => draw_movie_cast(f, app, content_area, block), @@ -212,6 +399,24 @@ fn draw_movie_info(f: &mut Frame<'_, B>, app: &mut App, area: Rect) } } +fn draw_movie_overview(f: &mut Frame<'_, B>, app: &mut App, content_area: Rect) { + let mut overview = Text::from( + app + .data + .radarr_data + .collection_movies + .current_selection_clone() + .overview, + ); + overview.patch_style(style_default()); + + let paragraph = Paragraph::new(overview) + .block(layout_block_with_title(title_style("Overview"))) + .wrap(Wrap { trim: false }); + + f.render_widget(paragraph, content_area); +} + fn draw_movie_details( f: &mut Frame<'_, B>, app: &App, @@ -231,7 +436,21 @@ fn draw_movie_details( .unwrap() .split(": ") .collect::>()[1]; - let mut text = Text::from(movie_details); + let mut text = Text::from( + app + .data + .radarr_data + .movie_details + .items + .iter() + .map(|line| { + let split = line.split(':').collect::>(); + let title = format!("{}:", split[0]); + + spans_info_default(title, split[1].to_owned()) + }) + .collect::>(), + ); text.patch_style(determine_style_from_download_status(download_status)); let paragraph = Paragraph::new(text) @@ -245,6 +464,61 @@ fn draw_movie_details( } } +fn draw_file_info(f: &mut Frame<'_, B>, app: &App, content_area: Rect, block: Block) { + let file_info = app.data.radarr_data.file_details.to_owned(); + + if !file_info.is_empty() { + let audio_details = app.data.radarr_data.audio_details.to_owned(); + let video_details = app.data.radarr_data.video_details.to_owned(); + let chunks = vertical_chunks( + vec![ + Constraint::Length(1), + Constraint::Length(5), + Constraint::Length(1), + Constraint::Length(6), + Constraint::Length(1), + Constraint::Length(7), + ], + content_area, + ); + let mut file_details_title = Text::from("File Details"); + let mut audio_details_title = Text::from("Audio Details"); + let mut video_details_title = Text::from("Video Details"); + file_details_title.patch_style(style_bold()); + audio_details_title.patch_style(style_bold()); + video_details_title.patch_style(style_bold()); + + let file_details_title_paragraph = Paragraph::new(file_details_title).block(borderless_block()); + let audio_details_title_paragraph = + Paragraph::new(audio_details_title).block(borderless_block()); + let video_details_title_paragraph = + Paragraph::new(video_details_title).block(borderless_block()); + + let file_details = Text::from(file_info); + let audio_details = Text::from(audio_details); + let video_details = Text::from(video_details); + + let file_details_paragraph = Paragraph::new(file_details) + .block(layout_block_bottom_border()) + .wrap(Wrap { trim: false }); + let audio_details_paragraph = Paragraph::new(audio_details) + .block(layout_block_bottom_border()) + .wrap(Wrap { trim: false }); + let video_details_paragraph = Paragraph::new(video_details) + .block(borderless_block()) + .wrap(Wrap { trim: false }); + + f.render_widget(file_details_title_paragraph, chunks[0]); + f.render_widget(file_details_paragraph, chunks[1]); + f.render_widget(audio_details_title_paragraph, chunks[2]); + f.render_widget(audio_details_paragraph, chunks[3]); + f.render_widget(video_details_title_paragraph, chunks[4]); + f.render_widget(video_details_paragraph, chunks[5]); + } else { + loading(f, block, content_area, app.is_loading); + } +} + fn draw_movie_history( f: &mut Frame<'_, B>, app: &mut App, diff --git a/src/ui/utils.rs b/src/ui/utils.rs index 47815e0..e5c93a4 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -66,6 +66,39 @@ pub fn layout_block_top_border<'a>() -> Block<'a> { Block::default().borders(Borders::TOP) } +pub fn layout_block_bottom_border<'a>() -> Block<'a> { + Block::default().borders(Borders::BOTTOM) +} + +pub fn borderless_block<'a>() -> Block<'a> { + Block::default() +} + +pub fn spans_info_with_style<'a>( + title: String, + content: String, + title_style: Style, + content_style: Style, +) -> Spans<'a> { + Spans::from(vec![ + Span::styled(title, title_style), + Span::styled(content, content_style), + ]) +} + +pub fn spans_info_default<'a>(title: String, content: String) -> Spans<'a> { + spans_info_with_style(title, content, style_bold(), style_default()) +} + +pub fn spans_info_primary<'a>(title: String, content: String) -> Spans<'a> { + spans_info_with_style( + title, + content, + style_primary().add_modifier(Modifier::BOLD), + style_default(), + ) +} + pub fn style_bold() -> Style { Style::default().add_modifier(Modifier::BOLD) }