feat(ui): Downloads tab support

This commit is contained in:
2024-12-02 15:57:48 -07:00
parent f338dfcb12
commit f0d8555a8a
7 changed files with 213 additions and 27 deletions
+4
View File
@@ -1,3 +1,4 @@
use downloads::DownloadsHandler;
use library::LibraryHandler;
use crate::{
@@ -32,6 +33,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SonarrHandler<'a, 'b
_ if LibraryHandler::accepts(self.active_sonarr_block) => {
LibraryHandler::with(self.key, self.app, self.active_sonarr_block, self.context).handle();
}
_ if DownloadsHandler::accepts(self.active_sonarr_block) => {
DownloadsHandler::with(self.key, self.app, self.active_sonarr_block, self.context).handle()
}
_ => self.handle_key_event(),
}
}
@@ -48,36 +48,28 @@ mod tests {
#[rstest]
fn test_delegates_library_blocks_to_library_handler(
#[values(
// ActiveSonarrBlock::AddSeriesAlreadyInLibrary,
// ActiveSonarrBlock::AddSeriesConfirmPrompt,
// ActiveSonarrBlock::AddSeriesEmptySearchResults,
// ActiveSonarrBlock::AddSeriesPrompt,
// ActiveSonarrBlock::AddSeriesSearchInput,
// ActiveSonarrBlock::AddSeriesSearchResults,
// ActiveSonarrBlock::AddSeriesSelectLanguageProfile,
// ActiveSonarrBlock::AddSeriesSelectMonitor,
// ActiveSonarrBlock::AddSeriesSelectQualityProfile,
// ActiveSonarrBlock::AddSeriesSelectRootFolder,
// ActiveSonarrBlock::AddSeriesSelectSeriesType,
// ActiveSonarrBlock::AddSeriesTagsInput,
// ActiveSonarrBlock::AddSeriesToggleUseSeasonFolder,
ActiveSonarrBlock::AddSeriesAlreadyInLibrary,
ActiveSonarrBlock::AddSeriesEmptySearchResults,
ActiveSonarrBlock::AddSeriesPrompt,
ActiveSonarrBlock::AddSeriesSearchInput,
ActiveSonarrBlock::AddSeriesSearchResults,
ActiveSonarrBlock::AddSeriesSelectLanguageProfile,
ActiveSonarrBlock::AddSeriesSelectMonitor,
ActiveSonarrBlock::AddSeriesSelectQualityProfile,
ActiveSonarrBlock::AddSeriesSelectRootFolder,
ActiveSonarrBlock::AddSeriesSelectSeriesType,
ActiveSonarrBlock::AddSeriesTagsInput,
// ActiveSonarrBlock::AutomaticallySearchEpisodePrompt,
// ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
// ActiveSonarrBlock::AutomaticallySearchSeriesPrompt,
// ActiveSonarrBlock::DeleteEpisodeFilePrompt,
// ActiveSonarrBlock::DeleteSeriesConfirmPrompt,
// ActiveSonarrBlock::DeleteSeriesPrompt,
// ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion,
// ActiveSonarrBlock::DeleteSeriesToggleDeleteFile,
// ActiveSonarrBlock::EditSeriesPrompt,
// ActiveSonarrBlock::EditSeriesConfirmPrompt,
// ActiveSonarrBlock::EditSeriesPathInput,
// ActiveSonarrBlock::EditSeriesSelectSeriesType,
// ActiveSonarrBlock::EditSeriesSelectQualityProfile,
// ActiveSonarrBlock::EditSeriesSelectLanguageProfile,
// ActiveSonarrBlock::EditSeriesTagsInput,
// ActiveSonarrBlock::EditSeriesToggleMonitored,
// ActiveSonarrBlock::EditSeriesToggleSeasonFolder,
ActiveSonarrBlock::DeleteSeriesPrompt,
ActiveSonarrBlock::EditSeriesPrompt,
ActiveSonarrBlock::EditSeriesPathInput,
ActiveSonarrBlock::EditSeriesSelectSeriesType,
ActiveSonarrBlock::EditSeriesSelectQualityProfile,
ActiveSonarrBlock::EditSeriesSelectLanguageProfile,
ActiveSonarrBlock::EditSeriesTagsInput,
// ActiveSonarrBlock::EpisodeDetails,
// ActiveSonarrBlock::EpisodeFile,
// ActiveSonarrBlock::EpisodeHistory,
@@ -119,4 +111,20 @@ mod tests {
active_sonarr_block
);
}
#[rstest]
fn test_delegates_downloads_blocks_to_downloads_handler(
#[values(
ActiveSonarrBlock::Downloads,
ActiveSonarrBlock::DeleteDownloadPrompt,
ActiveSonarrBlock::UpdateDownloadsPrompt
)]
active_sonarr_block: ActiveSonarrBlock,
) {
test_handler_delegation!(
SonarrHandler,
ActiveSonarrBlock::Downloads,
active_sonarr_block
);
}
}
@@ -0,0 +1,19 @@
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS};
use crate::ui::sonarr_ui::downloads::DownloadsUi;
use crate::ui::DrawUi;
#[test]
fn test_downloads_ui_accepts() {
ActiveSonarrBlock::iter().for_each(|active_sonarr_block| {
if DOWNLOADS_BLOCKS.contains(&active_sonarr_block) {
assert!(DownloadsUi::accepts(active_sonarr_block.into()));
} else {
assert!(!DownloadsUi::accepts(active_sonarr_block.into()));
}
});
}
}
+142
View File
@@ -0,0 +1,142 @@
use ratatui::layout::{Constraint, Rect};
use ratatui::widgets::{Cell, Row};
use ratatui::Frame;
use crate::app::App;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS};
use crate::models::sonarr_models::DownloadRecord;
use crate::models::{HorizontallyScrollableText, Route};
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::DrawUi;
use crate::utils::convert_f64_to_gb;
#[cfg(test)]
#[path = "downloads_ui_tests.rs"]
mod downloads_ui_tests;
pub(super) struct DownloadsUi;
impl DrawUi for DownloadsUi {
fn accepts(route: Route) -> bool {
if let Route::Sonarr(active_sonarr_block, _) = route {
return DOWNLOADS_BLOCKS.contains(&active_sonarr_block);
}
false
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
match active_sonarr_block {
ActiveSonarrBlock::Downloads => draw_downloads(f, app, area),
ActiveSonarrBlock::DeleteDownloadPrompt => {
let prompt = format!(
"Do you really want to delete this download: \n{}?",
app.data.sonarr_data.downloads.current_selection().title
);
let confirmation_prompt = ConfirmationPrompt::new()
.title("Cancel Download")
.prompt(&prompt)
.yes_no_value(app.data.sonarr_data.prompt_confirm);
draw_downloads(f, app, area);
f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area());
}
ActiveSonarrBlock::UpdateDownloadsPrompt => {
let confirmation_prompt = ConfirmationPrompt::new()
.title("Update Downloads")
.prompt("Do you want to update your downloads?")
.yes_no_value(app.data.sonarr_data.prompt_confirm);
draw_downloads(f, app, area);
f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area());
}
_ => (),
}
}
}
}
fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let current_selection = if app.data.sonarr_data.downloads.items.is_empty() {
DownloadRecord::default()
} else {
app.data.sonarr_data.downloads.current_selection().clone()
};
let downloads_table_footer = app
.data
.sonarr_data
.main_tabs
.get_active_tab_contextual_help();
let downloads_row_mapping = |download_record: &DownloadRecord| {
let DownloadRecord {
title,
size,
sizeleft,
download_client,
indexer,
output_path,
..
} = download_record;
if output_path.is_some() {
output_path.as_ref().unwrap().scroll_left_or_reset(
get_width_from_percentage(area, 18),
current_selection == *download_record,
app.tick_count % app.ticks_until_scroll == 0,
);
}
let percent = if *size == 0.0 {
0.0
} else {
1f64 - (*sizeleft / *size)
};
let file_size: f64 = convert_f64_to_gb(*size);
Row::new(vec![
Cell::from(title.to_owned()),
Cell::from(format!("{:.0}%", percent * 100.0)),
Cell::from(format!("{file_size:.2} GB")),
Cell::from(
output_path
.as_ref()
.unwrap_or(&HorizontallyScrollableText::default())
.to_string(),
),
Cell::from(indexer.to_owned()),
Cell::from(download_client.to_owned()),
])
.primary()
};
let downloads_table = ManagarrTable::new(
Some(&mut app.data.sonarr_data.downloads),
downloads_row_mapping,
)
.block(layout_block_top_border())
.loading(app.is_loading)
.footer(downloads_table_footer)
.headers([
"Title",
"Percent Complete",
"Size",
"Output Path",
"Indexer",
"Download Client",
])
.constraints([
Constraint::Percentage(30),
Constraint::Percentage(11),
Constraint::Percentage(11),
Constraint::Percentage(18),
Constraint::Percentage(17),
Constraint::Percentage(13),
]);
f.render_widget(downloads_table, area);
}
+3
View File
@@ -1,6 +1,7 @@
use std::{cmp, iter};
use chrono::{Duration, Utc};
use downloads::DownloadsUi;
use library::LibraryUi;
use ratatui::{
layout::{Constraint, Layout, Rect},
@@ -32,6 +33,7 @@ use super::{
DrawUi,
};
mod downloads;
mod library;
#[cfg(test)]
@@ -51,6 +53,7 @@ impl DrawUi for SonarrUi {
match route {
_ if LibraryUi::accepts(route) => LibraryUi::draw(f, app, content_area),
_ if DownloadsUi::accepts(route) => DownloadsUi::draw(f, app, content_area),
_ => (),
}
}
+4
View File
@@ -67,6 +67,10 @@ pub fn convert_to_gb(bytes: i64) -> f64 {
bytes as f64 / 1024f64.powi(3)
}
pub fn convert_f64_to_gb(bytes: f64) -> f64 {
bytes / 1024f64.powi(3)
}
pub fn convert_runtime(runtime: i64) -> (i64, i64) {
let hours = runtime / 60;
let minutes = runtime % 60;
+7 -1
View File
@@ -2,7 +2,7 @@
mod tests {
use pretty_assertions::assert_eq;
use crate::utils::{convert_runtime, convert_to_gb};
use crate::utils::{convert_f64_to_gb, convert_runtime, convert_to_gb};
#[test]
fn test_convert_to_gb() {
@@ -10,6 +10,12 @@ mod tests {
assert_eq!(convert_to_gb(2662879723), 2.4799999995157123);
}
#[test]
fn test_convert_f64_to_gb() {
assert_eq!(convert_f64_to_gb(2147483648f64), 2f64);
assert_eq!(convert_f64_to_gb(2662879723f64), 2.4799999995157123);
}
#[test]
fn test_convert_runtime() {
let (hours, minutes) = convert_runtime(154);