From 44db47f8eeb4dc130c3e0325fd4c5784415a8820 Mon Sep 17 00:00:00 2001 From: Dark-Alex-17 Date: Tue, 8 Aug 2023 10:50:04 -0600 Subject: [PATCH] Added error windows with scrolling text, and a colorized Radarr logo. Also added header row with header tabs --- README.md | 2 +- src/app/mod.rs | 36 +++++++++++++- src/handlers/mod.rs | 7 +++ src/handlers/radarr_handler.rs | 3 +- src/network/radarr_network.rs | 14 ++++-- src/ui/mod.rs | 87 ++++++++++++++++++++++++++++++---- src/ui/radarr_ui.rs | 14 +++--- src/ui/utils.rs | 15 ++++++ 8 files changed, 154 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index d95665b..c0811e0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # managarr -A TUI for managing *arr servers. +A Servarr management TUI. diff --git a/src/app/mod.rs b/src/app/mod.rs index 04543d7..207a123 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,11 +1,13 @@ use std::time::Duration; -use log::{debug, error}; +use anyhow::anyhow; +use log::error; use reqwest::Client; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::Sender; use tokio::time::Instant; +use crate::app::models::{TabRoute, TabState}; use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; use crate::network::radarr_network::RadarrEvent; use crate::network::NetworkEvent; @@ -17,6 +19,7 @@ pub mod radarr; #[derive(Clone, PartialEq, Eq, Debug)] pub enum Route { Radarr(ActiveRadarrBlock), + Sonarr, } impl From for Route { @@ -30,6 +33,8 @@ const DEFAULT_ROUTE: Route = Route::Radarr(ActiveRadarrBlock::Movies); pub struct App { navigation_stack: Vec, network_tx: Option>, + pub server_tabs: TabState, + pub error: String, pub client: Client, pub title: &'static str, pub tick_until_poll: u64, @@ -56,6 +61,7 @@ impl App { if let Err(e) = network_tx.send(action).await { self.is_loading = false; error!("Failed to send event. {:?}", e); + self.handle_error(anyhow!(e)); } } } @@ -66,10 +72,26 @@ impl App { pub fn reset(&mut self) { self.reset_tick_count(); + self.error = String::default(); self.data = Data::default(); } + pub fn handle_error(&mut self, error: anyhow::Error) { + if self.error.is_empty() { + self.error = format!("{} ", error); + } + } + + 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) => { @@ -92,6 +114,7 @@ impl App { self.dispatch_by_radarr_block(active_block).await; } } + _ => (), } self.is_routing = false; @@ -127,6 +150,17 @@ impl Default for App { App { navigation_stack: vec![DEFAULT_ROUTE], network_tx: None, + error: String::default(), + server_tabs: TabState::new(vec![ + TabRoute { + title: "Radarr".to_owned(), + route: ActiveRadarrBlock::Movies.into(), + }, + TabRoute { + title: "Sonarr".to_owned(), + route: Route::Sonarr, + }, + ]), client: Client::new(), title: "Managarr", tick_until_poll: 20, diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 4ee7342..95cdc7b 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -9,5 +9,12 @@ pub async fn handle_key_events(key: Key, app: &mut App) { Route::Radarr(active_radarr_block) => { handle_radarr_key_events(key, app, active_radarr_block).await } + _ => (), + } +} + +pub async fn handle_clear_errors(app: &mut App) { + if !app.error.is_empty() { + app.error = String::default(); } } diff --git a/src/handlers/radarr_handler.rs b/src/handlers/radarr_handler.rs index 8bd5bec..444607c 100644 --- a/src/handlers/radarr_handler.rs +++ b/src/handlers/radarr_handler.rs @@ -1,6 +1,7 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::models::{Scrollable, ScrollableText, StatefulTable}; use crate::app::radarr::ActiveRadarrBlock; +use crate::handlers::handle_clear_errors; use crate::{App, Key}; pub async fn handle_radarr_key_events( @@ -97,6 +98,6 @@ async fn handle_esc(app: &mut App, active_radarr_block: ActiveRadarrBlock) { app.pop_navigation_stack(); app.data.radarr_data.reset_movie_info_tab(); } - _ => (), + _ => handle_clear_errors(app).await, } } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index bc685fd..69fd272 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use chrono::{DateTime, Utc}; use derivative::Derivative; use indoc::formatdoc; @@ -200,7 +201,8 @@ impl<'a> Network<'a> { async fn get_healthcheck(&self, resource: &str) { if let Err(e) = self.call_radarr_api(resource).await.send().await { - error!("Healthcheck failed. {:?}", e) + error!("Healthcheck failed. {:?}", e); + self.app.lock().await.handle_error(anyhow!(e)); } } @@ -399,9 +401,15 @@ impl<'a> Network<'a> { let app = self.app.lock().await; app_update_fn(value, app); } - Err(e) => error!("Failed to parse response! {:?}", e), + Err(e) => { + error!("Failed to parse response! {:?}", e); + self.app.lock().await.handle_error(anyhow!(e)); + } }, - Err(e) => error!("Failed to fetch resource. {:?}", e), + Err(e) => { + error!("Failed to fetch resource. {:?}", e); + self.app.lock().await.handle_error(anyhow!(e)); + } } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e17bc77..14fd8e5 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,12 +1,12 @@ use tui::backend::Backend; -use tui::layout::{Constraint, Rect}; +use tui::layout::{Alignment, Constraint, Rect}; use tui::text::{Span, Spans, Text}; -use tui::widgets::Block; use tui::widgets::Clear; use tui::widgets::Paragraph; use tui::widgets::Row; use tui::widgets::Table; use tui::widgets::Tabs; +use tui::widgets::{Block, Borders, Wrap}; use tui::Frame; use crate::app::models::{StatefulTable, TabState}; @@ -15,7 +15,8 @@ use crate::logos::{ BAZARR_LOGO, LIDARR_LOGO, PROWLARR_LOGO, RADARR_LOGO, READARR_LOGO, SONARR_LOGO, }; use crate::ui::utils::{ - centered_rect, layout_block_top_border, style_default_bold, style_highlight, style_secondary, + centered_rect, horizontal_chunks_with_margin, layout_block_top_border, logo_block, + style_default_bold, style_failure, style_help, style_highlight, style_primary, style_secondary, style_system_function, title_block, vertical_chunks_with_margin, }; @@ -25,18 +26,83 @@ mod utils; static HIGHLIGHT_SYMBOL: &str = "=> "; pub fn ui(f: &mut Frame, app: &mut App) { - let main_chunks = vertical_chunks_with_margin( - vec![Constraint::Length(16), Constraint::Length(0)], - f.size(), - 1, - ); + let main_chunks = if !app.error.is_empty() { + let chunks = vertical_chunks_with_margin( + vec![ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(16), + Constraint::Length(0), + ], + f.size(), + 1, + ); - draw_context_row(f, app, main_chunks[0]); + draw_error(f, app, chunks[1]); + + vec![chunks[0], chunks[2], chunks[3]] + } else { + vertical_chunks_with_margin( + vec![ + Constraint::Length(3), + Constraint::Length(16), + Constraint::Length(0), + ], + f.size(), + 1, + ) + }; + + draw_header_row(f, app, main_chunks[0]); + draw_context_row(f, app, main_chunks[1]); match app.get_current_route() { - Route::Radarr(_) => radarr_ui::draw_radarr_ui(f, app, main_chunks[1]), + Route::Radarr(_) => radarr_ui::draw_radarr_ui(f, app, main_chunks[2]), + _ => (), } } +fn draw_header_row(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let chunks = + horizontal_chunks_with_margin(vec![Constraint::Length(75), Constraint::Min(0)], area, 1); + + let titles = app + .server_tabs + .tabs + .iter() + .map(|tab| Spans::from(Span::styled(&tab.title, style_default_bold()))) + .collect(); + let tabs = Tabs::new(titles) + .block(logo_block()) + .highlight_style(style_secondary()) + .select(app.server_tabs.index); + let help = Paragraph::new(Text::from( + "<↑↓> scroll | select | change servarr | help ", + )) + .block(Block::default()) + .style(style_help()) + .alignment(Alignment::Right); + + f.render_widget(tabs, area); + f.render_widget(help, chunks[1]); +} + +fn draw_error(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let block = Block::default() + .title("Error | to close") + .style(style_failure()) + .borders(Borders::ALL); + + let mut text = Text::from(app.error.clone()); + text.patch_style(style_failure()); + + let paragraph = Paragraph::new(text) + .block(block) + .wrap(Wrap { trim: true }) + .style(style_primary()); + + f.render_widget(paragraph, area); +} + pub fn draw_popup_over( f: &mut Frame<'_, B>, app: &mut App, @@ -86,6 +152,7 @@ pub fn draw_large_popup_over( fn draw_context_row(f: &mut Frame<'_, B>, app: &App, area: Rect) { match app.get_current_route() { Route::Radarr(_) => radarr_ui::draw_radarr_context_row(f, app, area), + _ => (), } } diff --git a/src/ui/radarr_ui.rs b/src/ui/radarr_ui.rs index 706aa3a..4381896 100644 --- a/src/ui/radarr_ui.rs +++ b/src/ui/radarr_ui.rs @@ -5,7 +5,7 @@ use chrono::{Duration, Utc}; use log::debug; use tui::backend::Backend; use tui::layout::{Alignment, Constraint, Rect}; -use tui::style::Style; +use tui::style::{Color, Style}; use tui::text::Text; use tui::widgets::{Block, Cell, Paragraph, Row, Wrap}; use tui::Frame; @@ -15,7 +15,7 @@ use crate::app::{App, Route}; use crate::logos::RADARR_LOGO; use crate::network::radarr_network::{DiskSpace, DownloadRecord, Movie, MovieHistoryItem}; use crate::ui::utils::{ - horizontal_chunks_with_margin, layout_block_top_border, line_gague_with_label, + 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, vertical_chunks_with_margin, }; @@ -38,11 +38,7 @@ pub(super) fn draw_radarr_ui(f: &mut Frame<'_, B>, app: &mut App, ar } pub(super) fn draw_radarr_context_row(f: &mut Frame<'_, B>, app: &App, area: Rect) { - let chunks = horizontal_chunks_with_margin( - vec![Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)], - area, - 1, - ); + let chunks = horizontal_chunks(vec![Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)], area); draw_stats_context(f, app, chunks[0]); draw_downloads_context(f, app, chunks[1]); @@ -332,7 +328,9 @@ fn draw_stats_context(f: &mut Frame<'_, B>, app: &App, area: Rect) { ))) .block(Block::default()); - let logo = Paragraph::new(Text::from(RADARR_LOGO)) + let mut logo_text = Text::from(RADARR_LOGO); + logo_text.patch_style(Style::default().fg(Color::LightYellow)); + let logo = Paragraph::new(logo_text) .block(Block::default()) .alignment(Alignment::Center); let storage = diff --git a/src/ui/utils.rs b/src/ui/utils.rs index f965ec2..178f4a8 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -106,6 +106,10 @@ pub fn style_failure() -> Style { Style::default().fg(Color::Red) } +pub fn style_help() -> Style { + Style::default().fg(Color::LightBlue) +} + pub fn title_style(title: &str) -> Span<'_> { Span::styled(title, style_bold()) } @@ -114,6 +118,17 @@ pub fn title_block(title: &str) -> Block<'_> { layout_block_with_title(title_style(title)) } +pub fn logo_block<'a>() -> Block<'a> { + Block::default().borders(Borders::ALL).title(Span::styled( + "Managarr - A Servarr management TUI", + Style::default() + .fg(Color::Black) + .bg(Color::LightGreen) + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::ITALIC), + )) +} + pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical)