Compare commits

...

5 Commits

Author SHA1 Message Date
c8a06f3601 refactored managarr table initializer so a mutable app reference can be passed 2026-01-05 09:49:03 -07:00
50d4ddfa28 Merge branch 'refs/heads/main' into tachyonfx
# Conflicts:
#	src/app/mod.rs
#	src/main.rs
2025-12-19 13:45:35 -07:00
ea73f1d3d4 testing 2025-12-19 13:09:26 -07:00
d69056e5d6 feat: Further improved responsiveness and speed of the TUI 2025-12-19 12:28:33 -07:00
051f30b097 feat: Improved overall UI responsiveness 2025-12-18 16:19:03 -07:00
37 changed files with 1113 additions and 766 deletions
Generated
+120 -8
View File
@@ -41,6 +41,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anpa"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d032745fe46100dbcb28ee6e30f12c4b148786f8889e07cd0a3445eeb54970f"
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.21" version = "0.6.21"
@@ -217,6 +223,31 @@ dependencies = [
"objc2", "objc2",
] ]
[[package]]
name = "bon"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1"
dependencies = [
"bon-macros",
"rustversion",
]
[[package]]
name = "bon-macros"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645"
dependencies = [
"darling 0.21.3",
"ident_case",
"prettyplease",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.111",
]
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "1.12.1" version = "1.12.1"
@@ -376,6 +407,20 @@ dependencies = [
"static_assertions", "static_assertions",
] ]
[[package]]
name = "compact_str"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]] [[package]]
name = "confy" name = "confy"
version = "0.6.1" version = "0.6.1"
@@ -459,8 +504,18 @@ version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.11",
"darling_macro", "darling_macro 0.20.11",
]
[[package]]
name = "darling"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core 0.21.3",
"darling_macro 0.21.3",
] ]
[[package]] [[package]]
@@ -477,13 +532,38 @@ dependencies = [
"syn 2.0.111", "syn 2.0.111",
] ]
[[package]]
name = "darling_core"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.111",
]
[[package]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.20.11" version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.11",
"quote",
"syn 2.0.111",
]
[[package]]
name = "darling_macro"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core 0.21.3",
"quote", "quote",
"syn 2.0.111", "syn 2.0.111",
] ]
@@ -536,7 +616,7 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9" checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9"
dependencies = [ dependencies = [
"darling", "darling 0.20.11",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.111", "syn 2.0.111",
@@ -664,7 +744,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583f1f514d2754010ff71ed6853068cacbe43cc142cc076aa1b871d9754efc48" checksum = "583f1f514d2754010ff71ed6853068cacbe43cc142cc076aa1b871d9754efc48"
dependencies = [ dependencies = [
"darling", "darling 0.20.11",
"quote", "quote",
"syn 2.0.111", "syn 2.0.111",
] ]
@@ -673,7 +753,7 @@ dependencies = [
name = "enum_display_style_derive" name = "enum_display_style_derive"
version = "0.6.1" version = "0.6.1"
dependencies = [ dependencies = [
"darling", "darling 0.20.11",
"quote", "quote",
"syn 2.0.111", "syn 2.0.111",
] ]
@@ -1249,7 +1329,7 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c"
dependencies = [ dependencies = [
"darling", "darling 0.20.11",
"indoc", "indoc",
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1454,6 +1534,7 @@ dependencies = [
"serial_test", "serial_test",
"strum", "strum",
"strum_macros", "strum_macros",
"tachyonfx",
"tokio", "tokio",
"tokio-util", "tokio-util",
"urlencoding", "urlencoding",
@@ -1477,6 +1558,12 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "micromath"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815"
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@@ -2005,6 +2092,16 @@ dependencies = [
"yansi", "yansi",
] ]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.111",
]
[[package]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "3.4.0" version = "3.4.0"
@@ -2109,7 +2206,7 @@ checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cassowary", "cassowary",
"compact_str", "compact_str 0.8.1",
"crossterm", "crossterm",
"indoc", "indoc",
"instability", "instability",
@@ -2704,6 +2801,21 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "tachyonfx"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de9d53c5afe979f7de9d5ba856c9829b118d6aca6335750201f1ada46bc198e2"
dependencies = [
"anpa",
"bon",
"compact_str 0.9.0",
"micromath",
"ratatui",
"thiserror 2.0.17",
"unicode-width 0.2.0",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.23.0" version = "3.23.0"
+1
View File
@@ -67,6 +67,7 @@ openssl = { version = "0.10.70", features = ["vendored"] }
veil = "0.2.0" veil = "0.2.0"
validate_theme_derive = "0.1.0" validate_theme_derive = "0.1.0"
enum_display_style_derive = "0.1.0" enum_display_style_derive = "0.1.0"
tachyonfx = { version = "0.21.0", features = ["sendable"] }
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0.16" assert_cmd = "2.0.16"
+9 -1
View File
@@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::{fs, process}; use std::{fs, process};
use tachyonfx::{Duration, EffectManager};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use veil::Redact; use veil::Redact;
@@ -40,6 +41,9 @@ pub struct App<'a> {
pub ticks_until_scroll: u64, pub ticks_until_scroll: u64,
pub tick_count: u64, pub tick_count: u64,
pub ui_scroll_tick_count: u64, pub ui_scroll_tick_count: u64,
pub last_tick: Duration,
pub effects: EffectManager<()>,
pub has_active_effect: bool,
pub is_routing: bool, pub is_routing: bool,
pub is_loading: bool, pub is_loading: bool,
pub should_refresh: bool, pub should_refresh: bool,
@@ -189,6 +193,7 @@ impl App<'_> {
pub fn push_navigation_stack(&mut self, route: Route) { pub fn push_navigation_stack(&mut self, route: Route) {
self.navigation_stack.push(route); self.navigation_stack.push(route);
self.is_routing = true; self.is_routing = true;
self.has_active_effect = false;
} }
pub fn pop_navigation_stack(&mut self) { pub fn pop_navigation_stack(&mut self) {
@@ -237,6 +242,9 @@ impl Default for App<'_> {
ticks_until_scroll: 4, ticks_until_scroll: 4,
tick_count: 0, tick_count: 0,
ui_scroll_tick_count: 0, ui_scroll_tick_count: 0,
last_tick: Duration::ZERO,
effects: EffectManager::default(),
has_active_effect: false,
is_loading: false, is_loading: false,
is_routing: false, is_routing: false,
should_refresh: false, should_refresh: false,
@@ -265,10 +273,10 @@ impl App<'_> {
config: Some(ServarrConfig::default()), config: Some(ServarrConfig::default()),
}, },
]), ]),
..App::default() ..App::default()
} }
} }
pub fn test_default_fully_populated() -> Self { pub fn test_default_fully_populated() -> Self {
App { App {
data: Data { data: Data {
+4 -1
View File
@@ -18,6 +18,7 @@ use std::panic::PanicHookInfo;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Instant;
use std::{io, panic, process}; use std::{io, panic, process};
use tokio::select; use tokio::select;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
@@ -242,9 +243,12 @@ async fn start_ui(
terminal.hide_cursor()?; terminal.hide_cursor()?;
let input_events = Events::new(); let input_events = Events::new();
let mut last_frame_instant = Instant::now();
loop { loop {
let mut app = app.lock().await; let mut app = app.lock().await;
app.last_tick = last_frame_instant.elapsed().into();
last_frame_instant = Instant::now();
terminal.draw(|f| ui(f, &mut app))?; terminal.draw(|f| ui(f, &mut app))?;
@@ -256,7 +260,6 @@ async fn start_ui(
handlers::handle_events(key, &mut app); handlers::handle_events(key, &mut app);
} }
Some(InputEvent::Tick) => app.on_tick().await, Some(InputEvent::Tick) => app.on_tick().await,
_ => {} _ => {}
} }
+7 -5
View File
@@ -92,6 +92,11 @@ pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) {
if app.keymapping_table.is_some() { if app.keymapping_table.is_some() {
draw_help_popup(f, app); draw_help_popup(f, app);
} }
let area = f.area();
app
.effects
.process_effects(app.last_tick, f.buffer_mut(), area);
} }
fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
@@ -160,12 +165,9 @@ pub fn draw_help_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
]) ])
.primary() .primary()
}; };
let keymapping_table = ManagarrTable::new( let keymapping_table =
Some(app.keymapping_table.as_mut().unwrap()), ManagarrTable::new(app, |app| app.keymapping_table.as_mut(), keymap_row_mapping)
keymap_row_mapping,
)
.block(title_block("Keybindings")) .block(title_block("Keybindings"))
.loading(app.is_loading)
.headers(["Key", "Alt Key", "Description"]) .headers(["Key", "Alt Key", "Description"])
.constraints([ .constraints([
Constraint::Ratio(1, 3), Constraint::Ratio(1, 3),
+6 -4
View File
@@ -81,6 +81,8 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
app.data.radarr_data.blocklist.current_selection().clone() app.data.radarr_data.blocklist.current_selection().clone()
}; };
let ui_scroll_tick_count = app.ui_scroll_tick_count;
let is_sorting = active_radarr_block == ActiveRadarrBlock::BlocklistSortPrompt;
let blocklist_row_mapping = |blocklist_item: &BlocklistItem| { let blocklist_row_mapping = |blocklist_item: &BlocklistItem| {
let BlocklistItem { let BlocklistItem {
@@ -96,7 +98,7 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
movie.title.scroll_left_or_reset( movie.title.scroll_left_or_reset(
get_width_from_percentage(area, 20), get_width_from_percentage(area, 20),
current_selection == *blocklist_item, current_selection == *blocklist_item,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
let languages_string = languages let languages_string = languages
@@ -125,12 +127,12 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.primary() .primary()
}; };
let blocklist_table = ManagarrTable::new( let blocklist_table = ManagarrTable::new(
Some(&mut app.data.radarr_data.blocklist), app,
|app| Some(&mut app.data.radarr_data.blocklist),
blocklist_row_mapping, blocklist_row_mapping,
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .sorting(is_sorting)
.sorting(active_radarr_block == ActiveRadarrBlock::BlocklistSortPrompt)
.headers([ .headers([
"Movie Title", "Movie Title",
"Source Title", "Source Title",
@@ -56,7 +56,8 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
Layout::vertical([Constraint::Percentage(25), Constraint::Fill(0)]) Layout::vertical([Constraint::Percentage(25), Constraint::Fill(0)])
.margin(1) .margin(1)
.areas(area); .areas(area);
let collection_selection = app.data.radarr_data.collections.current_selection();
let collection_selection = app.data.radarr_data.collections.current_selection().clone();
let quality_profile = app let quality_profile = app
.data .data
.radarr_data .radarr_data
@@ -74,15 +75,19 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
.current_selection() .current_selection()
.clone() .clone()
}; };
let movie_row_mapper = |movie: &CollectionMovie| {
let in_library = if app let ui_scroll_tick_count = app.ui_scroll_tick_count;
let movie_tmdb_ids: Vec<_> = app
.data .data
.radarr_data .radarr_data
.movies .movies
.items .items
.iter() .iter()
.any(|mov| mov.tmdb_id == movie.tmdb_id) .map(|mov| mov.tmdb_id)
{ .collect();
let movie_row_mapper = move |movie: &CollectionMovie| {
let in_library = if movie_tmdb_ids.contains(&movie.tmdb_id) {
"" ""
} else { } else {
"" ""
@@ -90,7 +95,7 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
movie.title.scroll_left_or_reset( movie.title.scroll_left_or_reset(
get_width_from_percentage(table_area, 20), get_width_from_percentage(table_area, 20),
current_selection == *movie, current_selection == *movie,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
let (hours, minutes) = convert_runtime(movie.runtime); let (hours, minutes) = convert_runtime(movie.runtime);
let imdb_rating = movie let imdb_rating = movie
@@ -179,11 +184,11 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
.block(borderless_block()) .block(borderless_block())
.wrap(Wrap { trim: false }); .wrap(Wrap { trim: false });
let movies_table = ManagarrTable::new( let movies_table = ManagarrTable::new(
Some(&mut app.data.radarr_data.collection_movies), app,
|app| Some(&mut app.data.radarr_data.collection_movies),
movie_row_mapper, movie_row_mapper,
) )
.block(layout_block_top_border_with_title(title_style("Movies"))) .block(layout_block_top_border_with_title(title_style("Movies")))
.loading(app.is_loading)
.headers([ .headers([
"", "",
"Title", "Title",
+24 -17
View File
@@ -63,14 +63,25 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
} else { } else {
Collection::default() Collection::default()
}; };
let quality_profile_map = &app.data.radarr_data.quality_profile_map; let quality_profile_map = app.data.radarr_data.quality_profile_map.clone();
let content = Some(&mut app.data.radarr_data.collections); let ui_scroll_tick_count = app.ui_scroll_tick_count;
let is_loading = app.is_loading
|| app.data.radarr_data.movies.is_empty()
|| app.data.radarr_data.quality_profile_map.is_empty();
let is_sorting = active_radarr_block == ActiveRadarrBlock::CollectionsSortPrompt;
let is_searching = active_radarr_block == ActiveRadarrBlock::SearchCollection;
let search_produced_empty_results =
active_radarr_block == ActiveRadarrBlock::SearchCollectionError;
let is_filtering = active_radarr_block == ActiveRadarrBlock::FilterCollections;
let filter_produced_empty_results =
active_radarr_block == ActiveRadarrBlock::FilterCollectionsError;
let collection_row_mapping = |collection: &Collection| { let collection_row_mapping = |collection: &Collection| {
let number_of_movies = collection.movies.as_ref().unwrap_or(&Vec::new()).len(); let number_of_movies = collection.movies.as_ref().unwrap_or(&Vec::new()).len();
collection.title.scroll_left_or_reset( collection.title.scroll_left_or_reset(
get_width_from_percentage(area, 25), get_width_from_percentage(area, 25),
*collection == current_selection, *collection == current_selection,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
let monitored = if collection.monitored { "🏷" } else { "" }; let monitored = if collection.monitored { "🏷" } else { "" };
let search_on_add = if collection.search_on_add { let search_on_add = if collection.search_on_add {
@@ -100,22 +111,18 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
]) ])
.primary() .primary()
}; };
let collections_table = ManagarrTable::new(content, collection_row_mapping) let collections_table = ManagarrTable::new(
.loading( app,
app.is_loading |app| Some(&mut app.data.radarr_data.collections),
|| app.data.radarr_data.movies.is_empty() collection_row_mapping,
|| app.data.radarr_data.quality_profile_map.is_empty(),
) )
.loading(is_loading)
.block(layout_block_top_border()) .block(layout_block_top_border())
.sorting(active_radarr_block == ActiveRadarrBlock::CollectionsSortPrompt) .sorting(is_sorting)
.searching(active_radarr_block == ActiveRadarrBlock::SearchCollection) .searching(is_searching)
.search_produced_empty_results( .search_produced_empty_results(search_produced_empty_results)
active_radarr_block == ActiveRadarrBlock::SearchCollectionError, .filtering(is_filtering)
) .filter_produced_empty_results(filter_produced_empty_results)
.filtering(active_radarr_block == ActiveRadarrBlock::FilterCollections)
.filter_produced_empty_results(
active_radarr_block == ActiveRadarrBlock::FilterCollectionsError,
)
.headers([ .headers([
"Collection", "Collection",
"Number of Movies", "Number of Movies",
+5 -4
View File
@@ -71,8 +71,9 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
app.data.radarr_data.downloads.current_selection().clone() app.data.radarr_data.downloads.current_selection().clone()
}; };
let ui_scroll_tick_count = app.ui_scroll_tick_count;
let downloads_row_mapping = |download_record: &DownloadRecord| { let downloads_row_mapping = move |download_record: &DownloadRecord| {
let DownloadRecord { let DownloadRecord {
title, title,
size, size,
@@ -87,7 +88,7 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
output_path.as_ref().unwrap().scroll_left_or_reset( output_path.as_ref().unwrap().scroll_left_or_reset(
get_width_from_percentage(area, 18), get_width_from_percentage(area, 18),
current_selection == *download_record, current_selection == *download_record,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
} }
@@ -114,11 +115,11 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.primary() .primary()
}; };
let downloads_table = ManagarrTable::new( let downloads_table = ManagarrTable::new(
Some(&mut app.data.radarr_data.downloads), app,
|app| Some(&mut app.data.radarr_data.downloads),
downloads_row_mapping, downloads_row_mapping,
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.headers([ .headers([
"Title", "Title",
"Percent Complete", "Percent Complete",
+6 -7
View File
@@ -111,7 +111,9 @@ impl DrawUi for IndexersUi {
} }
fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let indexers_row_mapping = |indexer: &'_ Indexer| { let tags_map = app.data.radarr_data.tags_map.clone();
let indexers_row_mapping = move |indexer: &'_ Indexer| {
let Indexer { let Indexer {
name, name,
enable_rss, enable_rss,
@@ -136,10 +138,7 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let tags: String = tags let tags: String = tags
.iter() .iter()
.map(|tag_id| { .map(|tag_id| {
app tags_map
.data
.radarr_data
.tags_map
.get_by_left(&tag_id.as_i64().unwrap()) .get_by_left(&tag_id.as_i64().unwrap())
.unwrap_or(&empty_tag) .unwrap_or(&empty_tag)
.clone() .clone()
@@ -158,11 +157,11 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.primary() .primary()
}; };
let indexers_table = ManagarrTable::new( let indexers_table = ManagarrTable::new(
Some(&mut app.data.radarr_data.indexers), app,
|app| Some(&mut app.data.radarr_data.indexers),
indexers_row_mapping, indexers_row_mapping,
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.headers([ .headers([
"Indexer", "Indexer",
"RSS", "RSS",
@@ -33,6 +33,7 @@ impl DrawUi for TestAllIndexersUi {
fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let is_loading = app.is_loading || app.data.radarr_data.indexer_test_all_results.is_none(); let is_loading = app.is_loading || app.data.radarr_data.indexer_test_all_results.is_none();
let block = title_block("Test All Indexers"); let block = title_block("Test All Indexers");
let ui_scroll_tick_count = app.ui_scroll_tick_count;
let current_selection = let current_selection =
if let Some(test_all_results) = app.data.radarr_data.indexer_test_all_results.as_ref() { if let Some(test_all_results) = app.data.radarr_data.indexer_test_all_results.as_ref() {
@@ -45,7 +46,7 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are
result.validation_failures.scroll_left_or_reset( result.validation_failures.scroll_left_or_reset(
get_width_from_percentage(area, 86), get_width_from_percentage(area, 86),
*result == current_selection, *result == current_selection,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
let pass_fail = if result.is_valid { "" } else { "" }; let pass_fail = if result.is_valid { "" } else { "" };
let row = Row::new(vec![ let row = Row::new(vec![
@@ -62,7 +63,8 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are
}; };
let indexers_test_results_table = ManagarrTable::new( let indexers_test_results_table = ManagarrTable::new(
app.data.radarr_data.indexer_test_all_results.as_mut(), app,
|app| app.data.radarr_data.indexer_test_all_results.as_mut(),
test_results_row_mapping, test_results_row_mapping,
) )
.loading(is_loading) .loading(is_loading)
+19 -14
View File
@@ -80,13 +80,14 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Layout::vertical([Constraint::Length(3), Constraint::Fill(0)]) Layout::vertical([Constraint::Length(3), Constraint::Fill(0)])
.margin(1) .margin(1)
.areas(area); .areas(area);
let block_content = &app let block_content = app
.data .data
.radarr_data .radarr_data
.add_movie_search .add_movie_search
.as_ref() .as_ref()
.expect("add_movie_search must be populated") .expect("add_movie_search must be populated")
.text; .text
.clone();
let offset = app let offset = app
.data .data
.radarr_data .radarr_data
@@ -95,6 +96,16 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.expect("add_movie_search must be populated") .expect("add_movie_search must be populated")
.offset .offset
.load(Ordering::SeqCst); .load(Ordering::SeqCst);
let ui_scroll_tick_count = app.ui_scroll_tick_count;
let library_tmdb_ids: Vec<i64> = app
.data
.radarr_data
.movies
.items
.iter()
.map(|m| m.tmdb_id)
.collect();
let search_results_row_mapping = |movie: &AddMovieSearchResult| { let search_results_row_mapping = |movie: &AddMovieSearchResult| {
let (hours, minutes) = convert_runtime(movie.runtime); let (hours, minutes) = convert_runtime(movie.runtime);
let imdb_rating = movie let imdb_rating = movie
@@ -123,14 +134,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
format!("{rotten_tomatoes_rating}%") format!("{rotten_tomatoes_rating}%")
}; };
let in_library = if app let in_library = if library_tmdb_ids.contains(&movie.tmdb_id) {
.data
.radarr_data
.movies
.items
.iter()
.any(|mov| mov.tmdb_id == movie.tmdb_id)
{
"" ""
} else { } else {
"" ""
@@ -139,7 +143,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
movie.title.scroll_left_or_reset( movie.title.scroll_left_or_reset(
get_width_from_percentage(area, 27), get_width_from_percentage(area, 27),
*movie == current_selection, *movie == current_selection,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -157,7 +161,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { if let Route::Radarr(active_radarr_block, _) = app.get_current_route() {
match active_radarr_block { match active_radarr_block {
ActiveRadarrBlock::AddMovieSearchInput => { ActiveRadarrBlock::AddMovieSearchInput => {
let search_box = InputBox::new(block_content) let search_box = InputBox::new(&block_content)
.offset(offset) .offset(offset)
.block(title_block_centered("Add Movie")); .block(title_block_centered("Add Movie"));
@@ -181,7 +185,8 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
| ActiveRadarrBlock::AddMovieAlreadyInLibrary | ActiveRadarrBlock::AddMovieAlreadyInLibrary
| ActiveRadarrBlock::AddMovieTagsInput => { | ActiveRadarrBlock::AddMovieTagsInput => {
let search_results_table = ManagarrTable::new( let search_results_table = ManagarrTable::new(
app.data.radarr_data.add_searched_movies.as_mut(), app,
|app| app.data.radarr_data.add_searched_movies.as_mut(),
search_results_row_mapping, search_results_row_mapping,
) )
.loading(is_loading) .loading(is_loading)
@@ -212,7 +217,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} }
f.render_widget( f.render_widget(
InputBox::new(block_content) InputBox::new(&block_content)
.offset(offset) .offset(offset)
.block(title_block_centered("Add Movie")), .block(title_block_centered("Add Movie")),
search_box_area, search_box_area,
+24 -14
View File
@@ -64,7 +64,9 @@ impl DrawUi for LibraryUi {
.yes_no_value(app.data.radarr_data.prompt_confirm); .yes_no_value(app.data.radarr_data.prompt_confirm);
f.render_widget( f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt), Popup::new(confirmation_prompt)
.size(Size::MediumPrompt)
.app(app),
f.area(), f.area(),
); );
} }
@@ -81,16 +83,21 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
Movie::default() Movie::default()
}; };
let quality_profile_map = &app.data.radarr_data.quality_profile_map; let quality_profile_map = app.data.radarr_data.quality_profile_map.clone();
let tags_map = &app.data.radarr_data.tags_map; let tags_map = app.data.radarr_data.tags_map.clone();
let downloads_vec = &app.data.radarr_data.downloads.items; let downloads_vec = app.data.radarr_data.downloads.items.clone();
let content = Some(&mut app.data.radarr_data.movies); let ui_scroll_tick_count = app.ui_scroll_tick_count;
let is_sorting = active_radarr_block == ActiveRadarrBlock::MoviesSortPrompt;
let is_searching = active_radarr_block == ActiveRadarrBlock::SearchMovie;
let search_produced_empty_results = active_radarr_block == ActiveRadarrBlock::SearchMovieError;
let is_filtering = active_radarr_block == ActiveRadarrBlock::FilterMovies;
let filter_produced_empty_results = active_radarr_block == ActiveRadarrBlock::FilterMoviesError;
let library_table_row_mapping = |movie: &Movie| { let library_table_row_mapping = |movie: &Movie| {
movie.title.scroll_left_or_reset( movie.title.scroll_left_or_reset(
get_width_from_percentage(area, 27), get_width_from_percentage(area, 27),
*movie == current_selection, *movie == current_selection,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
let monitored = if movie.monitored { "🏷" } else { "" }; let monitored = if movie.monitored { "🏷" } else { "" };
let studio = movie.studio.clone().unwrap_or_default(); let studio = movie.studio.clone().unwrap_or_default();
@@ -112,7 +119,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.join(", "); .join(", ");
decorate_with_row_style( decorate_with_row_style(
downloads_vec, &downloads_vec,
movie, movie,
Row::new(vec![ Row::new(vec![
Cell::from(movie.title.to_string()), Cell::from(movie.title.to_string()),
@@ -128,14 +135,17 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]), ]),
) )
}; };
let library_table = ManagarrTable::new(content, library_table_row_mapping) let library_table = ManagarrTable::new(
app,
|app| Some(&mut app.data.radarr_data.movies),
library_table_row_mapping,
)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .sorting(is_sorting)
.sorting(active_radarr_block == ActiveRadarrBlock::MoviesSortPrompt) .searching(is_searching)
.searching(active_radarr_block == ActiveRadarrBlock::SearchMovie) .search_produced_empty_results(search_produced_empty_results)
.search_produced_empty_results(active_radarr_block == ActiveRadarrBlock::SearchMovieError) .filtering(is_filtering)
.filtering(active_radarr_block == ActiveRadarrBlock::FilterMovies) .filter_produced_empty_results(filter_produced_empty_results)
.filter_produced_empty_results(active_radarr_block == ActiveRadarrBlock::FilterMoviesError)
.headers([ .headers([
"Title", "Title",
"Year", "Year",
+61 -54
View File
@@ -10,7 +10,7 @@ use serde_json::Number;
use crate::app::App; use crate::app::App;
use crate::models::Route; use crate::models::Route;
use crate::models::radarr_models::{Credit, MovieHistoryItem, RadarrRelease}; use crate::models::radarr_models::{Credit, MovieHistoryItem, RadarrRelease};
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS};
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{ use crate::ui::utils::{
@@ -225,15 +225,17 @@ fn draw_movie_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
} }
fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Some(movie_details_modal) = app.data.radarr_data.movie_details_modal.as_mut() { let current_selection = if let Some(modal) = app.data.radarr_data.movie_details_modal.as_ref() {
let current_selection = if movie_details_modal.movie_history.items.is_empty() { if modal.movie_history.items.is_empty() {
MovieHistoryItem::default() MovieHistoryItem::default()
} else { } else {
movie_details_modal modal.movie_history.current_selection().clone()
.movie_history }
.current_selection() } else {
.clone() MovieHistoryItem::default()
}; };
let ui_scroll_tick_count = app.ui_scroll_tick_count;
let history_row_mapping = |movie_history_item: &MovieHistoryItem| { let history_row_mapping = |movie_history_item: &MovieHistoryItem| {
let MovieHistoryItem { let MovieHistoryItem {
source_title, source_title,
@@ -246,7 +248,7 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
movie_history_item.source_title.scroll_left_or_reset( movie_history_item.source_title.scroll_left_or_reset(
get_width_from_percentage(area, 34), get_width_from_percentage(area, 34),
current_selection == *movie_history_item, current_selection == *movie_history_item,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -265,11 +267,18 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.primary() .primary()
}; };
let history_table = ManagarrTable::new( let history_table = ManagarrTable::new(
Some(&mut movie_details_modal.movie_history), app,
|app| {
app
.data
.radarr_data
.movie_details_modal
.as_mut()
.map(|m| &mut m.movie_history)
},
history_row_mapping, history_row_mapping,
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.headers(["Source Title", "Event Type", "Languages", "Quality", "Date"]) .headers(["Source Title", "Event Type", "Languages", "Quality", "Date"])
.constraints([ .constraints([
Constraint::Percentage(34), Constraint::Percentage(34),
@@ -281,11 +290,8 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(history_table, area); f.render_widget(history_table, area);
} }
}
fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
match app.data.radarr_data.movie_details_modal.as_mut() {
Some(movie_details_modal) if !app.is_loading => {
let cast_row_mapping = |cast_member: &Credit| { let cast_row_mapping = |cast_member: &Credit| {
let Credit { let Credit {
person_name, person_name,
@@ -299,28 +305,26 @@ fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let content = Some(&mut movie_details_modal.movie_cast); let cast_table = ManagarrTable::new(
let cast_table = ManagarrTable::new(content, cast_row_mapping) app,
|app| {
app
.data
.radarr_data
.movie_details_modal
.as_mut()
.map(|m| &mut m.movie_cast)
},
cast_row_mapping,
)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.headers(["Cast Member", "Character"]) .headers(["Cast Member", "Character"])
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
f.render_widget(cast_table, area); f.render_widget(cast_table, area);
} }
_ => f.render_widget(
LoadingBlock::new(
app.is_loading || app.data.radarr_data.movie_details_modal.is_none(),
layout_block_top_border(),
),
area,
),
}
}
fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
match app.data.radarr_data.movie_details_modal.as_mut() {
Some(movie_details_modal) if !app.is_loading => {
let crew_row_mapping = |crew_member: &Credit| { let crew_row_mapping = |crew_member: &Credit| {
let Credit { let Credit {
person_name, person_name,
@@ -336,26 +340,25 @@ fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let content = Some(&mut movie_details_modal.movie_crew); let crew_table = ManagarrTable::new(
let crew_table = ManagarrTable::new(content, crew_row_mapping) app,
|app| {
app
.data
.radarr_data
.movie_details_modal
.as_mut()
.map(|m| &mut m.movie_crew)
},
crew_row_mapping,
)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.headers(["Crew Member", "Job", "Department"]) .headers(["Crew Member", "Job", "Department"])
.constraints(iter::repeat_n(Constraint::Ratio(1, 3), 3)); .constraints(iter::repeat_n(Constraint::Ratio(1, 3), 3));
f.render_widget(crew_table, area); f.render_widget(crew_table, area);
} }
_ => f.render_widget(
LoadingBlock::new(
app.is_loading || app.data.radarr_data.movie_details_modal.is_none(),
layout_block_top_border(),
),
area,
),
}
}
fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { if let Route::Radarr(active_radarr_block, _) = app.get_current_route() {
let (current_selection, is_empty) = match app.data.radarr_data.movie_details_modal.as_ref() { let (current_selection, is_empty) = match app.data.radarr_data.movie_details_modal.as_ref() {
@@ -369,16 +372,9 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
_ => (RadarrRelease::default(), true), _ => (RadarrRelease::default(), true),
}; };
let current_route = app.get_current_route(); let current_route = app.get_current_route();
let mut default_movie_details_modal = MovieDetailsModal::default(); let ui_scroll_tick_count = app.ui_scroll_tick_count;
let content = Some( let is_sorting = active_radarr_block == ActiveRadarrBlock::ManualSearchSortPrompt;
&mut app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap_or(&mut default_movie_details_modal)
.movie_releases,
);
let releases_row_mapping = |release: &RadarrRelease| { let releases_row_mapping = |release: &RadarrRelease| {
let RadarrRelease { let RadarrRelease {
protocol, protocol,
@@ -398,7 +394,7 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
get_width_from_percentage(area, 30), get_width_from_percentage(area, 30),
current_selection == *release current_selection == *release
&& current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(), && current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(),
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
let size = convert_to_gb(*size); let size = convert_to_gb(*size);
let rejected_str = if *rejected { "" } else { "" }; let rejected_str = if *rejected { "" } else { "" };
@@ -443,10 +439,21 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let releases_table = ManagarrTable::new(content, releases_row_mapping) let releases_table = ManagarrTable::new(
app,
|app| {
app
.data
.radarr_data
.movie_details_modal
.as_mut()
.map(|m| &mut m.movie_releases)
},
releases_row_mapping,
)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading || is_empty) .loading(is_empty)
.sorting(active_radarr_block == ActiveRadarrBlock::ManualSearchSortPrompt) .sorting(is_sorting)
.headers([ .headers([
"Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality", "Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality",
]) ])
+2 -2
View File
@@ -83,11 +83,11 @@ fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
}; };
let root_folders_table = ManagarrTable::new( let root_folders_table = ManagarrTable::new(
Some(&mut app.data.radarr_data.root_folders), app,
|app| Some(&mut app.data.radarr_data.root_folders),
root_folders_row_mapping, root_folders_row_mapping,
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.headers(["Path", "Free Space", "Unmapped Folders"]) .headers(["Path", "Free Space", "Unmapped Folders"])
.constraints([ .constraints([
Constraint::Ratio(3, 5), Constraint::Ratio(3, 5),
+7 -4
View File
@@ -96,9 +96,12 @@ fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let tasks_table = ManagarrTable::new(Some(&mut app.data.radarr_data.tasks), tasks_row_mapping) let tasks_table = ManagarrTable::new(
app,
|app| Some(&mut app.data.radarr_data.tasks),
tasks_row_mapping,
)
.block(title_block("Tasks")) .block(title_block("Tasks"))
.loading(app.is_loading)
.highlight_rows(false) .highlight_rows(false)
.headers(TASK_TABLE_HEADERS) .headers(TASK_TABLE_HEADERS)
.constraints(TASK_TABLE_CONSTRAINTS); .constraints(TASK_TABLE_CONSTRAINTS);
@@ -144,11 +147,11 @@ pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rec
.primary() .primary()
}; };
let events_table = ManagarrTable::new( let events_table = ManagarrTable::new(
Some(&mut app.data.radarr_data.queued_events), app,
|app| Some(&mut app.data.radarr_data.queued_events),
events_row_mapping, events_row_mapping,
) )
.block(title_block("Queued Events")) .block(title_block("Queued Events"))
.loading(app.is_loading)
.highlight_rows(false) .highlight_rows(false)
.headers(["Trigger", "Status", "Name", "Queued", "Started", "Duration"]) .headers(["Trigger", "Status", "Name", "Queued", "Started", "Duration"])
.constraints([ .constraints([
+5 -2
View File
@@ -93,8 +93,11 @@ fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let tasks_table = ManagarrTable::new(Some(&mut app.data.radarr_data.tasks), tasks_row_mapping) let tasks_table = ManagarrTable::new(
.loading(app.is_loading) app,
|app| Some(&mut app.data.radarr_data.tasks),
tasks_row_mapping,
)
.margin(1) .margin(1)
.headers(TASK_TABLE_HEADERS) .headers(TASK_TABLE_HEADERS)
.constraints(TASK_TABLE_CONSTRAINTS); .constraints(TASK_TABLE_CONSTRAINTS);
@@ -17,9 +17,9 @@ expression: output
╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯ ╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯
╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ │ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Title ▼ Year Studio Runtime Rating Language Size Quality Profile Monitored Tags
=> Test 2023 21st Century Alex 2h 0m R English 3.30 GB HD - 1080p 🏷 alex
│ │ │ │
│ │ │ │
│ │ │ │
@@ -17,37 +17,37 @@ expression: output
╰───────────────────────────────────│ f filter │─────────────────╯╰──────────────────╯ ╰───────────────────────────────────│ f filter │─────────────────╯╰──────────────────╯
╭ Movies ─────────────────────────│ ctrl-r refresh │─────────────────────────────────────╮ ╭ Movies ─────────────────────────│ ctrl-r refresh │─────────────────────────────────────╮
│ Library │ Collections │ Downloads │ u update all │ │ │ Library │ Collections │ Downloads │ u update all │ │
───────────────────────────────────│ enter details │─────────────────────────────────────
Title ▼ │ esc cancel filter │ofile Monitored Tags
=> Test │ ↑ k scroll up 🏷 alex
j scroll down
h previous tab
│ → l next tab
│ pgUp ctrl-u page up
│ pgDown ctrl-d page down
│ tab next servarr │
│ shift-tab previous servarr │
│ q quit │
│ ? show/hide keybindings │
╰─────────────────────────────────────────────────────────────────────────────────────────╯
│ │ │ │
│ │ │ │
│ │ │ │
@@ -20,9 +20,9 @@ expression: output
╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯ ╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯
╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ │ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Title ▼ Year Studio Runtime Rating Language Size Quality Profile Monitored Tags
=> Test 2023 21st Century Alex 2h 0m R English 3.30 GB HD - 1080p 🏷 alex
│ │ │ │
│ │ │ │
│ │ │ │
+4 -3
View File
@@ -103,13 +103,14 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let is_sorting = active_sonarr_block == ActiveSonarrBlock::BlocklistSortPrompt;
let blocklist_table = ManagarrTable::new( let blocklist_table = ManagarrTable::new(
Some(&mut app.data.sonarr_data.blocklist), app,
|app| Some(&mut app.data.sonarr_data.blocklist),
blocklist_row_mapping, blocklist_row_mapping,
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .sorting(is_sorting)
.sorting(active_sonarr_block == ActiveSonarrBlock::BlocklistSortPrompt)
.headers([ .headers([
"Series Title", "Series Title",
"Source Title", "Source Title",
+4 -3
View File
@@ -72,6 +72,7 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
app.data.sonarr_data.downloads.current_selection().clone() app.data.sonarr_data.downloads.current_selection().clone()
}; };
let ui_scroll_tick_count = app.ui_scroll_tick_count;
let downloads_row_mapping = |download_record: &DownloadRecord| { let downloads_row_mapping = |download_record: &DownloadRecord| {
let DownloadRecord { let DownloadRecord {
@@ -88,7 +89,7 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
output_path.as_ref().unwrap().scroll_left_or_reset( output_path.as_ref().unwrap().scroll_left_or_reset(
get_width_from_percentage(area, 18), get_width_from_percentage(area, 18),
current_selection == *download_record, current_selection == *download_record,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
} }
@@ -120,11 +121,11 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.primary() .primary()
}; };
let downloads_table = ManagarrTable::new( let downloads_table = ManagarrTable::new(
Some(&mut app.data.sonarr_data.downloads), app,
|app| Some(&mut app.data.sonarr_data.downloads),
downloads_row_mapping, downloads_row_mapping,
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.headers([ .headers([
"Title", "Title",
"Percent Complete", "Percent Complete",
+10 -5
View File
@@ -55,6 +55,8 @@ fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
app.data.sonarr_data.history.current_selection().clone() app.data.sonarr_data.history.current_selection().clone()
}; };
let ui_scroll_tick_count = app.ui_scroll_tick_count;
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
let history_row_mapping = |history_item: &SonarrHistoryItem| { let history_row_mapping = |history_item: &SonarrHistoryItem| {
let SonarrHistoryItem { let SonarrHistoryItem {
@@ -69,7 +71,7 @@ fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
source_title.scroll_left_or_reset( source_title.scroll_left_or_reset(
get_width_from_percentage(area, 40), get_width_from_percentage(area, 40),
current_selection == *history_item, current_selection == *history_item,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -93,10 +95,12 @@ fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let history_table = let history_table = ManagarrTable::new(
ManagarrTable::new(Some(&mut app.data.sonarr_data.history), history_row_mapping) app,
|app| Some(&mut app.data.sonarr_data.history),
history_row_mapping,
)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.sorting(active_sonarr_block == ActiveSonarrBlock::HistorySortPrompt) .sorting(active_sonarr_block == ActiveSonarrBlock::HistorySortPrompt)
.searching(active_sonarr_block == ActiveSonarrBlock::SearchHistory) .searching(active_sonarr_block == ActiveSonarrBlock::SearchHistory)
.search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchHistoryError) .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchHistoryError)
@@ -154,5 +158,6 @@ fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
.style(Style::new().secondary()) .style(Style::new().secondary())
.alignment(Alignment::Left); .alignment(Alignment::Left);
f.render_widget(Popup::new(message).size(Size::NarrowMessage), f.area()); let widget = Popup::new(message).size(Size::NarrowMessage).app(app);
f.render_widget(widget, f.area());
} }
+5 -6
View File
@@ -111,6 +111,8 @@ impl DrawUi for IndexersUi {
} }
fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let tags_map = app.data.sonarr_data.tags_map.clone();
let indexers_row_mapping = |indexer: &'_ Indexer| { let indexers_row_mapping = |indexer: &'_ Indexer| {
let Indexer { let Indexer {
name, name,
@@ -136,10 +138,7 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let tags: String = tags let tags: String = tags
.iter() .iter()
.map(|tag_id| { .map(|tag_id| {
app tags_map
.data
.sonarr_data
.tags_map
.get_by_left(&tag_id.as_i64().unwrap()) .get_by_left(&tag_id.as_i64().unwrap())
.unwrap_or(&empty_tag) .unwrap_or(&empty_tag)
.clone() .clone()
@@ -158,11 +157,11 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.primary() .primary()
}; };
let indexers_table = ManagarrTable::new( let indexers_table = ManagarrTable::new(
Some(&mut app.data.sonarr_data.indexers), app,
|app| Some(&mut app.data.sonarr_data.indexers),
indexers_row_mapping, indexers_row_mapping,
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.headers([ .headers([
"Indexer", "Indexer",
"RSS", "RSS",
@@ -39,12 +39,14 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are
} else { } else {
IndexerTestResultModalItem::default() IndexerTestResultModalItem::default()
}; };
let ui_scroll_tick_count = app.ui_scroll_tick_count;
f.render_widget(title_block("Test All Indexers"), area); f.render_widget(title_block("Test All Indexers"), area);
let test_results_row_mapping = |result: &IndexerTestResultModalItem| { let test_results_row_mapping = |result: &IndexerTestResultModalItem| {
result.validation_failures.scroll_left_or_reset( result.validation_failures.scroll_left_or_reset(
get_width_from_percentage(area, 86), get_width_from_percentage(area, 86),
*result == current_selection, *result == current_selection,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
let pass_fail = if result.is_valid { "" } else { "" }; let pass_fail = if result.is_valid { "" } else { "" };
let row = Row::new(vec![ let row = Row::new(vec![
@@ -61,7 +63,8 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are
}; };
let indexers_test_results_table = ManagarrTable::new( let indexers_test_results_table = ManagarrTable::new(
app.data.sonarr_data.indexer_test_all_results.as_mut(), app,
|app| app.data.sonarr_data.indexer_test_all_results.as_mut(),
test_results_row_mapping, test_results_row_mapping,
) )
.loading(is_loading) .loading(is_loading)
+26 -14
View File
@@ -75,13 +75,14 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Layout::vertical([Constraint::Length(3), Constraint::Fill(0)]) Layout::vertical([Constraint::Length(3), Constraint::Fill(0)])
.margin(1) .margin(1)
.areas(area); .areas(area);
let block_content = &app let block_content = app
.data .data
.sonarr_data .sonarr_data
.add_series_search .add_series_search
.as_ref() .as_ref()
.expect("add_series_search must be populated") .expect("add_series_search must be populated")
.text; .text
.clone();
let offset = app let offset = app
.data .data
.sonarr_data .sonarr_data
@@ -90,6 +91,16 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.expect("add_series_search must be populated") .expect("add_series_search must be populated")
.offset .offset
.load(Ordering::SeqCst); .load(Ordering::SeqCst);
let series_tvdb_ids: Vec<_> = app
.data
.sonarr_data
.series
.items
.iter()
.map(|s| s.tvdb_id)
.collect();
let ui_scroll_tick_count = app.ui_scroll_tick_count;
let search_results_row_mapping = |series: &AddSeriesSearchResult| { let search_results_row_mapping = |series: &AddSeriesSearchResult| {
let rating = series.ratings.clone().unwrap_or_default().value; let rating = series.ratings.clone().unwrap_or_default().value;
let series_rating = if rating == 0.0 { let series_rating = if rating == 0.0 {
@@ -97,14 +108,7 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
format!("{rating:.1}") format!("{rating:.1}")
}; };
let in_library = if app let in_library = if series_tvdb_ids.contains(&series.tvdb_id) {
.data
.sonarr_data
.series
.items
.iter()
.any(|mov| mov.tvdb_id == series.tvdb_id)
{
"" ""
} else { } else {
"" ""
@@ -119,7 +123,7 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
series.title.scroll_left_or_reset( series.title.scroll_left_or_reset(
get_width_from_percentage(area, 27), get_width_from_percentage(area, 27),
*series == current_selection, *series == current_selection,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -137,7 +141,7 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
match active_sonarr_block { match active_sonarr_block {
ActiveSonarrBlock::AddSeriesSearchInput => { ActiveSonarrBlock::AddSeriesSearchInput => {
let search_box = InputBox::new(block_content) let search_box = InputBox::new(&block_content)
.offset(offset) .offset(offset)
.block(title_block_centered("Add Series")); .block(title_block_centered("Add Series"));
@@ -162,7 +166,8 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
| ActiveSonarrBlock::AddSeriesAlreadyInLibrary | ActiveSonarrBlock::AddSeriesAlreadyInLibrary
| ActiveSonarrBlock::AddSeriesTagsInput => { | ActiveSonarrBlock::AddSeriesTagsInput => {
let search_results_table = ManagarrTable::new( let search_results_table = ManagarrTable::new(
app.data.sonarr_data.add_searched_series.as_mut(), app,
|app| app.data.sonarr_data.add_searched_series.as_mut(),
search_results_row_mapping, search_results_row_mapping,
) )
.loading(is_loading) .loading(is_loading)
@@ -181,13 +186,20 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]); ]);
f.render_widget(search_results_table, results_area); f.render_widget(search_results_table, results_area);
f.render_widget(
InputBox::new(&block_content)
.offset(offset)
.block(title_block_centered("Add Series")),
search_box_area,
);
return;
} }
_ => (), _ => (),
} }
} }
f.render_widget( f.render_widget(
InputBox::new(block_content) InputBox::new(&block_content)
.offset(offset) .offset(offset)
.block(title_block_centered("Add Series")), .block(title_block_centered("Add Series")),
search_box_area, search_box_area,
+23 -12
View File
@@ -267,6 +267,7 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
.current_selection() .current_selection()
.clone() .clone()
}; };
let ui_scroll_tick_count = app.ui_scroll_tick_count;
let history_row_mapping = |history_item: &SonarrHistoryItem| { let history_row_mapping = |history_item: &SonarrHistoryItem| {
let SonarrHistoryItem { let SonarrHistoryItem {
@@ -281,7 +282,7 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
source_title.scroll_left_or_reset( source_title.scroll_left_or_reset(
get_width_from_percentage(area, 40), get_width_from_percentage(area, 40),
current_selection == *history_item, current_selection == *history_item,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -305,7 +306,11 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
]) ])
.primary() .primary()
}; };
let mut episode_history_table = &mut app let history_table = ManagarrTable::new(
app,
|app| {
Some(
&mut app
.data .data
.sonarr_data .sonarr_data
.season_details_modal .season_details_modal
@@ -314,11 +319,12 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
.episode_details_modal .episode_details_modal
.as_mut() .as_mut()
.expect("episode_details_modal must exist in this context") .expect("episode_details_modal must exist in this context")
.episode_history; .episode_history,
let history_table = )
ManagarrTable::new(Some(&mut episode_history_table), history_row_mapping) },
history_row_mapping,
)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) .headers(["Source Title", "Event Type", "Language", "Quality", "Date"])
.constraints([ .constraints([
Constraint::Percentage(40), Constraint::Percentage(40),
@@ -409,6 +415,7 @@ fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
episode_details_modal.episode_releases.is_empty(), episode_details_modal.episode_releases.is_empty(),
) )
}; };
let ui_scroll_tick_count = app.ui_scroll_tick_count;
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
let episode_release_row_mapping = |release: &SonarrRelease| { let episode_release_row_mapping = |release: &SonarrRelease| {
@@ -431,7 +438,7 @@ fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
get_width_from_percentage(area, 30), get_width_from_percentage(area, 30),
current_selection == *release current_selection == *release
&& active_sonarr_block != ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt, && active_sonarr_block != ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
let size = convert_to_gb(*size); let size = convert_to_gb(*size);
let rejected_str = if *rejected { "" } else { "" }; let rejected_str = if *rejected { "" } else { "" };
@@ -480,7 +487,11 @@ fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let mut episode_release_table = &mut app let release_table = ManagarrTable::new(
app,
|app| {
Some(
&mut app
.data .data
.sonarr_data .sonarr_data
.season_details_modal .season_details_modal
@@ -489,13 +500,13 @@ fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.episode_details_modal .episode_details_modal
.as_mut() .as_mut()
.expect("episode_details_modal must exist in this context") .expect("episode_details_modal must exist in this context")
.episode_releases; .episode_releases,
let release_table = ManagarrTable::new( )
Some(&mut episode_release_table), },
episode_release_row_mapping, episode_release_row_mapping,
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading || is_empty) .loading(is_empty)
.sorting(active_sonarr_block == ActiveSonarrBlock::ManualEpisodeSearchSortPrompt) .sorting(active_sonarr_block == ActiveSonarrBlock::ManualEpisodeSearchSortPrompt)
.headers([ .headers([
"Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality", "Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality",
+10 -7
View File
@@ -86,16 +86,16 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
Series::default() Series::default()
}; };
let quality_profile_map = &app.data.sonarr_data.quality_profile_map; let quality_profile_map = app.data.sonarr_data.quality_profile_map.clone();
let language_profile_map = &app.data.sonarr_data.language_profiles_map; let language_profile_map = app.data.sonarr_data.language_profiles_map.clone();
let tags_map = &app.data.sonarr_data.tags_map; let tags_map = app.data.sonarr_data.tags_map.clone();
let content = Some(&mut app.data.sonarr_data.series); let ui_scroll_tick_count = app.ui_scroll_tick_count;
let series_table_row_mapping = |series: &Series| { let series_table_row_mapping = |series: &Series| {
series.title.scroll_left_or_reset( series.title.scroll_left_or_reset(
get_width_from_percentage(area, 23), get_width_from_percentage(area, 23),
*series == current_selection, *series == current_selection,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
let monitored = if series.monitored { "🏷" } else { "" }; let monitored = if series.monitored { "🏷" } else { "" };
let certification = series.certification.clone().unwrap_or_default(); let certification = series.certification.clone().unwrap_or_default();
@@ -139,9 +139,12 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]), ]),
) )
}; };
let series_table = ManagarrTable::new(content, series_table_row_mapping) let series_table = ManagarrTable::new(
app,
|app| Some(&mut app.data.sonarr_data.series),
series_table_row_mapping,
)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.sorting(active_sonarr_block == ActiveSonarrBlock::SeriesSortPrompt) .sorting(active_sonarr_block == ActiveSonarrBlock::SeriesSortPrompt)
.searching(active_sonarr_block == ActiveSonarrBlock::SearchSeries) .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeries)
.filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeries) .filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeries)
+42 -25
View File
@@ -162,16 +162,7 @@ fn draw_episodes_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.episode_files .episode_files
.items .items
.clone(); .clone();
let content = Some( let downloads_vec = app.data.sonarr_data.downloads.items.clone();
&mut app
.data
.sonarr_data
.season_details_modal
.as_mut()
.expect("Season details modal is unpopulated")
.episodes,
);
let downloads_vec = &app.data.sonarr_data.downloads.items;
let episode_row_mapping = |episode: &Episode| { let episode_row_mapping = |episode: &Episode| {
let Episode { let Episode {
@@ -202,7 +193,7 @@ fn draw_episodes_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
}; };
decorate_with_row_style( decorate_with_row_style(
downloads_vec, &downloads_vec,
episode, episode,
Row::new(vec![ Row::new(vec![
Cell::from(episode_monitored.to_owned()), Cell::from(episode_monitored.to_owned()),
@@ -215,9 +206,22 @@ fn draw_episodes_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
) )
}; };
let is_searching = active_sonarr_block == ActiveSonarrBlock::SearchEpisodes; let is_searching = active_sonarr_block == ActiveSonarrBlock::SearchEpisodes;
let season_table = ManagarrTable::new(content, episode_row_mapping) let season_table = ManagarrTable::new(
app,
|app| {
Some(
&mut app
.data
.sonarr_data
.season_details_modal
.as_mut()
.expect("Season details modal is unpopulated")
.episodes,
)
},
episode_row_mapping,
)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.searching(is_searching) .searching(is_searching)
.search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchEpisodesError) .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchEpisodesError)
.headers([ .headers([
@@ -256,6 +260,7 @@ fn draw_season_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.current_selection() .current_selection()
.clone() .clone()
}; };
let ui_scroll_tick_count = app.ui_scroll_tick_count;
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
let history_row_mapping = |history_item: &SonarrHistoryItem| { let history_row_mapping = |history_item: &SonarrHistoryItem| {
@@ -271,7 +276,7 @@ fn draw_season_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
source_title.scroll_left_or_reset( source_title.scroll_left_or_reset(
get_width_from_percentage(area, 40), get_width_from_percentage(area, 40),
current_selection == *history_item, current_selection == *history_item,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -295,17 +300,22 @@ fn draw_season_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let mut season_history_table = &mut app let history_table = ManagarrTable::new(
app,
|app| {
Some(
&mut app
.data .data
.sonarr_data .sonarr_data
.season_details_modal .season_details_modal
.as_mut() .as_mut()
.expect("season_details_modal must exist in this context") .expect("season_details_modal must exist in this context")
.season_history; .season_history,
let history_table = )
ManagarrTable::new(Some(&mut season_history_table), history_row_mapping) },
history_row_mapping,
)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.sorting(active_sonarr_block == ActiveSonarrBlock::SeasonHistorySortPrompt) .sorting(active_sonarr_block == ActiveSonarrBlock::SeasonHistorySortPrompt)
.searching(active_sonarr_block == ActiveSonarrBlock::SearchSeasonHistory) .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeasonHistory)
.search_produced_empty_results( .search_produced_empty_results(
@@ -360,6 +370,7 @@ fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
season_details_modal.season_releases.is_empty(), season_details_modal.season_releases.is_empty(),
) )
}; };
let ui_scroll_tick_count = app.ui_scroll_tick_count;
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
let season_release_row_mapping = |release: &SonarrRelease| { let season_release_row_mapping = |release: &SonarrRelease| {
@@ -382,7 +393,7 @@ fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
get_width_from_percentage(area, 30), get_width_from_percentage(area, 30),
current_selection == *release current_selection == *release
&& active_sonarr_block != ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt, && active_sonarr_block != ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
let size = convert_to_gb(*size); let size = convert_to_gb(*size);
let rejected_str = if *rejected { "" } else { "" }; let rejected_str = if *rejected { "" } else { "" };
@@ -431,17 +442,23 @@ fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let mut season_release_table = &mut app let release_table = ManagarrTable::new(
app,
|app| {
Some(
&mut app
.data .data
.sonarr_data .sonarr_data
.season_details_modal .season_details_modal
.as_mut() .as_mut()
.expect("season_details_modal must exist in this context") .expect("season_details_modal must exist in this context")
.season_releases; .season_releases,
let release_table = )
ManagarrTable::new(Some(&mut season_release_table), season_release_row_mapping) },
season_release_row_mapping,
)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading || is_empty) .loading(is_empty)
.sorting(active_sonarr_block == ActiveSonarrBlock::ManualSeasonSearchSortPrompt) .sorting(active_sonarr_block == ActiveSonarrBlock::ManualSeasonSearchSortPrompt)
.headers([ .headers([
"Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality", "Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality",
+12 -13
View File
@@ -228,7 +228,6 @@ pub fn draw_series_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
let content = Some(&mut app.data.sonarr_data.seasons);
let season_row_mapping = |season: &Season| { let season_row_mapping = |season: &Season| {
let Season { let Season {
title, title,
@@ -271,9 +270,12 @@ fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} }
}; };
let is_searching = active_sonarr_block == ActiveSonarrBlock::SearchSeason; let is_searching = active_sonarr_block == ActiveSonarrBlock::SearchSeason;
let season_table = ManagarrTable::new(content, season_row_mapping) let season_table = ManagarrTable::new(
app,
|app| Some(&mut app.data.sonarr_data.seasons),
season_row_mapping,
)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.searching(is_searching) .searching(is_searching)
.search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchSeasonError) .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchSeasonError)
.headers(["Monitored", "Season", "Episode Count", "Size on Disk"]) .headers(["Monitored", "Season", "Episode Count", "Size on Disk"])
@@ -300,6 +302,7 @@ fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
series_history.current_selection().clone() series_history.current_selection().clone()
}; };
let ui_scroll_tick_count = app.ui_scroll_tick_count;
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
let history_row_mapping = |history_item: &SonarrHistoryItem| { let history_row_mapping = |history_item: &SonarrHistoryItem| {
@@ -315,7 +318,7 @@ fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
source_title.scroll_left_or_reset( source_title.scroll_left_or_reset(
get_width_from_percentage(area, 40), get_width_from_percentage(area, 40),
current_selection == *history_item, current_selection == *history_item,
app.ui_scroll_tick_count == 0, ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -339,16 +342,12 @@ fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let mut series_history_table = app let history_table = ManagarrTable::new(
.data app,
.sonarr_data |app| app.data.sonarr_data.series_history.as_mut(),
.series_history history_row_mapping,
.as_mut() )
.expect("series_history must be populated");
let history_table =
ManagarrTable::new(Some(&mut series_history_table), history_row_mapping)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.sorting(active_sonarr_block == ActiveSonarrBlock::SeriesHistorySortPrompt) .sorting(active_sonarr_block == ActiveSonarrBlock::SeriesHistorySortPrompt)
.searching(active_sonarr_block == ActiveSonarrBlock::SearchSeriesHistory) .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeriesHistory)
.search_produced_empty_results( .search_produced_empty_results(
+2 -2
View File
@@ -84,11 +84,11 @@ fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
}; };
let root_folders_table = ManagarrTable::new( let root_folders_table = ManagarrTable::new(
Some(&mut app.data.sonarr_data.root_folders), app,
|app| Some(&mut app.data.sonarr_data.root_folders),
root_folders_row_mapping, root_folders_row_mapping,
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading)
.headers(["Path", "Free Space", "Unmapped Folders"]) .headers(["Path", "Free Space", "Unmapped Folders"])
.constraints([ .constraints([
Constraint::Ratio(3, 5), Constraint::Ratio(3, 5),
+7 -4
View File
@@ -89,9 +89,12 @@ fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let tasks_table = ManagarrTable::new(Some(&mut app.data.sonarr_data.tasks), tasks_row_mapping) let tasks_table = ManagarrTable::new(
app,
|app| Some(&mut app.data.sonarr_data.tasks),
tasks_row_mapping,
)
.block(title_block("Tasks")) .block(title_block("Tasks"))
.loading(app.is_loading)
.highlight_rows(false) .highlight_rows(false)
.headers(TASK_TABLE_HEADERS) .headers(TASK_TABLE_HEADERS)
.constraints(TASK_TABLE_CONSTRAINTS); .constraints(TASK_TABLE_CONSTRAINTS);
@@ -137,11 +140,11 @@ pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rec
.primary() .primary()
}; };
let events_table = ManagarrTable::new( let events_table = ManagarrTable::new(
Some(&mut app.data.sonarr_data.queued_events), app,
|app| Some(&mut app.data.sonarr_data.queued_events),
events_row_mapping, events_row_mapping,
) )
.block(title_block("Queued Events")) .block(title_block("Queued Events"))
.loading(app.is_loading)
.highlight_rows(false) .highlight_rows(false)
.headers(["Trigger", "Status", "Name", "Queued", "Started", "Duration"]) .headers(["Trigger", "Status", "Name", "Queued", "Started", "Duration"])
.constraints([ .constraints([
+16 -8
View File
@@ -92,8 +92,19 @@ fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let tasks_table = ManagarrTable::new(Some(&mut app.data.sonarr_data.tasks), tasks_row_mapping) let current_route = app.get_current_route();
.loading(app.is_loading) let prompt_confirm = app.data.sonarr_data.prompt_confirm;
let task_name = if app.data.sonarr_data.tasks.items.is_empty() {
String::new()
} else {
app.data.sonarr_data.tasks.current_selection().name.clone()
};
let tasks_table = ManagarrTable::new(
app,
|app| Some(&mut app.data.sonarr_data.tasks),
tasks_row_mapping,
)
.margin(1) .margin(1)
.headers(TASK_TABLE_HEADERS) .headers(TASK_TABLE_HEADERS)
.constraints(TASK_TABLE_CONSTRAINTS); .constraints(TASK_TABLE_CONSTRAINTS);
@@ -102,17 +113,14 @@ fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(tasks_table, area); f.render_widget(tasks_table, area);
if matches!( if matches!(
app.get_current_route(), current_route,
Route::Sonarr(ActiveSonarrBlock::SystemTaskStartConfirmPrompt, _) Route::Sonarr(ActiveSonarrBlock::SystemTaskStartConfirmPrompt, _)
) { ) {
let prompt = format!( let prompt = format!("Do you want to manually start this task: {}?", task_name);
"Do you want to manually start this task: {}?",
app.data.sonarr_data.tasks.current_selection().name
);
let confirmation_prompt = ConfirmationPrompt::new() let confirmation_prompt = ConfirmationPrompt::new()
.title("Start Task") .title("Start Task")
.prompt(&prompt) .prompt(&prompt)
.yes_no_value(app.data.sonarr_data.prompt_confirm); .yes_no_value(prompt_confirm);
f.render_widget( f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt), Popup::new(confirmation_prompt).size(Size::MediumPrompt),
+31 -4
View File
@@ -1,21 +1,33 @@
use crate::app::App;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use derive_setters::Setters;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::prelude::Text; use ratatui::prelude::Text;
use ratatui::style::Style;
use ratatui::widgets::{Block, Paragraph, Widget}; use ratatui::widgets::{Block, Paragraph, Widget};
use tachyonfx::pattern::SweepPattern;
use tachyonfx::fx;
#[cfg(test)] #[cfg(test)]
#[path = "loading_block_tests.rs"] #[path = "loading_block_tests.rs"]
mod loading_block_tests; mod loading_block_tests;
pub struct LoadingBlock<'a> { #[derive(Setters)]
pub struct LoadingBlock<'a, 'b> {
is_loading: bool, is_loading: bool,
block: Block<'a>, block: Block<'a>,
#[setters(strip_option)]
app: Option<&'a mut App<'b>>,
} }
impl<'a> LoadingBlock<'a> { impl<'a, 'b> LoadingBlock<'a, 'b> {
pub fn new(is_loading: bool, block: Block<'a>) -> Self { pub fn new(is_loading: bool, block: Block<'a>) -> Self {
Self { is_loading, block } Self {
is_loading,
block,
app: None,
}
} }
fn render_loading_block(self, area: Rect, buf: &mut Buffer) { fn render_loading_block(self, area: Rect, buf: &mut Buffer) {
@@ -27,10 +39,25 @@ impl<'a> LoadingBlock<'a> {
} else { } else {
self.block.render(area, buf); self.block.render(area, buf);
} }
if let Some(app) = self.app
&& !app.has_active_effect
{
let color = Style::new()
.failure()
.fg
.expect("primary fg color is unset");
let fx = fx::repeating(
fx::paint_fg(color, 1000)
.with_pattern(SweepPattern::left_to_right(10))
.with_area(area),
);
app.effects.add_effect(fx);
app.has_active_effect = true;
}
} }
} }
impl Widget for LoadingBlock<'_> { impl Widget for LoadingBlock<'_, '_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_loading_block(area, buf); self.render_loading_block(area, buf);
} }
+83 -42
View File
@@ -1,6 +1,7 @@
use super::input_box_popup::InputBoxPopup; use super::input_box_popup::InputBoxPopup;
use super::message::Message; use super::message::Message;
use super::popup::Size; use super::popup::Size;
use crate::app::App;
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
use crate::ui::HIGHLIGHT_SYMBOL; use crate::ui::HIGHLIGHT_SYMBOL;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
@@ -16,23 +17,28 @@ use ratatui::prelude::{Style, Stylize, Text};
use ratatui::widgets::{Block, ListItem, Row, StatefulWidget, Table, Widget, WidgetRef}; use ratatui::widgets::{Block, ListItem, Row, StatefulWidget, Table, Widget, WidgetRef};
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use tachyonfx::{Interpolation, fx};
#[cfg(test)] #[cfg(test)]
#[path = "managarr_table_tests.rs"] #[path = "managarr_table_tests.rs"]
mod managarr_table_tests; mod managarr_table_tests;
#[derive(Setters)] #[derive(Setters)]
pub struct ManagarrTable<'a, T, F> pub struct ManagarrTable<'a, 'b, T, F, G>
where where
F: Fn(&T) -> Row<'a>, F: Fn(&T) -> Row<'a>,
T: Clone + PartialEq + Eq + Debug, T: Clone + PartialEq + Eq + Debug,
G: for<'c> FnMut(&'c mut App<'b>) -> Option<&'c mut StatefulTable<T>>,
{ {
#[setters(strip_option)] #[setters(skip)]
content: Option<&'a mut StatefulTable<T>>, app: &'a mut App<'b>,
#[setters(skip)]
content_accessor: G,
#[setters(skip)] #[setters(skip)]
table_headers: Vec<String>, table_headers: Vec<String>,
#[setters(skip)] #[setters(skip)]
constraints: Vec<Constraint>, constraints: Vec<Constraint>,
#[setters(skip)]
row_mapper: F, row_mapper: F,
block: Block<'a>, block: Block<'a>,
margin: u16, margin: u16,
@@ -51,48 +57,71 @@ where
search_box_offset: usize, search_box_offset: usize,
filter_box_content_length: usize, filter_box_content_length: usize,
filter_box_offset: usize, filter_box_offset: usize,
#[setters(skip)]
sort_header_info: Option<(usize, bool)>,
} }
impl<'a, T, F> ManagarrTable<'a, T, F> impl<'a, 'b, T, F, G> ManagarrTable<'a, 'b, T, F, G>
where where
F: Fn(&T) -> Row<'a>, F: Fn(&T) -> Row<'a>,
T: Clone + PartialEq + Eq + Debug, T: Clone + PartialEq + Eq + Debug,
G: for<'c> FnMut(&'c mut App<'b>) -> Option<&'c mut StatefulTable<T>>,
{ {
pub fn new(content: Option<&'a mut StatefulTable<T>>, row_mapper: F) -> Self { pub fn new(app: &'a mut App<'b>, mut content_accessor: G, row_mapper: F) -> Self {
let mut managarr_table = Self { let is_loading = app.is_loading;
content: None,
// Extract values from content in a scoped block so the borrow ends
let (
search_box_content_length,
search_box_offset,
filter_box_content_length,
filter_box_offset,
sort_header_info,
) = {
if let Some(content) = content_accessor(&mut *app) {
let (scl, sbo) = if let Some(search) = content.search.as_ref() {
(search.text.len(), search.offset.load(Ordering::SeqCst))
} else {
(0, 0)
};
let (fcl, fbo) = if let Some(filter) = content.filter.as_ref() {
(filter.text.len(), filter.offset.load(Ordering::SeqCst))
} else {
(0, 0)
};
let sort_info = content.sort.as_ref().map(|sort_list| {
let idx = sort_list.state.selected().unwrap_or(0);
(idx, content.sort_asc)
});
(scl, sbo, fcl, fbo, sort_info)
} else {
(0, 0, 0, 0, None)
}
};
Self {
app,
content_accessor,
table_headers: Vec::new(), table_headers: Vec::new(),
constraints: Vec::new(), constraints: Vec::new(),
row_mapper, row_mapper,
block: borderless_block(), block: borderless_block(),
margin: 0, margin: 0,
is_loading: false, is_loading,
highlight_rows: true, highlight_rows: true,
is_sorting: false, is_sorting: false,
is_searching: false, is_searching: false,
search_produced_empty_results: false, search_produced_empty_results: false,
is_filtering: false, is_filtering: false,
filter_produced_empty_results: false, filter_produced_empty_results: false,
search_box_content_length: 0, search_box_content_length,
search_box_offset: 0, search_box_offset,
filter_box_content_length: 0, filter_box_content_length,
filter_box_offset: 0, filter_box_offset,
}; sort_header_info,
if let Some(content) = content.as_ref() {
if let Some(search) = content.search.as_ref() {
managarr_table.search_box_content_length = search.text.len();
managarr_table.search_box_offset = search.offset.load(Ordering::SeqCst);
} else if let Some(filter) = content.filter.as_ref() {
managarr_table.filter_box_content_length = filter.text.len();
managarr_table.filter_box_offset = filter.offset.load(Ordering::SeqCst);
} }
} }
managarr_table.content = content;
managarr_table
}
pub fn headers<I>(mut self, headers: I) -> Self pub fn headers<I>(mut self, headers: I) -> Self
where where
I: IntoIterator, I: IntoIterator,
@@ -111,9 +140,9 @@ where
self self
} }
fn render_table(self, area: Rect, buf: &mut Buffer) { fn render_table(mut self, area: Rect, buf: &mut Buffer) {
let table_headers = self.parse_headers(); let table_headers = self.parse_headers();
let table_area = { let content_area = {
let [content_area, _] = Layout::vertical([Constraint::Fill(1), Constraint::Fill(0)]) let [content_area, _] = Layout::vertical([Constraint::Fill(1), Constraint::Fill(0)])
.margin(self.margin) .margin(self.margin)
.areas(area); .areas(area);
@@ -122,7 +151,9 @@ where
}; };
let loading_block = LoadingBlock::new(self.is_loading, self.block.clone()); let loading_block = LoadingBlock::new(self.is_loading, self.block.clone());
if let Some(content) = self.content // Render table content in a scoped block so the borrow ends before we access effects
{
if let Some(content) = (self.content_accessor)(&mut *self.app)
&& !self.is_loading && !self.is_loading
{ {
let (table_contents, table_state) = if content.filtered_items.is_some() { let (table_contents, table_state) = if content.filtered_items.is_some() {
@@ -140,7 +171,7 @@ where
let mut table = Table::new(rows, &self.constraints) let mut table = Table::new(rows, &self.constraints)
.header(headers) .header(headers)
.block(self.block); .block(self.block.clone());
if self.highlight_rows { if self.highlight_rows {
table = table table = table
@@ -148,7 +179,7 @@ where
.highlight_symbol(HIGHLIGHT_SYMBOL); .highlight_symbol(HIGHLIGHT_SYMBOL);
} }
StatefulWidget::render(table, table_area, buf, table_state); StatefulWidget::render(table, content_area, buf, table_state);
if content.sort.is_some() && self.is_sorting { if content.sort.is_some() && self.is_sorting {
let selectable_list = SelectableList::new(content.sort.as_mut().unwrap(), |item| { let selectable_list = SelectableList::new(content.sort.as_mut().unwrap(), |item| {
@@ -156,7 +187,7 @@ where
}); });
Popup::new(selectable_list) Popup::new(selectable_list)
.dimensions(20, 50) .dimensions(20, 50)
.render(table_area, buf); .render(content_area, buf);
} }
if self.is_searching { if self.is_searching {
@@ -164,7 +195,7 @@ where
InputBoxPopup::new(&box_content.text) InputBoxPopup::new(&box_content.text)
.offset(box_content.offset.load(Ordering::SeqCst)) .offset(box_content.offset.load(Ordering::SeqCst))
.block(title_block_centered("Search")) .block(title_block_centered("Search"))
.render_ref(table_area, buf); .render_ref(content_area, buf);
} }
if self.is_filtering { if self.is_filtering {
@@ -172,37 +203,46 @@ where
InputBoxPopup::new(&box_content.text) InputBoxPopup::new(&box_content.text)
.offset(box_content.offset.load(Ordering::SeqCst)) .offset(box_content.offset.load(Ordering::SeqCst))
.block(title_block_centered("Filter")) .block(title_block_centered("Filter"))
.render_ref(table_area, buf); .render_ref(content_area, buf);
} }
if self.search_produced_empty_results { if self.search_produced_empty_results {
Popup::new(Message::new("No items found matching search")) Popup::new(Message::new("No items found matching search"))
.size(Size::Message) .size(Size::Message)
.render(table_area, buf); .render(content_area, buf);
} }
if self.filter_produced_empty_results { if self.filter_produced_empty_results {
Popup::new(Message::new("The given filter produced empty results")) Popup::new(Message::new("The given filter produced empty results"))
.size(Size::Message) .size(Size::Message)
.render(table_area, buf); .render(content_area, buf);
} }
} else { } else {
loading_block.render(table_area, buf); loading_block.render(content_area, buf);
} }
} else { } else {
loading_block.render(table_area, buf); loading_block.render(content_area, buf);
}
}
// Now the content borrow has ended, we can access app for effects
if !self.app.has_active_effect {
let timer = (100, Interpolation::Linear);
let fx = fx::coalesce(timer).with_area(content_area);
self.app.effects.add_effect(fx);
self.app.has_active_effect = true;
} }
} }
fn parse_headers(&self) -> Vec<Text<'a>> { fn parse_headers(&self) -> Vec<Text<'a>> {
if let Some(ref content) = self.content if let Some((idx, sort_asc)) = self.sort_header_info
&& let Some(ref sort_list) = content.sort
&& !self.is_sorting && !self.is_sorting
{ {
let mut new_headers = self.table_headers.clone(); let mut new_headers = self.table_headers.clone();
let idx = sort_list.state.selected().unwrap_or(0); let direction = if sort_asc { "" } else { "" };
let direction = if content.sort_asc { "" } else { "" }; if idx < new_headers.len() {
new_headers[idx].push_str(direction); new_headers[idx].push_str(direction);
}
return new_headers.into_iter().map(Text::from).collect(); return new_headers.into_iter().map(Text::from).collect();
} }
@@ -238,10 +278,11 @@ where
} }
} }
impl<'a, T, F> Widget for ManagarrTable<'a, T, F> impl<'a, 'b, T, F, G> Widget for ManagarrTable<'a, 'b, T, F, G>
where where
F: Fn(&T) -> Row<'a>, F: Fn(&T) -> Row<'a>,
T: Clone + PartialEq + Eq + Debug, T: Clone + PartialEq + Eq + Debug,
G: for<'c> FnMut(&'c mut App<'b>) -> Option<&'c mut StatefulTable<T>>,
{ {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_table(area, buf); self.render_table(area, buf);
+84 -49
View File
@@ -1,7 +1,8 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::app::App;
use crate::models::stateful_list::StatefulList; use crate::models::stateful_list::StatefulList;
use crate::models::stateful_table::{SortOption, StatefulTable}; use crate::models::stateful_table::SortOption;
use crate::models::{HorizontallyScrollableText, Scrollable}; use crate::models::{HorizontallyScrollableText, Scrollable};
use crate::ui::utils::borderless_block; use crate::ui::utils::borderless_block;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
@@ -14,15 +15,19 @@ mod tests {
#[test] #[test]
fn test_managarr_table_new() { fn test_managarr_table_new() {
let items = vec!["item1", "item2", "item3"]; let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default(); let mut app = App::test_default();
stateful_table.set_items(items.clone()); app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| {
let mut movie = crate::models::radarr_models::Movie::default();
movie.title = crate::models::HorizontallyScrollableText::from(s);
movie
}).collect());
let managarr_table = let managarr_table = ManagarrTable::new(
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])); &mut app,
|app| Some(&mut app.data.radarr_data.movies),
|movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]),
);
let row_mapper = managarr_table.row_mapper;
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new()); assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new()); assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.block, borderless_block()); assert_eq!(managarr_table.block, borderless_block());
@@ -43,20 +48,24 @@ mod tests {
#[test] #[test]
fn test_managarr_table_new_search_box_populated() { fn test_managarr_table_new_search_box_populated() {
let items = vec!["item1", "item2", "item3"]; let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default(); let mut app = App::test_default();
stateful_table.set_items(items.clone()); app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| {
let mut movie = crate::models::radarr_models::Movie::default();
movie.title = crate::models::HorizontallyScrollableText::from(s);
movie
}).collect());
let horizontally_scrollable_test = HorizontallyScrollableText { let horizontally_scrollable_test = HorizontallyScrollableText {
text: "test".to_owned(), text: "test".to_owned(),
offset: AtomicUsize::new(3), offset: AtomicUsize::new(3),
}; };
stateful_table.search = Some(horizontally_scrollable_test); app.data.radarr_data.movies.search = Some(horizontally_scrollable_test);
let managarr_table = let managarr_table = ManagarrTable::new(
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])); &mut app,
|app| Some(&mut app.data.radarr_data.movies),
|movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]),
);
let row_mapper = managarr_table.row_mapper;
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new()); assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new()); assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.block, borderless_block()); assert_eq!(managarr_table.block, borderless_block());
@@ -77,20 +86,24 @@ mod tests {
#[test] #[test]
fn test_managarr_table_new_filter_box_populated() { fn test_managarr_table_new_filter_box_populated() {
let items = vec!["item1", "item2", "item3"]; let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default(); let mut app = App::test_default();
stateful_table.set_items(items.clone()); app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| {
let mut movie = crate::models::radarr_models::Movie::default();
movie.title = crate::models::HorizontallyScrollableText::from(s);
movie
}).collect());
let horizontally_scrollable_test = HorizontallyScrollableText { let horizontally_scrollable_test = HorizontallyScrollableText {
text: "test".to_owned(), text: "test".to_owned(),
offset: AtomicUsize::new(3), offset: AtomicUsize::new(3),
}; };
stateful_table.filter = Some(horizontally_scrollable_test); app.data.radarr_data.movies.filter = Some(horizontally_scrollable_test);
let managarr_table = let managarr_table = ManagarrTable::new(
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])); &mut app,
|app| Some(&mut app.data.radarr_data.movies),
|movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]),
);
let row_mapper = managarr_table.row_mapper;
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new()); assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new()); assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.block, borderless_block()); assert_eq!(managarr_table.block, borderless_block());
@@ -111,18 +124,22 @@ mod tests {
#[test] #[test]
fn test_managarr_table_headers() { fn test_managarr_table_headers() {
let items = vec!["item1", "item2", "item3"]; let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default(); let mut app = App::test_default();
stateful_table.set_items(items.clone()); app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| {
let mut movie = crate::models::radarr_models::Movie::default();
movie.title = crate::models::HorizontallyScrollableText::from(s);
movie
}).collect());
let headers = ["column 1", "column 2"]; let headers = ["column 1", "column 2"];
let managarr_table = let managarr_table = ManagarrTable::new(
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) &mut app,
|app| Some(&mut app.data.radarr_data.movies),
|movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]),
)
.headers(headers); .headers(headers);
let row_mapper = managarr_table.row_mapper;
assert_eq!(managarr_table.table_headers, headers); assert_eq!(managarr_table.table_headers, headers);
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.constraints, Vec::new()); assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.block, borderless_block()); assert_eq!(managarr_table.block, borderless_block());
assert_eq!(managarr_table.margin, 0); assert_eq!(managarr_table.margin, 0);
@@ -142,18 +159,22 @@ mod tests {
#[test] #[test]
fn test_managarr_table_constraints() { fn test_managarr_table_constraints() {
let items = vec!["item1", "item2", "item3"]; let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default(); let mut app = App::test_default();
stateful_table.set_items(items.clone()); app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| {
let mut movie = crate::models::radarr_models::Movie::default();
movie.title = crate::models::HorizontallyScrollableText::from(s);
movie
}).collect());
let constraints = [Constraint::Length(1), Constraint::Fill(1)]; let constraints = [Constraint::Length(1), Constraint::Fill(1)];
let managarr_table = let managarr_table = ManagarrTable::new(
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) &mut app,
|app| Some(&mut app.data.radarr_data.movies),
|movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]),
)
.constraints(constraints); .constraints(constraints);
let row_mapper = managarr_table.row_mapper;
assert_eq!(managarr_table.constraints, constraints); assert_eq!(managarr_table.constraints, constraints);
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new()); assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.block, borderless_block()); assert_eq!(managarr_table.block, borderless_block());
assert_eq!(managarr_table.margin, 0); assert_eq!(managarr_table.margin, 0);
@@ -185,13 +206,20 @@ mod tests {
}, },
]); ]);
sort_list.scroll_down(); sort_list.scroll_down();
let mut stateful_table = StatefulTable::default(); let mut app = App::test_default();
stateful_table.set_items(items.clone()); app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| {
stateful_table.sort = Some(sort_list); let mut movie = crate::models::radarr_models::Movie::default();
movie.title = crate::models::HorizontallyScrollableText::from(s);
movie
}).collect());
app.data.radarr_data.movies.sort = Some(sort_list);
let headers = ["column 1", "column 2"]; let headers = ["column 1", "column 2"];
let managarr_table = let managarr_table = ManagarrTable::new(
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) &mut app,
|app| Some(&mut app.data.radarr_data.movies),
|movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]),
)
.headers(headers); .headers(headers);
assert_eq!( assert_eq!(
@@ -215,14 +243,21 @@ mod tests {
}, },
]); ]);
sort_list.scroll_down(); sort_list.scroll_down();
let mut stateful_table = StatefulTable::default(); let mut app = App::test_default();
stateful_table.set_items(items.clone()); app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| {
stateful_table.sort = Some(sort_list); let mut movie = crate::models::radarr_models::Movie::default();
stateful_table.sort_asc = true; movie.title = crate::models::HorizontallyScrollableText::from(s);
movie
}).collect());
app.data.radarr_data.movies.sort = Some(sort_list);
app.data.radarr_data.movies.sort_asc = true;
let headers = ["column 1", "column 2"]; let headers = ["column 1", "column 2"];
let managarr_table = let managarr_table = ManagarrTable::new(
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) &mut app,
|app| Some(&mut app.data.radarr_data.movies),
|movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]),
)
.headers(headers); .headers(headers);
assert_eq!( assert_eq!(
+25 -13
View File
@@ -1,7 +1,10 @@
use crate::app::App;
use crate::ui::utils::{background_block, centered_rect}; use crate::ui::utils::{background_block, centered_rect};
use derive_setters::Setters;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::widgets::{Block, Clear, Widget}; use ratatui::widgets::{Block, Clear, Widget};
use tachyonfx::{Interpolation, fx};
#[cfg(test)] #[cfg(test)]
#[path = "popup_tests.rs"] #[path = "popup_tests.rs"]
@@ -49,15 +52,25 @@ impl Size {
} }
} }
pub struct Popup<'a, T: Widget> { #[derive(Setters)]
pub struct Popup<'a, 'b, T: Widget> {
#[setters(skip)]
widget: T, widget: T,
margin: u16, margin: u16,
#[setters(skip)]
percent_x: u16, percent_x: u16,
#[setters(skip)]
percent_y: u16, percent_y: u16,
#[setters(strip_option)]
block: Option<Block<'a>>, block: Option<Block<'a>>,
#[setters(strip_option)]
app: Option<&'a mut App<'b>>,
} }
impl<'a, T: Widget> Popup<'a, T> { impl<T> Popup<'_, '_, T>
where
T: Widget,
{
pub fn new(widget: T) -> Self { pub fn new(widget: T) -> Self {
Self { Self {
widget, widget,
@@ -65,6 +78,7 @@ impl<'a, T: Widget> Popup<'a, T> {
percent_y: 0, percent_y: 0,
margin: 0, margin: 0,
block: None, block: None,
app: None,
} }
} }
@@ -81,16 +95,6 @@ impl<'a, T: Widget> Popup<'a, T> {
self self
} }
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn margin(mut self, margin: u16) -> Self {
self.margin = margin;
self
}
fn render_popup(self, area: Rect, buf: &mut Buffer) { fn render_popup(self, area: Rect, buf: &mut Buffer) {
let mut popup_area = centered_rect(self.percent_x, self.percent_y, area); let mut popup_area = centered_rect(self.percent_x, self.percent_y, area);
let height = if popup_area.height < 3 { let height = if popup_area.height < 3 {
@@ -114,10 +118,18 @@ impl<'a, T: Widget> Popup<'a, T> {
.areas(popup_area); .areas(popup_area);
self.widget.render(content_area, buf); self.widget.render(content_area, buf);
if let Some(app) = self.app
&& !app.has_active_effect
{
let timer = (100, Interpolation::Linear);
let fx = fx::coalesce(timer).with_area(content_area);
app.effects.add_effect(fx);
app.has_active_effect = true;
}
} }
} }
impl<T: Widget> Widget for Popup<'_, T> { impl<T: Widget> Widget for Popup<'_, '_, T> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_popup(area, buf); self.render_popup(area, buf);
} }