diff --git a/src/app/mod.rs b/src/app/mod.rs index 207a123..72f4759 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -7,9 +7,8 @@ use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::Sender; use tokio::time::Instant; -use crate::app::models::{TabRoute, TabState}; +use crate::app::models::{HorizontallyScrollableText, TabRoute, TabState}; use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; -use crate::network::radarr_network::RadarrEvent; use crate::network::NetworkEvent; pub(crate) mod key_binding; @@ -34,7 +33,7 @@ pub struct App { navigation_stack: Vec, network_tx: Option>, pub server_tabs: TabState, - pub error: String, + pub error: HorizontallyScrollableText, pub client: Client, pub title: &'static str, pub tick_until_poll: u64, @@ -72,47 +71,23 @@ impl App { pub fn reset(&mut self) { self.reset_tick_count(); - self.error = String::default(); + self.error = HorizontallyScrollableText::default(); self.data = Data::default(); } pub fn handle_error(&mut self, error: anyhow::Error) { - if self.error.is_empty() { - self.error = format!("{} ", error); + if self.error.text.is_empty() { + self.error = HorizontallyScrollableText::new(error.to_string()); } } - fn scroll_error_horizontally(&mut self) { - let first_char = self.error.chars().next().unwrap(); - self.error = format!("{}{}", &self.error[1..], first_char); - } - pub async fn on_tick(&mut self, is_first_render: bool) { - if !self.error.is_empty() { - self.scroll_error_horizontally(); - } - if self.tick_count % self.tick_until_poll == 0 || self.is_routing { match self.get_current_route() { Route::Radarr(active_radarr_block) => { - let active_block = active_radarr_block.clone(); - - if is_first_render { - self.dispatch(RadarrEvent::GetQualityProfiles.into()).await; - self.dispatch(RadarrEvent::GetOverview.into()).await; - self.dispatch(RadarrEvent::GetStatus.into()).await; - self.dispatch_by_radarr_block(active_block.clone()).await; - } - - if self.is_routing - || self - .network_tick_frequency - .checked_sub(self.last_tick.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)) - .is_zero() - { - self.dispatch_by_radarr_block(active_block).await; - } + self + .radarr_on_tick(active_radarr_block.clone(), is_first_render) + .await; } _ => (), } @@ -150,7 +125,7 @@ impl Default for App { App { navigation_stack: vec![DEFAULT_ROUTE], network_tx: None, - error: String::default(), + error: HorizontallyScrollableText::default(), server_tabs: TabState::new(vec![ TabRoute { title: "Radarr".to_owned(), @@ -163,9 +138,9 @@ impl Default for App { ]), client: Client::new(), title: "Managarr", - tick_until_poll: 20, + tick_until_poll: 50, tick_count: 0, - network_tick_frequency: Duration::from_secs(10), + network_tick_frequency: Duration::from_secs(20), last_tick: Instant::now(), is_loading: false, is_routing: false, diff --git a/src/app/models.rs b/src/app/models.rs index 5684de6..f715134 100644 --- a/src/app/models.rs +++ b/src/app/models.rs @@ -1,3 +1,7 @@ +use std::cell::RefCell; +use std::fmt::{Display, Formatter}; + +use serde::Deserialize; use tui::widgets::TableState; use crate::app::Route; @@ -21,7 +25,7 @@ impl Default for StatefulTable { } } -impl StatefulTable { +impl StatefulTable { pub fn set_items(&mut self, items: Vec) { let items_len = items.len(); self.items = items; @@ -42,6 +46,10 @@ impl StatefulTable { pub fn current_selection(&self) -> &T { &self.items[self.state.selected().unwrap_or(0)] } + + pub fn current_selection_clone(&self) -> T { + self.items[self.state.selected().unwrap_or(0)].clone() + } } impl Scrollable for StatefulTable { @@ -108,6 +116,47 @@ impl Scrollable for ScrollableText { } } +#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(from = "String")] +pub struct HorizontallyScrollableText { + pub text: String, + pub offset: RefCell, +} + +impl From for HorizontallyScrollableText { + fn from(input: String) -> HorizontallyScrollableText { + HorizontallyScrollableText::new(input) + } +} + +impl Display for HorizontallyScrollableText { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if *self.offset.borrow() == 0 { + write!(f, "{}", self.text) + } else { + write!(f, "{}", &self.text[*self.offset.borrow()..]) + } + } +} + +impl HorizontallyScrollableText { + pub fn new(input: String) -> HorizontallyScrollableText { + HorizontallyScrollableText { + text: format!("{} ", input), + offset: RefCell::new(0), + } + } + + pub fn scroll_text(&self) { + let new_offset = *self.offset.borrow() + 1; + *self.offset.borrow_mut() = new_offset % self.text.len(); + } + + pub fn reset_offset(&self) { + *self.offset.borrow_mut() = 0; + } +} + #[derive(Clone)] pub struct TabRoute { pub title: String, diff --git a/src/app/radarr.rs b/src/app/radarr.rs index b251a42..6ceed16 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::time::Duration; use chrono::{DateTime, Utc}; use strum::EnumIter; @@ -6,7 +7,7 @@ use strum::EnumIter; use crate::app::models::{ScrollableText, StatefulTable, TabRoute, TabState}; use crate::app::App; use crate::network::radarr_network::{ - DiskSpace, DownloadRecord, Movie, MovieHistoryItem, RadarrEvent, + Credit, DiskSpace, DownloadRecord, Movie, MovieHistoryItem, RadarrEvent, }; pub struct RadarrData { @@ -18,14 +19,18 @@ pub struct RadarrData { pub quality_profile_map: HashMap, pub movie_details: ScrollableText, pub movie_history: StatefulTable, + pub movie_cast: StatefulTable, + pub movie_crew: StatefulTable, pub main_tabs: TabState, pub movie_info_tabs: TabState, } impl RadarrData { - pub fn reset_movie_info_tab(&mut self) { + pub fn reset_movie_info_tabs(&mut self) { self.movie_details = ScrollableText::default(); self.movie_history = StatefulTable::default(); + self.movie_cast = StatefulTable::default(); + self.movie_crew = StatefulTable::default(); self.movie_info_tabs.index = 0; } @@ -45,6 +50,8 @@ impl Default for RadarrData { quality_profile_map: HashMap::default(), movie_details: ScrollableText::default(), movie_history: StatefulTable::default(), + movie_cast: StatefulTable::default(), + movie_crew: StatefulTable::default(), main_tabs: TabState::new(vec![ TabRoute { title: "Library".to_owned(), @@ -64,6 +71,14 @@ impl Default for RadarrData { title: "History".to_owned(), route: ActiveRadarrBlock::MovieHistory.into(), }, + TabRoute { + title: "Cast".to_owned(), + route: ActiveRadarrBlock::Cast.into(), + }, + TabRoute { + title: "Crew".to_owned(), + route: ActiveRadarrBlock::Crew.into(), + }, ]), } } @@ -74,6 +89,8 @@ pub enum ActiveRadarrBlock { AddMovie, Calendar, Collections, + Cast, + Crew, Events, Logs, Movies, @@ -86,7 +103,7 @@ pub enum ActiveRadarrBlock { } impl App { - pub(super) async fn dispatch_by_radarr_block(&mut self, active_radarr_block: ActiveRadarrBlock) { + 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 => { @@ -95,15 +112,46 @@ impl App { } ActiveRadarrBlock::MovieDetails => { self.is_loading = true; - self.dispatch(RadarrEvent::GetMovieDetails.into()).await + self.dispatch(RadarrEvent::GetMovieDetails.into()).await; } ActiveRadarrBlock::MovieHistory => { self.is_loading = true; - self.dispatch(RadarrEvent::GetMovieHistory.into()).await + self.dispatch(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.reset_tick_count(); } + + pub(super) async fn radarr_on_tick( + &mut self, + active_radarr_block: ActiveRadarrBlock, + 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_by_radarr_block(&active_radarr_block).await; + } + + if self.is_routing + || self + .network_tick_frequency + .checked_sub(self.last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)) + .is_zero() + { + self.dispatch_by_radarr_block(&active_radarr_block).await; + } + } } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 95cdc7b..27343fe 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,3 +1,4 @@ +use crate::app::models::HorizontallyScrollableText; use crate::app::{App, Route}; use crate::event::Key; use crate::handlers::radarr_handler::handle_radarr_key_events; @@ -14,7 +15,7 @@ pub async fn handle_key_events(key: Key, app: &mut App) { } pub async fn handle_clear_errors(app: &mut App) { - if !app.error.is_empty() { - app.error = String::default(); + if !app.error.text.is_empty() { + app.error = HorizontallyScrollableText::default(); } } diff --git a/src/handlers/radarr_handler.rs b/src/handlers/radarr_handler.rs index 444607c..0d39aaf 100644 --- a/src/handlers/radarr_handler.rs +++ b/src/handlers/radarr_handler.rs @@ -36,7 +36,10 @@ async fn handle_tab_action(key: Key, app: &mut App, active_radarr_block: ActiveR } _ => (), }, - ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::MovieHistory => match key { + ActiveRadarrBlock::MovieDetails + | ActiveRadarrBlock::MovieHistory + | ActiveRadarrBlock::Cast + | ActiveRadarrBlock::Crew => match key { _ if key == DEFAULT_KEYBINDINGS.left.key => { app.data.radarr_data.movie_info_tabs.previous(); app.pop_and_push_navigation_stack( @@ -70,6 +73,8 @@ async fn handle_scroll_up(app: &mut App, active_radarr_block: ActiveRadarrBlock) 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(), + ActiveRadarrBlock::Cast => app.data.radarr_data.movie_cast.scroll_up(), + ActiveRadarrBlock::Crew => app.data.radarr_data.movie_crew.scroll_up(), ActiveRadarrBlock::Downloads => app.data.radarr_data.downloads.scroll_up(), _ => (), } @@ -80,6 +85,8 @@ async fn handle_scroll_down(app: &mut App, active_radarr_block: ActiveRadarrBloc 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(), + ActiveRadarrBlock::Cast => app.data.radarr_data.movie_cast.scroll_down(), + ActiveRadarrBlock::Crew => app.data.radarr_data.movie_crew.scroll_down(), ActiveRadarrBlock::Downloads => app.data.radarr_data.downloads.scroll_down(), _ => (), } @@ -94,9 +101,12 @@ async fn handle_submit(app: &mut App, active_radarr_block: ActiveRadarrBlock) { async fn handle_esc(app: &mut App, active_radarr_block: ActiveRadarrBlock) { match active_radarr_block { - ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::MovieHistory => { + ActiveRadarrBlock::MovieDetails + | ActiveRadarrBlock::MovieHistory + | ActiveRadarrBlock::Cast + | ActiveRadarrBlock::Crew => { app.pop_navigation_stack(); - app.data.radarr_data.reset_movie_info_tab(); + app.data.radarr_data.reset_movie_info_tabs(); } _ => handle_clear_errors(app).await, } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 69fd272..783052e 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -9,7 +9,7 @@ use serde::Deserialize; use serde_json::Number; use tokio::sync::MutexGuard; -use crate::app::models::ScrollableText; +use crate::app::models::{HorizontallyScrollableText, ScrollableText}; use crate::app::{App, RadarrConfig}; use crate::network::utils::get_movie_status; use crate::network::{utils, Network, NetworkEvent}; @@ -19,6 +19,7 @@ use crate::utils::{convert_runtime, convert_to_gb}; pub enum RadarrEvent { GetDownloads, GetMovies, + GetMovieCredits, GetMovieDetails, GetMovieHistory, GetOverview, @@ -32,6 +33,7 @@ impl RadarrEvent { match self { RadarrEvent::GetDownloads => "/queue", RadarrEvent::GetMovies | RadarrEvent::GetMovieDetails => "/movie", + RadarrEvent::GetMovieCredits => "/credit", RadarrEvent::GetMovieHistory => "/history/movie", RadarrEvent::GetOverview => "/diskspace", RadarrEvent::GetQualityProfiles => "/qualityprofile", @@ -61,7 +63,7 @@ struct SystemStatus { start_time: DateTime, } -#[derive(Derivative, Deserialize, Debug)] +#[derive(Derivative, Deserialize, Debug, Clone)] #[derivative(Default)] #[serde(rename_all = "camelCase")] pub struct Movie { @@ -88,7 +90,7 @@ pub struct Movie { pub ratings: RatingsList, } -#[derive(Default, Deserialize, Debug)] +#[derive(Default, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct RatingsList { pub imdb: Option, @@ -96,7 +98,7 @@ pub struct RatingsList { pub rotten_tomatoes: Option, } -#[derive(Derivative, Deserialize, Debug)] +#[derive(Derivative, Deserialize, Debug, Clone)] #[derivative(Default)] pub struct Rating { #[derivative(Default(value = "Number::from(0)"))] @@ -110,7 +112,7 @@ pub struct DownloadsResponse { pub records: Vec, } -#[derive(Derivative, Deserialize, Debug)] +#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] #[derivative(Default)] #[serde(rename_all = "camelCase")] pub struct DownloadRecord { @@ -122,7 +124,7 @@ pub struct DownloadRecord { pub size: Number, #[derivative(Default(value = "Number::from(0)"))] pub sizeleft: Number, - pub output_path: String, + pub output_path: HorizontallyScrollableText, pub indexer: String, pub download_client: String, } @@ -136,31 +138,49 @@ struct QualityProfile { pub name: String, } -#[derive(Deserialize, Default, Debug, Clone)] +#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct MovieHistoryItem { - pub source_title: String, + pub source_title: HorizontallyScrollableText, pub quality: QualityHistory, pub languages: Vec, pub date: DateTime, pub event_type: String, } -#[derive(Deserialize, Default, Debug, Clone)] +#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)] pub struct Language { pub name: String, } -#[derive(Deserialize, Default, Debug, Clone)] +#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)] pub struct QualityHistory { pub quality: Quality, } -#[derive(Deserialize, Default, Debug, Clone)] +#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)] pub struct Quality { pub name: String, } +#[derive(Deserialize, PartialEq, Eq, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum CreditType { + Cast, + Crew, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Credit { + pub person_name: String, + pub character: Option, + pub department: Option, + pub job: Option, + #[serde(rename(deserialize = "type"))] + pub credit_type: CreditType, +} + impl<'a> Network<'a> { pub async fn handle_radarr_event(&self, radarr_event: RadarrEvent) { match radarr_event { @@ -176,6 +196,11 @@ impl<'a> Network<'a> { } RadarrEvent::GetStatus => self.get_status(RadarrEvent::GetStatus.resource()).await, RadarrEvent::GetMovies => self.get_movies(RadarrEvent::GetMovies.resource()).await, + RadarrEvent::GetMovieCredits => { + self + .get_credits(RadarrEvent::GetMovieCredits.resource()) + .await + } RadarrEvent::GetMovieDetails => { self .get_movie_details(RadarrEvent::GetMovieDetails.resource()) @@ -328,10 +353,9 @@ impl<'a> Network<'a> { } async fn get_movie_history(&self, resource: &str) { - let movie_id = self.extract_movie_id().await; self .handle_get_request::>( - format!("{}?movieId={}", resource.to_owned(), movie_id).as_str(), + self.append_movie_id_param(resource).await.as_str(), |movie_history_vec, mut app| { let mut reversed_movie_history_vec = movie_history_vec.to_vec(); reversed_movie_history_vec.reverse(); @@ -368,6 +392,29 @@ impl<'a> Network<'a> { .await; } + async fn get_credits(&self, resource: &str) { + self + .handle_get_request::>( + self.append_movie_id_param(resource).await.as_str(), + |credit_vec, mut app| { + let cast_vec: Vec = credit_vec + .iter() + .cloned() + .filter(|credit| credit.credit_type == CreditType::Cast) + .collect(); + let crew_vec: Vec = credit_vec + .iter() + .cloned() + .filter(|credit| credit.credit_type == CreditType::Crew) + .collect(); + + app.data.radarr_data.movie_cast.set_items(cast_vec); + app.data.radarr_data.movie_crew.set_items(crew_vec); + }, + ) + .await; + } + async fn call_radarr_api(&self, resource: &str) -> RequestBuilder { debug!("Creating RequestBuilder for resource: {:?}", resource); let app = self.app.lock().await; @@ -427,4 +474,9 @@ impl<'a> Network<'a> { .as_u64() .unwrap() } + + async fn append_movie_id_param(&self, resource: &str) -> String { + let movie_id = self.extract_movie_id().await; + format!("{}?movieId={}", resource.to_owned(), movie_id.to_owned()) + } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 14fd8e5..2411a72 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,3 +1,4 @@ +use log::debug; use tui::backend::Backend; use tui::layout::{Alignment, Constraint, Rect}; use tui::text::{Span, Spans, Text}; @@ -26,7 +27,7 @@ mod utils; static HIGHLIGHT_SYMBOL: &str = "=> "; pub fn ui(f: &mut Frame, app: &mut App) { - let main_chunks = if !app.error.is_empty() { + let main_chunks = if !app.error.text.is_empty() { let chunks = vertical_chunks_with_margin( vec![ Constraint::Length(3), @@ -92,7 +93,11 @@ fn draw_error(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { .style(style_failure()) .borders(Borders::ALL); - let mut text = Text::from(app.error.clone()); + if app.error.text.len() > area.width as usize { + app.error.scroll_text(); + } + + let mut text = Text::from(app.error.to_string()); text.patch_style(style_failure()); let paragraph = Paragraph::new(text) diff --git a/src/ui/radarr_ui.rs b/src/ui/radarr_ui.rs index 4381896..9110395 100644 --- a/src/ui/radarr_ui.rs +++ b/src/ui/radarr_ui.rs @@ -2,7 +2,6 @@ use std::iter; use std::ops::Sub; use chrono::{Duration, Utc}; -use log::debug; use tui::backend::Backend; use tui::layout::{Alignment, Constraint, Rect}; use tui::style::{Color, Style}; @@ -13,10 +12,10 @@ use tui::Frame; use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; use crate::app::{App, Route}; use crate::logos::RADARR_LOGO; -use crate::network::radarr_network::{DiskSpace, DownloadRecord, Movie, MovieHistoryItem}; +use crate::network::radarr_network::{Credit, DiskSpace, DownloadRecord, Movie, MovieHistoryItem}; use crate::ui::utils::{ - horizontal_chunks, horizontal_chunks_with_margin, layout_block_top_border, line_gague_with_label, - line_gague_with_title, style_bold, style_failure, style_success, style_warning, title_block, + 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, }; use crate::ui::{draw_large_popup_over, draw_table, draw_tabs, loading, TableProps}; @@ -29,7 +28,10 @@ pub(super) fn draw_radarr_ui(f: &mut Frame<'_, B>, app: &mut App, ar match active_radarr_block { ActiveRadarrBlock::Movies => draw_library(f, app, content_rect), ActiveRadarrBlock::Downloads => draw_downloads(f, app, content_rect), - ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::MovieHistory => { + ActiveRadarrBlock::MovieDetails + | ActiveRadarrBlock::MovieHistory + | ActiveRadarrBlock::Cast + | ActiveRadarrBlock::Crew => { draw_large_popup_over(f, app, content_rect, draw_library, draw_movie_info) } _ => (), @@ -119,7 +121,7 @@ fn draw_downloads_context(f: &mut Frame<'_, B>, app: &App, area: Rec .. } = &downloads_vec[i]; let percent = 1f64 - (sizeleft.as_f64().unwrap() / size.as_f64().unwrap()); - let download_gague = line_gague_with_title(title, percent); + let download_gague = line_gauge_with_title(title, percent); f.render_widget(download_gague, chunks[i]); } @@ -129,6 +131,13 @@ fn draw_downloads_context(f: &mut Frame<'_, B>, app: &App, area: Rec } fn draw_downloads(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let current_selection = if app.data.radarr_data.downloads.items.is_empty() { + DownloadRecord::default() + } else { + app.data.radarr_data.downloads.current_selection_clone() + }; + let width = (area.width as f32 * 0.30) as usize; + draw_table( f, area, @@ -162,6 +171,13 @@ fn draw_downloads(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { output_path, .. } = download_record; + + if current_selection == *download_record && output_path.text.len() > width { + output_path.scroll_text() + } else { + output_path.reset_offset(); + } + let percent = 1f64 - (sizeleft.as_f64().unwrap() / size.as_f64().unwrap()); let file_size: f64 = convert_to_gb(size.as_u64().unwrap()); @@ -169,7 +185,7 @@ fn draw_downloads(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { Cell::from(title.to_owned()), Cell::from(format!("{:.0}%", percent * 100.0)), Cell::from(format!("{:.2} GB", file_size)), - Cell::from(output_path.to_owned()), + Cell::from(output_path.to_string()), Cell::from(indexer.to_owned()), Cell::from(download_client.to_owned()), ]) @@ -189,6 +205,8 @@ fn draw_movie_info(f: &mut Frame<'_, B>, app: &mut App, area: Rect) match active_radarr_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), + ActiveRadarrBlock::Crew => draw_movie_crew(f, app, content_area, block), _ => (), } } @@ -233,6 +251,12 @@ fn draw_movie_history( content_area: Rect, block: Block, ) { + let current_selection = if app.data.radarr_data.movie_history.items.is_empty() { + MovieHistoryItem::default() + } else { + app.data.radarr_data.movie_history.current_selection_clone() + }; + draw_table( f, content_area, @@ -257,8 +281,16 @@ fn draw_movie_history( event_type, } = movie_history_item; + if current_selection == *movie_history_item + && movie_history_item.source_title.text.len() > (content_area.width as f64 * 0.34) as usize + { + source_title.scroll_text(); + } else { + source_title.reset_offset(); + } + Row::new(vec![ - Cell::from(source_title.to_owned()), + Cell::from(source_title.to_string()), Cell::from(event_type.to_owned()), Cell::from( languages @@ -276,6 +308,72 @@ fn draw_movie_history( ); } +fn draw_movie_cast( + f: &mut Frame<'_, B>, + app: &mut App, + content_area: Rect, + block: Block, +) { + draw_table( + f, + content_area, + block, + TableProps { + content: &mut app.data.radarr_data.movie_cast, + constraints: iter::repeat(Constraint::Ratio(1, 2)).take(2).collect(), + table_headers: vec!["Cast Member", "Character"], + }, + |cast_member| { + let Credit { + person_name, + character, + .. + } = cast_member; + + Row::new(vec![ + Cell::from(person_name.to_owned()), + Cell::from(character.clone().unwrap_or_default()), + ]) + .style(style_success()) + }, + app.is_loading, + ) +} + +fn draw_movie_crew( + f: &mut Frame<'_, B>, + app: &mut App, + content_area: Rect, + block: Block, +) { + draw_table( + f, + content_area, + block, + TableProps { + content: &mut app.data.radarr_data.movie_crew, + constraints: iter::repeat(Constraint::Ratio(1, 3)).take(3).collect(), + table_headers: vec!["Crew Member", "Job", "Department"], + }, + |crew_member| { + let Credit { + person_name, + job, + department, + .. + } = crew_member; + + Row::new(vec![ + Cell::from(person_name.to_owned()), + Cell::from(job.clone().unwrap_or_default()), + Cell::from(department.clone().unwrap_or_default()), + ]) + .style(style_success()) + }, + app.is_loading, + ); +} + fn draw_stats_context(f: &mut Frame<'_, B>, app: &App, area: Rect) { let block = title_block("Stats"); @@ -353,7 +451,7 @@ fn draw_stats_context(f: &mut Frame<'_, B>, app: &App, area: Rect) { 1f64 - (free_space.as_u64().unwrap() as f64 / total_space.as_u64().unwrap() as f64) }; - let space_gauge = line_gague_with_label(title.as_str(), ratio); + let space_gauge = line_gauge_with_label(title.as_str(), ratio); f.render_widget(space_gauge, chunks[i + 4]); } diff --git a/src/ui/utils.rs b/src/ui/utils.rs index 178f4a8..47815e0 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -120,10 +120,9 @@ pub fn title_block(title: &str) -> Block<'_> { pub fn logo_block<'a>() -> Block<'a> { Block::default().borders(Borders::ALL).title(Span::styled( - "Managarr - A Servarr management TUI", + " Managarr - A Servarr management TUI ", Style::default() - .fg(Color::Black) - .bg(Color::LightGreen) + .fg(Color::Magenta) .add_modifier(Modifier::BOLD) .add_modifier(Modifier::ITALIC), )) @@ -155,7 +154,7 @@ pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { .split(popup_layout[1])[1] } -pub fn line_gague_with_title(title: &str, ratio: f64) -> LineGauge { +pub fn line_gauge_with_title(title: &str, ratio: f64) -> LineGauge { LineGauge::default() .block(Block::default().title(title)) .gauge_style(Style::default().fg(Color::Cyan)) @@ -164,7 +163,7 @@ pub fn line_gague_with_title(title: &str, ratio: f64) -> LineGauge { .label(Spans::from(format!("{:.0}%", ratio * 100.0))) } -pub fn line_gague_with_label(title: &str, ratio: f64) -> LineGauge { +pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge { LineGauge::default() .block(Block::default()) .gauge_style(Style::default().fg(Color::Cyan))