feat(ui): Downloads tab support
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user