Files
managarr/src/ui/readarr_ui/mod.rs
T
Dark-Alex-17 f2ee1e71f6 feat: show sizes under a gigabyte in megabytes
Every size cell converted bytes to gigabytes and formatted the float in
place, so anything below a gigabyte rendered as 0.00 GB. Books and
tracks are almost always that small, which made the Size columns in
Readarr and Lidarr useless.

Replace convert_to_gb and convert_f64_to_gb with one format_size helper
that renders gigabytes at or above 1 GiB and megabytes below it, at the
precision each site already used. Every render site across the four
servarr trees now calls it, including the two movie and episode detail
strings built on the network side. Values at or above a gigabyte render
byte for byte as before.

Give the Readarr book statistics and book file fixtures and the Lidarr
album statistics fixture realistic sub-gigabyte sizes so the megabyte
path is pinned by the committed snapshots.
2026-09-18 21:44:22 -06:00

237 lines
7.1 KiB
Rust

#[cfg(test)]
use crate::ui::ui_test_utils::test_utils::Utc;
use chrono::Duration;
#[cfg(not(test))]
use chrono::Utc;
use history::HistoryUi;
use library::LibraryUi;
use ratatui::{
Frame,
layout::{Constraint, Layout, Rect},
style::Stylize,
text::Text,
widgets::Paragraph,
};
use std::{cmp, iter};
use super::{
DrawUi, draw_tabs,
styles::ManagarrStyle,
utils::{
borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block,
},
widgets::loading_block::LoadingBlock,
};
use crate::ui::readarr_ui::blocklist::BlocklistUi;
use crate::ui::readarr_ui::downloads::DownloadsUi;
use crate::ui::readarr_ui::indexers::IndexersUi;
use crate::ui::readarr_ui::root_folders::RootFoldersUi;
use crate::ui::readarr_ui::system::SystemUi;
use crate::ui::utils::{extract_monitored_disk_space_vec, extract_monitored_root_folders};
use crate::{
app::App,
logos::READARR_LOGO,
models::{
Route,
readarr_models::DownloadRecord,
servarr_data::readarr::readarr_data::ReadarrData,
servarr_models::{DiskSpace, RootFolder},
},
utils::format_size,
};
mod blocklist;
mod downloads;
mod history;
mod indexers;
mod library;
mod readarr_ui_utils;
mod root_folders;
mod system;
#[cfg(test)]
#[path = "readarr_ui_tests.rs"]
mod readarr_ui_tests;
pub(super) struct ReadarrUi;
impl DrawUi for ReadarrUi {
fn accepts(route: Route) -> bool {
matches!(route, Route::Readarr(_, _))
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let content_area = draw_tabs(f, area, "Authors", &app.data.readarr_data.main_tabs);
let route = app.get_current_route();
match route {
_ if LibraryUi::accepts(route) => LibraryUi::draw(f, app, content_area),
_ if DownloadsUi::accepts(route) => DownloadsUi::draw(f, app, content_area),
_ if BlocklistUi::accepts(route) => BlocklistUi::draw(f, app, content_area),
_ if HistoryUi::accepts(route) => HistoryUi::draw(f, app, content_area),
_ if RootFoldersUi::accepts(route) => RootFoldersUi::draw(f, app, content_area),
_ if IndexersUi::accepts(route) => IndexersUi::draw(f, app, content_area),
_ if SystemUi::accepts(route) => SystemUi::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_readarr_logo(f, logo_area);
}
}
fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
let block = title_block("Stats");
if !app.data.readarr_data.version.is_empty() {
f.render_widget(block, area);
let ReadarrData {
root_folders,
disk_space_vec,
start_time,
..
} = &app.data.readarr_data;
let monitored_disk_space_vec = extract_monitored_disk_space_vec(app, disk_space_vec.clone());
let monitored_root_folders = extract_monitored_root_folders(app, root_folders.items.clone());
let mut constraints = vec![
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
];
constraints.append(
&mut iter::repeat_n(
Constraint::Length(1),
monitored_disk_space_vec.len() + monitored_root_folders.len() + 1,
)
.collect(),
);
let stat_item_areas = Layout::vertical(constraints).margin(1).split(area);
let version_paragraph = Paragraph::new(Text::from(format!(
"Readarr Version: {}",
app.data.readarr_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..monitored_disk_space_vec.len() {
let DiskSpace {
path,
free_space,
total_space,
} = &monitored_disk_space_vec[i];
let title = if let Some(path) = path {
path
} else {
&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[monitored_disk_space_vec.len() + 3]);
for i in 0..monitored_root_folders.len() {
let RootFolder {
path, free_space, ..
} = &monitored_root_folders[i];
let space = format_size(*free_space, 2);
let root_folder_space = Paragraph::new(format!("{path}: {space} free"))
.block(borderless_block())
.default_color();
f.render_widget(
root_folder_space,
stat_item_areas[i + monitored_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.readarr_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 i64) / 2) - 1;
let items = cmp::min(downloads_vec.len(), max_items.unsigned_abs() as usize);
let download_item_areas =
Layout::vertical(iter::repeat_n(Constraint::Length(2), 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_readarr_logo(f: &mut Frame<'_>, area: Rect) {
let logo_text = Text::from(READARR_LOGO);
let logo = Paragraph::new(logo_text)
.light_magenta()
.block(layout_block().default_color())
.centered();
f.render_widget(logo, area);
}