From ec980ea32ce880a443a073077b52229e117a29df Mon Sep 17 00:00:00 2001 From: Dark-Alex-17 Date: Tue, 8 Aug 2023 10:50:04 -0600 Subject: [PATCH] Full popup description functionality --- Cargo.toml | 1 + src/app/mod.rs | 31 ++++++++++++------ src/app/radarr.rs | 10 ++++-- src/handlers/mod.rs | 2 +- src/handlers/radarr_handler.rs | 7 ++-- src/main.rs | 11 ++++--- src/network/radarr_network.rs | 58 +++++++++++++++++++--------------- src/network/utils.rs | 8 ++--- src/ui/mod.rs | 46 +++++++++++++++++++++++---- src/ui/radarr_ui.rs | 26 +++++++++++++-- src/ui/utils.rs | 4 +++ 11 files changed, 146 insertions(+), 58 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4137051..bff4fa1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ clap = { version = "4.0.30", features = ["help", "usage", "error-context", "deri confy = { version = "0.5.1", default_features = false, features = ["yaml_conf"] } crossterm = "0.25.0" derivative = "2.2.0" +indoc = "1.0.8" log = "0.4.17" log4rs = { version = "1.2.0", features = ["file_appender"] } reqwest = { version = "0.11.13", features = ["json"] } diff --git a/src/app/mod.rs b/src/app/mod.rs index aa85e90..f0d3fc9 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,7 +1,10 @@ +use std::time::Duration; + use log::{debug, error}; use reqwest::Client; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::Sender; +use tokio::time::Instant; use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; use crate::network::radarr_network::RadarrEvent; @@ -11,7 +14,7 @@ pub(crate) mod key_binding; pub mod models; pub mod radarr; -#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] pub enum Route { Radarr(ActiveRadarrBlock), } @@ -31,6 +34,8 @@ pub struct App { pub title: &'static str, pub tick_until_poll: u64, pub tick_count: u64, + pub last_tick: Instant, + pub network_tick_frequency: Duration, pub is_routing: bool, pub is_loading: bool, pub config: AppConfig, @@ -38,17 +43,15 @@ pub struct App { } impl App { - pub fn new(network_tx: Sender, tick_until_poll: u64, config: AppConfig) -> Self { + pub fn new(network_tx: Sender, config: AppConfig) -> Self { App { network_tx: Some(network_tx), - tick_until_poll, config, ..App::default() } } pub async fn dispatch(&mut self, action: NetworkEvent) { - self.is_loading = true; if let Some(network_tx) = &self.network_tx { if let Err(e) = network_tx.send(action).await { self.is_loading = false; @@ -74,12 +77,20 @@ impl App { 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; } - self.dispatch(RadarrEvent::GetOverview.into()).await; - self.dispatch(RadarrEvent::GetStatus.into()).await; - - self.dispatch_by_radarr_block(active_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_block).await; + } } } @@ -113,8 +124,10 @@ impl Default for App { network_tx: None, client: Client::new(), title: "Managarr", - tick_until_poll: 0, + tick_until_poll: 20, tick_count: 0, + network_tick_frequency: Duration::from_secs(5), + last_tick: Instant::now(), is_loading: false, is_routing: false, config: AppConfig::default(), diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 8030fcd..ce2ca84 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -18,7 +18,7 @@ pub struct RadarrData { pub movie_details: ScrollableText, } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] pub enum ActiveRadarrBlock { AddMovie, Calendar, @@ -35,15 +35,19 @@ pub enum ActiveRadarrBlock { impl App { pub(super) async fn dispatch_by_radarr_block(&mut self, active_radarr_block: ActiveRadarrBlock) { - self.reset_tick_count(); 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 => self.dispatch(RadarrEvent::GetMovieDetails.into()).await, + ActiveRadarrBlock::MovieDetails => { + self.is_loading = true; + self.dispatch(RadarrEvent::GetMovieDetails.into()).await + } _ => (), } + + self.reset_tick_count(); } } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 873718e..4ee7342 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -5,7 +5,7 @@ use crate::handlers::radarr_handler::handle_radarr_key_events; mod radarr_handler; pub async fn handle_key_events(key: Key, app: &mut App) { - match *app.get_current_route() { + match app.get_current_route().clone() { Route::Radarr(active_radarr_block) => { handle_radarr_key_events(key, app, active_radarr_block).await } diff --git a/src/handlers/radarr_handler.rs b/src/handlers/radarr_handler.rs index 7d93bb2..56c1e6a 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; +use crate::app::models::{Scrollable, ScrollableText}; use crate::app::radarr::ActiveRadarrBlock; use crate::{App, Key}; @@ -42,7 +42,10 @@ 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 => app.pop_navigation_stack(), + ActiveRadarrBlock::MovieDetails => { + app.pop_navigation_stack(); + app.data.radarr_data.movie_details = ScrollableText::default(); + } _ => (), } } diff --git a/src/main.rs b/src/main.rs index 3ba1d41..491f8b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,7 +39,7 @@ async fn main() -> Result<()> { let config = confy::load("managarr", "config")?; let (sync_network_tx, sync_network_rx) = mpsc::channel(500); - let app = Arc::new(Mutex::new(App::new(sync_network_tx, 5000 / 250, config))); + let app = Arc::new(Mutex::new(App::new(sync_network_tx, config))); let app_nw = Arc::clone(&app); @@ -55,10 +55,6 @@ async fn start_networking(mut network_rx: Receiver, app: &Arc>) -> Result<()> { loop { let mut app = app.lock().await; + + if is_first_render { + app.is_loading = true; + } + terminal.draw(|f| ui(f, &mut app))?; match input_events.next()? { diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index e94dab3..a68fdf3 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, Utc}; use derivative::Derivative; +use indoc::{formatdoc, indoc}; use log::{debug, error}; use reqwest::RequestBuilder; use serde::de::DeserializeOwned; @@ -148,7 +149,6 @@ impl<'a> Network<'a> { RadarrEvent::GetStatus => self.get_status(RadarrEvent::GetStatus.resource()).await, RadarrEvent::GetMovies => self.get_movies(RadarrEvent::GetMovies.resource()).await, RadarrEvent::GetMovieDetails => { - debug!("TEST received GetMovieDetails event"); self .get_movie_details(RadarrEvent::GetMovieDetails.resource()) .await @@ -164,8 +164,6 @@ impl<'a> Network<'a> { .await } } - - let mut app = self.app.lock().await; } async fn get_healthcheck(&self, resource: &str) { @@ -207,7 +205,6 @@ impl<'a> Network<'a> { } async fn get_movie_details(&self, resource: &str) { - debug!("TEST handling get_movie_details"); let movie_id = self .app .lock() @@ -223,7 +220,6 @@ impl<'a> Network<'a> { let mut url = resource.to_owned(); url.push('/'); url.push_str(movie_id.to_string().as_str()); - debug!("TEST sending request{}", url.as_str()); self .handle_get_request::(url.as_str(), |movie_response, mut app| { let Movie { @@ -251,39 +247,51 @@ impl<'a> Network<'a> { .unwrap() .to_owned(); let imdb_rating = if let Some(rating) = ratings.imdb { - rating.value.as_f64().unwrap() + if let Some(value) = rating.value.as_f64() { + format!("{:.1}", value) + } else { + "".to_owned() + } } else { - 0f64 + "".to_owned() }; let tmdb_rating = if let Some(rating) = ratings.tmdb { - rating.value.as_u64().unwrap() + if let Some(value) = rating.value.as_f64() { + format!("{}%", value * 10f64) + } else { + "".to_owned() + } } else { - 0u64 + "".to_owned() }; let rotten_tomatoes_rating = if let Some(rating) = ratings.rotten_tomatoes { - rating.value.as_u64().unwrap() + if let Some(value) = rating.value.as_u64() { + format!("{}%", value) + } else { + "".to_owned() + } } else { - 0u64 + "".to_owned() }; let status = get_movie_status(has_file, &app.data.radarr_data.downloads.items, id); - app.data.radarr_data.movie_details = ScrollableText::with_string(format!( - "Title: {}\n - Year: {}\n - Runtime: {}h {}m\n - Status: {}\n - Description: {}\n - TMDB: {}%\n - IMDB: {:.1}\n - Rotten Tomatoes: {}%\n - Quality Profile: {}\n - Size: {:.2} GB\n - Path: {}\n - Studio: {}\n - Genres: {}", + app.data.radarr_data.movie_details = ScrollableText::with_string(formatdoc!( + "Title: {} + Year: {} + Runtime: {}h {}m + Status: {} + Description: {} + TMDB: {} + IMDB: {} + Rotten Tomatoes: {} + Quality Profile: {} + Size: {:.2} GB + Path: {} + Studio: {} + Genres: {}", title, year, hours, diff --git a/src/network/utils.rs b/src/network/utils.rs index 1b83d21..bf2a917 100644 --- a/src/network/utils.rs +++ b/src/network/utils.rs @@ -10,20 +10,20 @@ pub async fn parse_response(response: Response) -> Result, + downloads_vec: &[DownloadRecord], movie_id: Number, ) -> String { if !has_file { if let Some(download) = downloads_vec .iter() - .find(|&download| download.movie_id == movie_id) + .find(|&download| download.movie_id.as_u64().unwrap() == movie_id.as_u64().unwrap()) { if download.status == "downloading" { return "Downloading".to_owned(); } - - return "Missing".to_owned(); } + + return "Missing".to_owned(); } "Downloaded".to_owned() diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b25c74b..32ebb01 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -11,7 +11,7 @@ use crate::logos::{ }; use crate::ui::utils::{ centered_rect, horizontal_chunks, horizontal_chunks_with_margin, style_secondary, - vertical_chunks, vertical_chunks_with_margin, + style_system_function, vertical_chunks, vertical_chunks_with_margin, }; mod radarr_ui; @@ -27,10 +27,10 @@ pub fn ui(f: &mut Frame, app: &mut App) { ); draw_context_row(f, app, main_chunks[0]); - match *app.get_current_route() { + match app.get_current_route().clone() { Route::Radarr(active_radarr_block) => match active_radarr_block { ActiveRadarrBlock::Movies => radarr_ui::draw_radarr_ui(f, app, main_chunks[1]), - ActiveRadarrBlock::MovieDetails => draw_popup_over( + ActiveRadarrBlock::MovieDetails => draw_small_popup_over( f, app, main_chunks[1], @@ -48,14 +48,46 @@ pub fn draw_popup_over( area: Rect, background_fn: fn(&mut Frame<'_, B>, &mut App, Rect), popup_fn: fn(&mut Frame<'_, B>, &App, Rect), + percent_x: u16, + percent_y: u16, ) { background_fn(f, app, area); - let popup_area = centered_rect(75, 75, f.size()); + let popup_area = centered_rect(percent_x, percent_y, f.size()); f.render_widget(Clear, popup_area); popup_fn(f, app, popup_area); } +pub fn draw_small_popup_over( + f: &mut Frame<'_, B>, + app: &mut App, + area: Rect, + background_fn: fn(&mut Frame<'_, B>, &mut App, Rect), + popup_fn: fn(&mut Frame<'_, B>, &App, Rect), +) { + draw_popup_over(f, app, area, background_fn, popup_fn, 40, 40); +} + +pub fn draw_medium_popup_over( + f: &mut Frame<'_, B>, + app: &mut App, + area: Rect, + background_fn: fn(&mut Frame<'_, B>, &mut App, Rect), + popup_fn: fn(&mut Frame<'_, B>, &App, Rect), +) { + draw_popup_over(f, app, area, background_fn, popup_fn, 60, 60); +} + +pub fn draw_large_popup_over( + f: &mut Frame<'_, B>, + app: &mut App, + area: Rect, + background_fn: fn(&mut Frame<'_, B>, &mut App, Rect), + popup_fn: fn(&mut Frame<'_, B>, &App, Rect), +) { + draw_popup_over(f, app, area, background_fn, popup_fn, 75, 75); +} + fn draw_context_row(f: &mut Frame<'_, B>, app: &App, area: Rect) { let chunks = horizontal_chunks( vec![ @@ -80,9 +112,11 @@ pub fn loading(f: &mut Frame<'_, B>, block: Block<'_>, area: Rect, i if is_loading { let text = "\n\n Loading ...\n\n".to_owned(); let mut text = Text::from(text); - text.patch_style(style_secondary()); + text.patch_style(style_system_function()); - let paragraph = Paragraph::new(text).style(style_secondary()).block(block); + let paragraph = Paragraph::new(text) + .style(style_system_function()) + .block(block); f.render_widget(paragraph, area); } else { f.render_widget(block, area) diff --git a/src/ui/radarr_ui.rs b/src/ui/radarr_ui.rs index 2b5d45f..32ff2d2 100644 --- a/src/ui/radarr_ui.rs +++ b/src/ui/radarr_ui.rs @@ -1,6 +1,7 @@ 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, Modifier, Style}; @@ -81,11 +82,21 @@ pub(super) fn draw_radarr_ui(f: &mut Frame<'_, B>, app: &mut App, ar pub(super) fn draw_movie_details(f: &mut Frame<'_, B>, app: &App, area: Rect) { let block = title_block("Movie Details"); - let movie_details = &app.data.radarr_data.movie_details.get_text(); + let movie_details = app.data.radarr_data.movie_details.get_text(); if !movie_details.is_empty() { - let mut text = Text::from(movie_details.clone()); - text.patch_style(style_primary()); + let download_status = app + .data + .radarr_data + .movie_details + .items + .iter() + .find(|&line| line.starts_with("Status: ")) + .unwrap() + .split(": ") + .collect::>()[1]; + let mut text = Text::from(movie_details); + text.patch_style(determine_style_from_download_status(download_status)); let paragraph = Paragraph::new(text) .block(block) @@ -197,3 +208,12 @@ fn determine_row_style(app: &App, movie: &Movie) -> Style { style_primary() } + +fn determine_style_from_download_status(download_status: &str) -> Style { + match download_status { + "Downloaded" => style_primary(), + "Downloading" => style_secondary(), + "Missing" => style_tertiary(), + _ => style_primary(), + } +} diff --git a/src/ui/utils.rs b/src/ui/utils.rs index 5608d7c..ea400ec 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -65,6 +65,10 @@ pub fn style_default() -> Style { Style::default().fg(Color::White) } +pub fn style_system_function() -> Style { + Style::default().fg(Color::Yellow) +} + pub fn style_primary() -> Style { Style::default().fg(Color::Green) }