feat(ui): Initial UI support for switching to Sonarr tabs

This commit is contained in:
2024-11-29 15:58:19 -07:00
parent 4d1b0fe301
commit 08f190fc6e
13 changed files with 537 additions and 19 deletions
+18 -3
View File
@@ -5,9 +5,9 @@ mod tests {
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES}; use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
use crate::app::{App, AppConfig, ServarrConfig, DEFAULT_ROUTE}; use crate::app::{App, AppConfig, Data, ServarrConfig, DEFAULT_ROUTE};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
use crate::models::{HorizontallyScrollableText, TabRoute}; use crate::models::{HorizontallyScrollableText, TabRoute};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::network::NetworkEvent; use crate::network::NetworkEvent;
@@ -118,10 +118,23 @@ mod tests {
#[test] #[test]
fn test_reset() { fn test_reset() {
let radarr_data = RadarrData {
version: "test".into(),
..RadarrData::default()
};
let sonarr_data = SonarrData {
version: "test".into(),
..SonarrData::default()
};
let data = Data {
radarr_data,
sonarr_data,
};
let mut app = App { let mut app = App {
tick_count: 2, tick_count: 2,
error: "Test error".to_owned().into(), error: "Test error".to_owned().into(),
is_first_render: false, is_first_render: false,
data,
..App::default() ..App::default()
}; };
@@ -130,6 +143,8 @@ mod tests {
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
assert_eq!(app.error, HorizontallyScrollableText::default()); assert_eq!(app.error, HorizontallyScrollableText::default());
assert!(app.is_first_render); assert!(app.is_first_render);
assert!(app.data.radarr_data.version.is_empty());
assert!(app.data.sonarr_data.version.is_empty());
} }
#[test] #[test]
+1 -1
View File
@@ -78,12 +78,12 @@ impl<'a> App<'a> {
self.tick_count = 0; self.tick_count = 0;
} }
// Allowing this code for now since we'll eventually be implementing additional Servarr support and we'll need it then
#[allow(dead_code)] #[allow(dead_code)]
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.reset_tick_count(); self.reset_tick_count();
self.error = HorizontallyScrollableText::default(); self.error = HorizontallyScrollableText::default();
self.is_first_render = true; self.is_first_render = true;
self.data = Data::default();
} }
pub fn handle_error(&mut self, error: Error) { pub fn handle_error(&mut self, error: Error) {
+1
View File
@@ -141,6 +141,7 @@ impl<'a> App<'a> {
self.refresh_radarr_metadata().await; self.refresh_radarr_metadata().await;
self.dispatch_by_radarr_block(&active_radarr_block).await; self.dispatch_by_radarr_block(&active_radarr_block).await;
self.is_first_render = false; self.is_first_render = false;
return;
} }
if self.should_refresh { if self.should_refresh {
+1
View File
@@ -138,6 +138,7 @@ impl<'a> App<'a> {
self.refresh_sonarr_metadata().await; self.refresh_sonarr_metadata().await;
self.dispatch_by_sonarr_block(&active_sonarr_block).await; self.dispatch_by_sonarr_block(&active_sonarr_block).await;
self.is_first_render = false; self.is_first_render = false;
return;
} }
if self.should_refresh { if self.should_refresh {
+8
View File
@@ -10,6 +10,7 @@ mod tests {
use crate::handlers::{handle_clear_errors, handle_prompt_toggle}; use crate::handlers::{handle_clear_errors, handle_prompt_toggle};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::HorizontallyScrollableText;
use crate::models::Route; use crate::models::Route;
#[test] #[test]
@@ -30,19 +31,26 @@ mod tests {
T: Into<Route> + Copy, T: Into<Route> + Copy,
{ {
let mut app = App::default(); let mut app = App::default();
app.error = "Test".into();
app.server_tabs.set_index(index); app.server_tabs.set_index(index);
handle_events(DEFAULT_KEYBINDINGS.previous_servarr.key, &mut app); handle_events(DEFAULT_KEYBINDINGS.previous_servarr.key, &mut app);
assert_eq!(app.server_tabs.get_active_route(), left_block.into()); assert_eq!(app.server_tabs.get_active_route(), left_block.into());
assert_eq!(app.get_current_route(), left_block.into()); assert_eq!(app.get_current_route(), left_block.into());
assert!(app.is_first_render);
assert_eq!(app.error, HorizontallyScrollableText::default());
app.server_tabs.set_index(index); app.server_tabs.set_index(index);
app.is_first_render = false;
app.error = "Test".into();
handle_events(DEFAULT_KEYBINDINGS.next_servarr.key, &mut app); handle_events(DEFAULT_KEYBINDINGS.next_servarr.key, &mut app);
assert_eq!(app.server_tabs.get_active_route(), right_block.into()); assert_eq!(app.server_tabs.get_active_route(), right_block.into());
assert_eq!(app.get_current_route(), right_block.into()); assert_eq!(app.get_current_route(), right_block.into());
assert!(app.is_first_render);
assert_eq!(app.error, HorizontallyScrollableText::default());
} }
#[rstest] #[rstest]
+2
View File
@@ -82,9 +82,11 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route> + Copy> {
pub fn handle_events(key: Key, app: &mut App<'_>) { pub fn handle_events(key: Key, app: &mut App<'_>) {
if key == DEFAULT_KEYBINDINGS.next_servarr.key { if key == DEFAULT_KEYBINDINGS.next_servarr.key {
app.reset();
app.server_tabs.next(); app.server_tabs.next();
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route()); app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
} else if key == DEFAULT_KEYBINDINGS.previous_servarr.key { } else if key == DEFAULT_KEYBINDINGS.previous_servarr.key {
app.reset();
app.server_tabs.previous(); app.server_tabs.previous();
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route()); app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
} else if let Route::Radarr(active_radarr_block, context) = app.get_current_route() { } else if let Route::Radarr(active_radarr_block, context) = app.get_current_route() {
+8 -9
View File
@@ -6,15 +6,14 @@ pub const RADARR_LOGO: &str = "⠀⣠⣶⢶⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⢿⡇⠀⠀⣀⣤⣶⡿⠛⠉⠀⠀⠀⠀ ⠀⢿⡇⠀⠀⣀⣤⣶⡿⠛⠉⠀⠀⠀⠀
⠀⠀⠰⠶⡿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠰⠶⡿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀
"; ";
// Allowing this code for now since we'll eventually be implementing additional Servarr support and we'll need it then pub const SONARR_LOGO: &str = "⠀⠀⠀⣠⣴⣶⣿⣿⣿⣿⣶⣦⣄⠀⠀⠀
#[allow(dead_code)] ⠀⣠⡀⠈⠻⣿⣿⣿⣿⣿⣿⠟⠁⢀⣀⠀
pub const SONARR_LOGO: &str = " ⢰⣿⣿⣦⠐⠄⠉⠉⠉⠉⠠⠂⣰⣿⣿⡆
⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀ ⣿⣿⣿⣿⡆⠀⣴⣿⣿⣦⠀⢰⣿⣿⣿⣿
⠀⠀⠀⠀⠀⠀⢀⣄⠙⠻⠟⠋⣤⠀⠀⠀⠀⠀⠀⠀ ⣿⣿⣿⣿⡇⠀⠻⣿⣿⠟⠀⠸⣿⣿⣿⣿
⠀⠀⠀⠀⠀⠀⢸⣿⠆⢾⡗⢸⣿⡇⠀⠀⠀⠀⠀⠀ ⠸⣿⣿⠟⠠⠂⠀⢀⡀⠀⠐⠄⠻⣿⣿⠇
⠀⠀⠀⠀⠀⠈⠋⣠⣴⣦⣄⠛⠀⠀⠀⠀⠀⠀ ⠙⠁⢀⣴⣾⣿⣿⣿⣿⣷⣦⡀⠈⠋
⠀⠀⠀⠀⠀⠈⠉⠉⠁⠀⠀⠀⠀⠀ ⠘⠻⠿⣿⣿⣿⣿⠿⠟⠋
"; ";
// Allowing this code for now since we'll eventually be implementing additional Servarr support and we'll need it then // Allowing this code for now since we'll eventually be implementing additional Servarr support and we'll need it then
#[allow(dead_code)] #[allow(dead_code)]
+12 -3
View File
@@ -8,6 +8,7 @@ use ratatui::widgets::Paragraph;
use ratatui::widgets::Tabs; use ratatui::widgets::Tabs;
use ratatui::widgets::Wrap; use ratatui::widgets::Wrap;
use ratatui::Frame; use ratatui::Frame;
use sonarr_ui::SonarrUi;
use crate::app::App; use crate::app::App;
use crate::models::{HorizontallyScrollableText, Route, TabState}; use crate::models::{HorizontallyScrollableText, Route, TabState};
@@ -20,6 +21,7 @@ use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::popup::Size; use crate::ui::widgets::popup::Size;
mod radarr_ui; mod radarr_ui;
mod sonarr_ui;
mod styles; mod styles;
mod utils; mod utils;
mod widgets; mod widgets;
@@ -57,9 +59,16 @@ pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) {
draw_header_row(f, app, header_area); draw_header_row(f, app, header_area);
if RadarrUi::accepts(app.get_current_route()) { match app.get_current_route() {
RadarrUi::draw_context_row(f, app, context_area); route if RadarrUi::accepts(route) => {
RadarrUi::draw(f, app, table_area); RadarrUi::draw_context_row(f, app, context_area);
RadarrUi::draw(f, app, table_area);
}
route if SonarrUi::accepts(route) => {
SonarrUi::draw_context_row(f, app, context_area);
SonarrUi::draw(f, app, table_area);
}
_ => (),
} }
} }
+5 -3
View File
@@ -1,4 +1,4 @@
use std::iter; use std::{cmp, iter};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::layout::{Constraint, Layout, Rect};
@@ -178,15 +178,17 @@ fn draw_downloads_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
if !downloads_vec.is_empty() { if !downloads_vec.is_empty() {
f.render_widget(block, area); f.render_widget(block, area);
let max_items = (((area.height as f64 / 2.0).floor() * 2.0) as usize) / 2;
let items = cmp::min(downloads_vec.len(), max_items - 1);
let download_item_areas = Layout::vertical( let download_item_areas = Layout::vertical(
iter::repeat(Constraint::Length(2)) iter::repeat(Constraint::Length(2))
.take(downloads_vec.len()) .take(items)
.collect::<Vec<Constraint>>(), .collect::<Vec<Constraint>>(),
) )
.margin(1) .margin(1)
.split(area); .split(area);
for i in 0..downloads_vec.len() { for i in 0..items {
let DownloadRecord { let DownloadRecord {
title, title,
sizeleft, sizeleft,
@@ -0,0 +1,75 @@
#[cfg(test)]
mod tests {
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::{
servarr_data::sonarr::sonarr_data::SERIES_BLOCKS, sonarr_models::SeriesStatus,
};
use crate::ui::sonarr_ui::library::LibraryUi;
use crate::ui::styles::ManagarrStyle;
use crate::ui::DrawUi;
use pretty_assertions::assert_eq;
use ratatui::widgets::{Cell, Row};
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::{
models::sonarr_models::{Series, SeriesStatistics},
ui::sonarr_ui::library::decorate_series_row_with_style,
};
#[test]
fn test_library_ui_accepts() {
let mut library_ui_blocks = Vec::new();
library_ui_blocks.extend(SERIES_BLOCKS);
ActiveSonarrBlock::iter().for_each(|active_radarr_block| {
if library_ui_blocks.contains(&active_radarr_block) {
assert!(LibraryUi::accepts(active_radarr_block.into()));
} else {
assert!(!LibraryUi::accepts(active_radarr_block.into()));
}
});
}
#[rstest]
#[case(SeriesStatus::Ended, None, RowStyle::Missing)]
#[case(SeriesStatus::Ended, Some(59.0), RowStyle::Missing)]
#[case(SeriesStatus::Ended, Some(100.0), RowStyle::Downloaded)]
#[case(SeriesStatus::Continuing, None, RowStyle::Missing)]
#[case(SeriesStatus::Continuing, Some(59.0), RowStyle::Missing)]
#[case(SeriesStatus::Continuing, Some(100.0), RowStyle::Unreleased)]
#[case(SeriesStatus::Upcoming, None, RowStyle::Unreleased)]
#[case(SeriesStatus::Deleted, None, RowStyle::Missing)]
fn test_decorate_series_row_with_style(
#[case] series_status: SeriesStatus,
#[case] percent_of_episodes: Option<f64>,
#[case] expected_row_style: RowStyle,
) {
let mut series = Series {
status: series_status,
..Series::default()
};
if let Some(percentage) = percent_of_episodes {
series.statistics = Some(SeriesStatistics {
percent_of_episodes: percentage,
..SeriesStatistics::default()
});
}
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
match expected_row_style {
RowStyle::Downloaded => assert_eq!(style, row.downloaded()),
RowStyle::Missing => assert_eq!(style, row.missing()),
RowStyle::Unreleased => assert_eq!(style, row.unreleased()),
}
}
enum RowStyle {
Downloaded,
Missing,
Unreleased,
}
}
+179
View File
@@ -0,0 +1,179 @@
use ratatui::{
layout::{Constraint, Rect},
widgets::{Cell, Row},
Frame,
};
use crate::{
app::App,
models::{
servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SERIES_BLOCKS},
sonarr_models::{Series, SeriesStatus},
EnumDisplayStyle, Route,
},
ui::{
styles::ManagarrStyle,
utils::{get_width_from_percentage, layout_block_top_border},
widgets::managarr_table::ManagarrTable,
DrawUi,
},
utils::convert_runtime,
};
#[cfg(test)]
#[path = "library_ui_tests.rs"]
mod library_ui_tests;
pub(super) struct LibraryUi;
impl DrawUi for LibraryUi {
fn accepts(route: Route) -> bool {
if let Route::Sonarr(active_sonarr_block, _) = route {
return SERIES_BLOCKS.contains(&active_sonarr_block);
}
false
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let route = app.get_current_route();
let mut series_ui_matchers = |active_sonarr_block: ActiveSonarrBlock| match active_sonarr_block
{
ActiveSonarrBlock::Series | ActiveSonarrBlock::SeriesSortPrompt => draw_series(f, app, area),
_ => (),
};
match route {
Route::Sonarr(active_sonarr_block, _) if SERIES_BLOCKS.contains(&active_sonarr_block) => {
series_ui_matchers(active_sonarr_block)
}
_ => (),
}
}
}
pub(super) fn draw_series(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
let current_selection = if !app.data.sonarr_data.series.items.is_empty() {
app.data.sonarr_data.series.current_selection().clone()
} else {
Series::default()
};
let quality_profile_map = &app.data.sonarr_data.quality_profile_map;
let language_profile_map = &app.data.sonarr_data.language_profiles_map;
let tags_map = &app.data.sonarr_data.tags_map;
let content = Some(&mut app.data.sonarr_data.series);
let help_footer = app
.data
.sonarr_data
.main_tabs
.get_active_tab_contextual_help();
let series_table_row_mapping = |series: &Series| {
series.title.scroll_left_or_reset(
get_width_from_percentage(area, 27),
*series == current_selection,
app.tick_count % app.ticks_until_scroll == 0,
);
let monitored = if series.monitored { "🏷" } else { "" };
let (hours, minutes) = convert_runtime(series.runtime);
let certification = series.certification.clone().unwrap_or_default();
let network = series.network.clone().unwrap_or_default();
let quality_profile = quality_profile_map
.get_by_left(&series.quality_profile_id)
.unwrap()
.to_owned();
let language_profile = language_profile_map
.get_by_left(&series.language_profile_id)
.unwrap()
.to_owned();
let tags = if !series.tags.is_empty() {
series
.tags
.iter()
.map(|tag_id| {
tags_map
.get_by_left(&tag_id.as_i64().unwrap())
.unwrap()
.clone()
})
.collect::<Vec<String>>()
.join(", ")
} else {
String::new()
};
decorate_series_row_with_style(
series,
Row::new(vec![
Cell::from(series.title.to_string()),
Cell::from(series.year.to_string()),
Cell::from(network),
Cell::from(format!("{hours}h {minutes}m")),
Cell::from(certification),
Cell::from(series.series_type.to_display_str()),
Cell::from(quality_profile),
Cell::from(language_profile),
Cell::from(monitored.to_owned()),
Cell::from(tags),
]),
)
};
let series_table = ManagarrTable::new(content, series_table_row_mapping)
.block(layout_block_top_border())
.loading(app.is_loading)
.footer(help_footer)
.sorting(active_sonarr_block == ActiveSonarrBlock::SeriesSortPrompt)
.headers([
"Title",
"Year",
"Network",
"Runtime",
"Rating",
"Type",
"Quality Profile",
"Language Profile",
"Monitored",
"Tags",
])
.constraints([
Constraint::Percentage(27),
Constraint::Percentage(4),
Constraint::Percentage(10),
Constraint::Percentage(6),
Constraint::Percentage(6),
Constraint::Percentage(6),
Constraint::Percentage(13),
Constraint::Percentage(10),
Constraint::Percentage(6),
Constraint::Percentage(12),
]);
f.render_widget(series_table, area);
}
}
fn decorate_series_row_with_style<'a>(series: &Series, row: Row<'a>) -> Row<'a> {
match series.status {
SeriesStatus::Ended => {
if let Some(ref stats) = series.statistics {
if stats.percent_of_episodes == 100.0 {
return row.downloaded();
}
}
row.missing()
}
SeriesStatus::Continuing => {
if let Some(ref stats) = series.statistics {
if stats.percent_of_episodes == 100.0 {
return row.unreleased();
}
}
row.missing()
}
SeriesStatus::Upcoming => row.unreleased(),
_ => row.missing(),
}
}
+211
View File
@@ -0,0 +1,211 @@
use std::{cmp, iter};
use chrono::{Duration, Utc};
use library::LibraryUi;
use log::debug;
use ratatui::{
layout::{Constraint, Layout, Rect},
style::Stylize,
text::Text,
widgets::Paragraph,
Frame,
};
use crate::{
app::App,
logos::SONARR_LOGO,
models::{
servarr_data::sonarr::sonarr_data::SonarrData,
servarr_models::{DiskSpace, RootFolder},
sonarr_models::DownloadRecord,
Route,
},
utils::convert_to_gb,
};
use super::{
draw_tabs,
styles::ManagarrStyle,
utils::{
borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block,
},
widgets::loading_block::LoadingBlock,
DrawUi,
};
mod library;
#[cfg(test)]
#[path = "sonarr_ui_tests.rs"]
mod sonarr_ui_tests;
pub(super) struct SonarrUi;
impl DrawUi for SonarrUi {
fn accepts(route: Route) -> bool {
matches!(route, Route::Sonarr(_, _))
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let content_area = draw_tabs(f, area, "Series", &app.data.sonarr_data.main_tabs);
let route = app.get_current_route();
match route {
_ if LibraryUi::accepts(route) => LibraryUi::draw(f, app, content_area),
_ => (),
}
}
fn draw_context_row(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
let [main_area, logo_area] =
Layout::horizontal([Constraint::Fill(0), Constraint::Length(20)]).areas(area);
let [stats_area, downloads_area] =
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(main_area);
draw_stats_context(f, app, stats_area);
draw_downloads_context(f, app, downloads_area);
draw_sonarr_logo(f, logo_area);
}
}
fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
let block = title_block("Stats");
if !app.data.sonarr_data.version.is_empty() {
f.render_widget(block, area);
let SonarrData {
root_folders,
disk_space_vec,
start_time,
..
} = &app.data.sonarr_data;
let mut constraints = vec![
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
];
constraints.append(
&mut iter::repeat(Constraint::Length(1))
.take(disk_space_vec.len() + root_folders.items.len() + 1)
.collect(),
);
let stat_item_areas = Layout::vertical(constraints).margin(1).split(area);
let version_paragraph = Paragraph::new(Text::from(format!(
"Sonarr Version: {}",
app.data.sonarr_data.version
)))
.block(borderless_block())
.bold();
let uptime = Utc::now() - start_time.to_owned();
let days = uptime.num_days();
let day_difference = uptime - Duration::days(days);
let hours = day_difference.num_hours();
let hour_difference = day_difference - Duration::hours(hours);
let minutes = hour_difference.num_minutes();
let seconds = (hour_difference - Duration::minutes(minutes)).num_seconds();
let uptime_paragraph = Paragraph::new(Text::from(format!(
"Uptime: {days}d {hours:0width$}:{minutes:0width$}:{seconds:0width$}",
width = 2
)))
.block(borderless_block())
.bold();
let storage = Paragraph::new(Text::from("Storage:")).block(borderless_block().bold());
let folders = Paragraph::new(Text::from("Root Folders:")).block(borderless_block().bold());
f.render_widget(version_paragraph, stat_item_areas[0]);
f.render_widget(uptime_paragraph, stat_item_areas[1]);
f.render_widget(storage, stat_item_areas[2]);
for i in 0..disk_space_vec.len() {
let DiskSpace {
free_space,
total_space,
} = &disk_space_vec[i];
let title = format!("Disk {}", i + 1);
let ratio = if *total_space == 0 {
0f64
} else {
1f64 - (*free_space as f64 / *total_space as f64)
};
let space_gauge = line_gauge_with_label(title.as_str(), ratio);
f.render_widget(space_gauge, stat_item_areas[i + 3]);
}
f.render_widget(folders, stat_item_areas[disk_space_vec.len() + 3]);
for i in 0..root_folders.items.len() {
let RootFolder {
path, free_space, ..
} = &root_folders.items[i];
let space: f64 = convert_to_gb(*free_space);
let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free"))
.block(borderless_block())
.default();
f.render_widget(
root_folder_space,
stat_item_areas[i + disk_space_vec.len() + 4],
)
}
} else {
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
}
}
fn draw_downloads_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
let block = title_block("Downloads");
let downloads_vec = &app.data.sonarr_data.downloads.items;
if !downloads_vec.is_empty() {
f.render_widget(block, area);
let max_items = (((area.height as f64 / 2.0).floor() * 2.0) as usize) / 2;
let items = cmp::min(downloads_vec.len(), max_items - 1);
debug!("Items: {items}");
let download_item_areas = Layout::vertical(
iter::repeat(Constraint::Length(2))
.take(items)
.collect::<Vec<Constraint>>(),
)
.margin(1)
.split(area);
for i in 0..items {
let DownloadRecord {
title,
sizeleft,
size,
..
} = &downloads_vec[i];
let percent = if *size == 0.0 {
0.0
} else {
1f64 - (*sizeleft / *size)
};
let download_gauge = line_gauge_with_title(title, percent);
f.render_widget(download_gauge, download_item_areas[i]);
}
} else {
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
}
}
fn draw_sonarr_logo(f: &mut Frame<'_>, area: Rect) {
let logo_text = Text::from(SONARR_LOGO);
let logo = Paragraph::new(logo_text)
.light_cyan()
.block(layout_block().default())
.centered();
f.render_widget(logo, area);
}
+16
View File
@@ -0,0 +1,16 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::{
models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock,
ui::{sonarr_ui::SonarrUi, DrawUi},
};
#[test]
fn test_sonarr_ui_accepts() {
ActiveSonarrBlock::iter().for_each(|active_sonarr_block| {
assert!(SonarrUi::accepts(active_sonarr_block.into()));
});
}
}