Full popup description functionality

This commit is contained in:
2023-08-08 10:50:04 -06:00
parent b24e0cdccd
commit ec980ea32c
11 changed files with 146 additions and 58 deletions
+1
View File
@@ -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"] }
+22 -9
View File
@@ -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<NetworkEvent>, tick_until_poll: u64, config: AppConfig) -> Self {
pub fn new(network_tx: Sender<NetworkEvent>, 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(),
+7 -3
View File
@@ -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();
}
}
+1 -1
View File
@@ -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
}
+5 -2
View File
@@ -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();
}
_ => (),
}
}
+6 -5
View File
@@ -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<NetworkEvent>, app: &Arc<Mute
let network = Network::new(reqwest::Client::new(), app);
while let Some(network_event) = network_rx.recv().await {
debug!(
"**************************************MAIN Received network event: {:?}",
network_event
);
network.handle_network_event(network_event).await;
}
}
@@ -78,6 +74,11 @@ async fn start_ui(app: &Arc<Mutex<App>>) -> 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()? {
+33 -25
View File
@@ -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::<Movie>(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,
+4 -4
View File
@@ -10,20 +10,20 @@ pub async fn parse_response<T: DeserializeOwned>(response: Response) -> Result<T
pub fn get_movie_status(
has_file: bool,
downloads_vec: &Vec<DownloadRecord>,
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()
+40 -6
View File
@@ -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<B: Backend>(f: &mut Frame<B>, 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<B: Backend>(
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<B: Backend>(
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<B: Backend>(
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<B: Backend>(
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<B: Backend>(f: &mut Frame<'_, B>, app: &App, area: Rect) {
let chunks = horizontal_chunks(
vec![
@@ -80,9 +112,11 @@ pub fn loading<B: Backend>(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)
+23 -3
View File
@@ -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<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, ar
pub(super) fn draw_movie_details<B: Backend>(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::<Vec<&str>>()[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(),
}
}
+4
View File
@@ -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)
}