Added error windows with scrolling text, and a colorized Radarr logo. Also added header row with header tabs

This commit is contained in:
2023-08-08 10:50:04 -06:00
parent daf08c10cc
commit 44db47f8ee
8 changed files with 154 additions and 24 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
# managarr # managarr
A TUI for managing *arr servers. A Servarr management TUI.
+35 -1
View File
@@ -1,11 +1,13 @@
use std::time::Duration; use std::time::Duration;
use log::{debug, error}; use anyhow::anyhow;
use log::error;
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio::time::Instant; use tokio::time::Instant;
use crate::app::models::{TabRoute, TabState};
use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; use crate::app::radarr::{ActiveRadarrBlock, RadarrData};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::network::NetworkEvent; use crate::network::NetworkEvent;
@@ -17,6 +19,7 @@ pub mod radarr;
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum Route { pub enum Route {
Radarr(ActiveRadarrBlock), Radarr(ActiveRadarrBlock),
Sonarr,
} }
impl From<ActiveRadarrBlock> for Route { impl From<ActiveRadarrBlock> for Route {
@@ -30,6 +33,8 @@ const DEFAULT_ROUTE: Route = Route::Radarr(ActiveRadarrBlock::Movies);
pub struct App { pub struct App {
navigation_stack: Vec<Route>, navigation_stack: Vec<Route>,
network_tx: Option<Sender<NetworkEvent>>, network_tx: Option<Sender<NetworkEvent>>,
pub server_tabs: TabState,
pub error: String,
pub client: Client, pub client: Client,
pub title: &'static str, pub title: &'static str,
pub tick_until_poll: u64, pub tick_until_poll: u64,
@@ -56,6 +61,7 @@ impl App {
if let Err(e) = network_tx.send(action).await { if let Err(e) = network_tx.send(action).await {
self.is_loading = false; self.is_loading = false;
error!("Failed to send event. {:?}", e); error!("Failed to send event. {:?}", e);
self.handle_error(anyhow!(e));
} }
} }
} }
@@ -66,10 +72,26 @@ impl App {
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.reset_tick_count(); self.reset_tick_count();
self.error = String::default();
self.data = Data::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) { 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 { if self.tick_count % self.tick_until_poll == 0 || self.is_routing {
match self.get_current_route() { match self.get_current_route() {
Route::Radarr(active_radarr_block) => { Route::Radarr(active_radarr_block) => {
@@ -92,6 +114,7 @@ impl App {
self.dispatch_by_radarr_block(active_block).await; self.dispatch_by_radarr_block(active_block).await;
} }
} }
_ => (),
} }
self.is_routing = false; self.is_routing = false;
@@ -127,6 +150,17 @@ impl Default for App {
App { App {
navigation_stack: vec![DEFAULT_ROUTE], navigation_stack: vec![DEFAULT_ROUTE],
network_tx: None, 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(), client: Client::new(),
title: "Managarr", title: "Managarr",
tick_until_poll: 20, tick_until_poll: 20,
+7
View File
@@ -9,5 +9,12 @@ pub async fn handle_key_events(key: Key, app: &mut App) {
Route::Radarr(active_radarr_block) => { Route::Radarr(active_radarr_block) => {
handle_radarr_key_events(key, app, active_radarr_block).await 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();
} }
} }
+2 -1
View File
@@ -1,6 +1,7 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::models::{Scrollable, ScrollableText, StatefulTable}; use crate::app::models::{Scrollable, ScrollableText, StatefulTable};
use crate::app::radarr::ActiveRadarrBlock; use crate::app::radarr::ActiveRadarrBlock;
use crate::handlers::handle_clear_errors;
use crate::{App, Key}; use crate::{App, Key};
pub async fn handle_radarr_key_events( 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.pop_navigation_stack();
app.data.radarr_data.reset_movie_info_tab(); app.data.radarr_data.reset_movie_info_tab();
} }
_ => (), _ => handle_clear_errors(app).await,
} }
} }
+11 -3
View File
@@ -1,3 +1,4 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use derivative::Derivative; use derivative::Derivative;
use indoc::formatdoc; use indoc::formatdoc;
@@ -200,7 +201,8 @@ impl<'a> Network<'a> {
async fn get_healthcheck(&self, resource: &str) { async fn get_healthcheck(&self, resource: &str) {
if let Err(e) = self.call_radarr_api(resource).await.send().await { 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; let app = self.app.lock().await;
app_update_fn(value, app); 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));
}
} }
} }
+77 -10
View File
@@ -1,12 +1,12 @@
use tui::backend::Backend; use tui::backend::Backend;
use tui::layout::{Constraint, Rect}; use tui::layout::{Alignment, Constraint, Rect};
use tui::text::{Span, Spans, Text}; use tui::text::{Span, Spans, Text};
use tui::widgets::Block;
use tui::widgets::Clear; use tui::widgets::Clear;
use tui::widgets::Paragraph; use tui::widgets::Paragraph;
use tui::widgets::Row; use tui::widgets::Row;
use tui::widgets::Table; use tui::widgets::Table;
use tui::widgets::Tabs; use tui::widgets::Tabs;
use tui::widgets::{Block, Borders, Wrap};
use tui::Frame; use tui::Frame;
use crate::app::models::{StatefulTable, TabState}; 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, BAZARR_LOGO, LIDARR_LOGO, PROWLARR_LOGO, RADARR_LOGO, READARR_LOGO, SONARR_LOGO,
}; };
use crate::ui::utils::{ 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, style_system_function, title_block, vertical_chunks_with_margin,
}; };
@@ -25,18 +26,83 @@ mod utils;
static HIGHLIGHT_SYMBOL: &str = "=> "; static HIGHLIGHT_SYMBOL: &str = "=> ";
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) { pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let main_chunks = vertical_chunks_with_margin( let main_chunks = if !app.error.is_empty() {
vec![Constraint::Length(16), Constraint::Length(0)], let chunks = vertical_chunks_with_margin(
f.size(), vec![
1, 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() { 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<B: Backend>(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 | <enter> select | <tab> 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<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area: Rect) {
let block = Block::default()
.title("Error | <esc> 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<B: Backend>( pub fn draw_popup_over<B: Backend>(
f: &mut Frame<'_, B>, f: &mut Frame<'_, B>,
app: &mut App, app: &mut App,
@@ -86,6 +152,7 @@ pub fn draw_large_popup_over<B: Backend>(
fn draw_context_row<B: Backend>(f: &mut Frame<'_, B>, app: &App, area: Rect) { fn draw_context_row<B: Backend>(f: &mut Frame<'_, B>, app: &App, area: Rect) {
match app.get_current_route() { match app.get_current_route() {
Route::Radarr(_) => radarr_ui::draw_radarr_context_row(f, app, area), Route::Radarr(_) => radarr_ui::draw_radarr_context_row(f, app, area),
_ => (),
} }
} }
+6 -8
View File
@@ -5,7 +5,7 @@ use chrono::{Duration, Utc};
use log::debug; use log::debug;
use tui::backend::Backend; use tui::backend::Backend;
use tui::layout::{Alignment, Constraint, Rect}; use tui::layout::{Alignment, Constraint, Rect};
use tui::style::Style; use tui::style::{Color, Style};
use tui::text::Text; use tui::text::Text;
use tui::widgets::{Block, Cell, Paragraph, Row, Wrap}; use tui::widgets::{Block, Cell, Paragraph, Row, Wrap};
use tui::Frame; use tui::Frame;
@@ -15,7 +15,7 @@ use crate::app::{App, Route};
use crate::logos::RADARR_LOGO; use crate::logos::RADARR_LOGO;
use crate::network::radarr_network::{DiskSpace, DownloadRecord, Movie, MovieHistoryItem}; use crate::network::radarr_network::{DiskSpace, DownloadRecord, Movie, MovieHistoryItem};
use crate::ui::utils::{ 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, line_gague_with_title, style_bold, style_failure, style_success, style_warning, title_block,
vertical_chunks_with_margin, vertical_chunks_with_margin,
}; };
@@ -38,11 +38,7 @@ pub(super) fn draw_radarr_ui<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, ar
} }
pub(super) fn draw_radarr_context_row<B: Backend>(f: &mut Frame<'_, B>, app: &App, area: Rect) { pub(super) fn draw_radarr_context_row<B: Backend>(f: &mut Frame<'_, B>, app: &App, area: Rect) {
let chunks = horizontal_chunks_with_margin( let chunks = horizontal_chunks(vec![Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)], area);
vec![Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
area,
1,
);
draw_stats_context(f, app, chunks[0]); draw_stats_context(f, app, chunks[0]);
draw_downloads_context(f, app, chunks[1]); draw_downloads_context(f, app, chunks[1]);
@@ -332,7 +328,9 @@ fn draw_stats_context<B: Backend>(f: &mut Frame<'_, B>, app: &App, area: Rect) {
))) )))
.block(Block::default()); .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()) .block(Block::default())
.alignment(Alignment::Center); .alignment(Alignment::Center);
let storage = let storage =
+15
View File
@@ -106,6 +106,10 @@ pub fn style_failure() -> Style {
Style::default().fg(Color::Red) Style::default().fg(Color::Red)
} }
pub fn style_help() -> Style {
Style::default().fg(Color::LightBlue)
}
pub fn title_style(title: &str) -> Span<'_> { pub fn title_style(title: &str) -> Span<'_> {
Span::styled(title, style_bold()) Span::styled(title, style_bold())
} }
@@ -114,6 +118,17 @@ pub fn title_block(title: &str) -> Block<'_> {
layout_block_with_title(title_style(title)) 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 { pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default() let popup_layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)