feat(ui): Downloads tab support
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
use downloads::DownloadsHandler;
|
||||||
use library::LibraryHandler;
|
use library::LibraryHandler;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -32,6 +33,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SonarrHandler<'a, 'b
|
|||||||
_ if LibraryHandler::accepts(self.active_sonarr_block) => {
|
_ if LibraryHandler::accepts(self.active_sonarr_block) => {
|
||||||
LibraryHandler::with(self.key, self.app, self.active_sonarr_block, self.context).handle();
|
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(),
|
_ => self.handle_key_event(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,36 +48,28 @@ mod tests {
|
|||||||
#[rstest]
|
#[rstest]
|
||||||
fn test_delegates_library_blocks_to_library_handler(
|
fn test_delegates_library_blocks_to_library_handler(
|
||||||
#[values(
|
#[values(
|
||||||
// ActiveSonarrBlock::AddSeriesAlreadyInLibrary,
|
ActiveSonarrBlock::AddSeriesAlreadyInLibrary,
|
||||||
// ActiveSonarrBlock::AddSeriesConfirmPrompt,
|
ActiveSonarrBlock::AddSeriesEmptySearchResults,
|
||||||
// ActiveSonarrBlock::AddSeriesEmptySearchResults,
|
ActiveSonarrBlock::AddSeriesPrompt,
|
||||||
// ActiveSonarrBlock::AddSeriesPrompt,
|
ActiveSonarrBlock::AddSeriesSearchInput,
|
||||||
// ActiveSonarrBlock::AddSeriesSearchInput,
|
ActiveSonarrBlock::AddSeriesSearchResults,
|
||||||
// ActiveSonarrBlock::AddSeriesSearchResults,
|
ActiveSonarrBlock::AddSeriesSelectLanguageProfile,
|
||||||
// ActiveSonarrBlock::AddSeriesSelectLanguageProfile,
|
ActiveSonarrBlock::AddSeriesSelectMonitor,
|
||||||
// ActiveSonarrBlock::AddSeriesSelectMonitor,
|
ActiveSonarrBlock::AddSeriesSelectQualityProfile,
|
||||||
// ActiveSonarrBlock::AddSeriesSelectQualityProfile,
|
ActiveSonarrBlock::AddSeriesSelectRootFolder,
|
||||||
// ActiveSonarrBlock::AddSeriesSelectRootFolder,
|
ActiveSonarrBlock::AddSeriesSelectSeriesType,
|
||||||
// ActiveSonarrBlock::AddSeriesSelectSeriesType,
|
ActiveSonarrBlock::AddSeriesTagsInput,
|
||||||
// ActiveSonarrBlock::AddSeriesTagsInput,
|
|
||||||
// ActiveSonarrBlock::AddSeriesToggleUseSeasonFolder,
|
|
||||||
// ActiveSonarrBlock::AutomaticallySearchEpisodePrompt,
|
// ActiveSonarrBlock::AutomaticallySearchEpisodePrompt,
|
||||||
// ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
|
// ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
|
||||||
// ActiveSonarrBlock::AutomaticallySearchSeriesPrompt,
|
// ActiveSonarrBlock::AutomaticallySearchSeriesPrompt,
|
||||||
// ActiveSonarrBlock::DeleteEpisodeFilePrompt,
|
// ActiveSonarrBlock::DeleteEpisodeFilePrompt,
|
||||||
// ActiveSonarrBlock::DeleteSeriesConfirmPrompt,
|
ActiveSonarrBlock::DeleteSeriesPrompt,
|
||||||
// ActiveSonarrBlock::DeleteSeriesPrompt,
|
ActiveSonarrBlock::EditSeriesPrompt,
|
||||||
// ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion,
|
ActiveSonarrBlock::EditSeriesPathInput,
|
||||||
// ActiveSonarrBlock::DeleteSeriesToggleDeleteFile,
|
ActiveSonarrBlock::EditSeriesSelectSeriesType,
|
||||||
// ActiveSonarrBlock::EditSeriesPrompt,
|
ActiveSonarrBlock::EditSeriesSelectQualityProfile,
|
||||||
// ActiveSonarrBlock::EditSeriesConfirmPrompt,
|
ActiveSonarrBlock::EditSeriesSelectLanguageProfile,
|
||||||
// ActiveSonarrBlock::EditSeriesPathInput,
|
ActiveSonarrBlock::EditSeriesTagsInput,
|
||||||
// ActiveSonarrBlock::EditSeriesSelectSeriesType,
|
|
||||||
// ActiveSonarrBlock::EditSeriesSelectQualityProfile,
|
|
||||||
// ActiveSonarrBlock::EditSeriesSelectLanguageProfile,
|
|
||||||
// ActiveSonarrBlock::EditSeriesTagsInput,
|
|
||||||
// ActiveSonarrBlock::EditSeriesToggleMonitored,
|
|
||||||
// ActiveSonarrBlock::EditSeriesToggleSeasonFolder,
|
|
||||||
// ActiveSonarrBlock::EpisodeDetails,
|
// ActiveSonarrBlock::EpisodeDetails,
|
||||||
// ActiveSonarrBlock::EpisodeFile,
|
// ActiveSonarrBlock::EpisodeFile,
|
||||||
// ActiveSonarrBlock::EpisodeHistory,
|
// ActiveSonarrBlock::EpisodeHistory,
|
||||||
@@ -119,4 +111,20 @@ mod tests {
|
|||||||
active_sonarr_block
|
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 std::{cmp, iter};
|
||||||
|
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
|
use downloads::DownloadsUi;
|
||||||
use library::LibraryUi;
|
use library::LibraryUi;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Layout, Rect},
|
layout::{Constraint, Layout, Rect},
|
||||||
@@ -32,6 +33,7 @@ use super::{
|
|||||||
DrawUi,
|
DrawUi,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod downloads;
|
||||||
mod library;
|
mod library;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -51,6 +53,7 @@ impl DrawUi for SonarrUi {
|
|||||||
|
|
||||||
match route {
|
match route {
|
||||||
_ if LibraryUi::accepts(route) => LibraryUi::draw(f, app, content_area),
|
_ 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)
|
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) {
|
pub fn convert_runtime(runtime: i64) -> (i64, i64) {
|
||||||
let hours = runtime / 60;
|
let hours = runtime / 60;
|
||||||
let minutes = runtime % 60;
|
let minutes = runtime % 60;
|
||||||
|
|||||||
+7
-1
@@ -2,7 +2,7 @@
|
|||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::assert_eq;
|
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]
|
#[test]
|
||||||
fn test_convert_to_gb() {
|
fn test_convert_to_gb() {
|
||||||
@@ -10,6 +10,12 @@ mod tests {
|
|||||||
assert_eq!(convert_to_gb(2662879723), 2.4799999995157123);
|
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]
|
#[test]
|
||||||
fn test_convert_runtime() {
|
fn test_convert_runtime() {
|
||||||
let (hours, minutes) = convert_runtime(154);
|
let (hours, minutes) = convert_runtime(154);
|
||||||
|
|||||||
Reference in New Issue
Block a user