feat: Improved overall UI responsiveness

This commit is contained in:
2025-12-18 16:19:03 -07:00
parent 6a9fd0999c
commit 051f30b097
21 changed files with 161 additions and 145 deletions
+24 -2
View File
@@ -7,12 +7,12 @@ mod tests {
use serial_test::serial; use serial_test::serial;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars}; use crate::app::{interpolate_env_vars, App, AppConfig, Data, ServarrConfig};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
use crate::models::{HorizontallyScrollableText, TabRoute}; use crate::models::{HorizontallyScrollableText, TabRoute};
use crate::network::NetworkEvent;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::network::NetworkEvent;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
#[test] #[test]
@@ -80,6 +80,7 @@ mod tests {
assert_eq!(app.tick_until_poll, 400); assert_eq!(app.tick_until_poll, 400);
assert_eq!(app.ticks_until_scroll, 4); assert_eq!(app.ticks_until_scroll, 4);
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
assert_eq!(app.ui_scroll_tick_count, 0);
assert!(!app.is_loading); assert!(!app.is_loading);
assert!(!app.is_routing); assert!(!app.is_routing);
assert!(!app.should_refresh); assert!(!app.should_refresh);
@@ -240,6 +241,27 @@ mod tests {
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
} }
#[test]
fn test_on_ui_scroll_tick() {
let mut app = App {
ticks_until_scroll: 1,
..App::default()
};
assert_eq!(app.ui_scroll_tick_count, 0);
assert_eq!(app.tick_count, 0);
app.on_ui_scroll_tick();
assert_eq!(app.ui_scroll_tick_count, 1);
assert_eq!(app.tick_count, 0);
app.on_ui_scroll_tick();
assert_eq!(app.ui_scroll_tick_count, 0);
assert_eq!(app.tick_count, 0);
}
#[tokio::test] #[tokio::test]
async fn test_on_tick_first_render() { async fn test_on_tick_first_render() {
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500); let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
+10
View File
@@ -39,6 +39,7 @@ pub struct App<'a> {
pub tick_until_poll: u64, pub tick_until_poll: u64,
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 is_routing: bool, pub is_routing: bool,
pub is_loading: bool, pub is_loading: bool,
pub should_refresh: bool, pub should_refresh: bool,
@@ -145,6 +146,14 @@ impl App<'_> {
self.tick_count = 0; self.tick_count = 0;
} }
pub fn on_ui_scroll_tick(&mut self) {
if self.ui_scroll_tick_count == self.ticks_until_scroll {
self.ui_scroll_tick_count = 0;
} else {
self.ui_scroll_tick_count += 1;
}
}
#[allow(dead_code)] #[allow(dead_code)]
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.reset_tick_count(); self.reset_tick_count();
@@ -227,6 +236,7 @@ impl Default for App<'_> {
tick_until_poll: 400, tick_until_poll: 400,
ticks_until_scroll: 4, ticks_until_scroll: 4,
tick_count: 0, tick_count: 0,
ui_scroll_tick_count: 0,
is_loading: false, is_loading: false,
is_routing: false, is_routing: false,
should_refresh: false, should_refresh: false,
+6 -2
View File
@@ -1,3 +1,4 @@
use anyhow::Result;
use std::sync::mpsc; use std::sync::mpsc;
use std::sync::mpsc::Receiver; use std::sync::mpsc::Receiver;
use std::thread; use std::thread;
@@ -49,7 +50,10 @@ impl Events {
Events { rx } Events { rx }
} }
pub fn next(&self) -> Result<InputEvent<Key>, mpsc::RecvError> { pub fn next(&self) -> Result<Option<InputEvent<Key>>> {
self.rx.recv() match self.rx.try_recv() {
Ok(event) => Ok(Some(event)),
_ => Ok(None),
}
} }
} }
+15 -10
View File
@@ -3,37 +3,39 @@
extern crate assertables; extern crate assertables;
use anyhow::Result; use anyhow::Result;
use clap::{CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version}; use clap::{crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser};
use clap_complete::generate; use clap_complete::generate;
use crossterm::execute; use crossterm::execute;
use crossterm::terminal::{ use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
}; };
use log::{debug, error, warn}; use log::{debug, error, warn};
use network::NetworkTrait; use network::NetworkTrait;
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend; use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use reqwest::Client; use reqwest::Client;
use std::panic::PanicHookInfo; use std::panic::PanicHookInfo;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::time::sleep;
use std::time::{Duration, 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;
use tokio::sync::{Mutex, mpsc}; use tokio::sync::{mpsc, Mutex};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use utils::{ use utils::{
build_network_client, load_config, start_cli_no_spinner, start_cli_with_spinner, tail_logs, build_network_client, load_config, start_cli_no_spinner, start_cli_with_spinner, tail_logs,
}; };
use crate::app::{App, log_and_print_error}; use crate::app::{log_and_print_error, App};
use crate::cli::Command; use crate::cli::Command;
use crate::event::Key;
use crate::event::input_event::{Events, InputEvent}; use crate::event::input_event::{Events, InputEvent};
use crate::event::Key;
use crate::network::{Network, NetworkEvent}; use crate::network::{Network, NetworkEvent};
use crate::ui::theme::{Theme, ThemeDefinitionsWrapper}; use crate::ui::theme::{Theme, ThemeDefinitionsWrapper};
use crate::ui::{THEME, ui}; use crate::ui::{ui, THEME};
use crate::utils::load_theme_config; use crate::utils::load_theme_config;
mod app; mod app;
@@ -249,7 +251,7 @@ async fn start_ui(
terminal.draw(|f| ui(f, &mut app))?; terminal.draw(|f| ui(f, &mut app))?;
match input_events.next()? { match input_events.next()? {
InputEvent::KeyEvent(key) => { Some(InputEvent::KeyEvent(key)) => {
if key == Key::Char('q') && !app.ignore_special_keys_for_textbox_input { if key == Key::Char('q') && !app.ignore_special_keys_for_textbox_input {
break; break;
} }
@@ -257,8 +259,11 @@ async fn start_ui(
handlers::handle_events(key, &mut app); handlers::handle_events(key, &mut app);
} }
InputEvent::Tick => app.on_tick().await, Some(InputEvent::Tick) => app.on_tick().await,
_ => {}
} }
let _ = sleep(Duration::from_millis(16)).await;
} }
terminal.show_cursor()?; terminal.show_cursor()?;
+5 -6
View File
@@ -1,7 +1,6 @@
use std::cell::Cell; use std::cell::Cell;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use ratatui::Frame;
use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::style::{Style, Stylize}; use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Text}; use ratatui::text::{Line, Text};
@@ -9,6 +8,7 @@ use ratatui::widgets::Paragraph;
use ratatui::widgets::Tabs; use ratatui::widgets::Tabs;
use ratatui::widgets::Wrap; use ratatui::widgets::Wrap;
use ratatui::widgets::{Clear, Row}; use ratatui::widgets::{Clear, Row};
use ratatui::Frame;
use sonarr_ui::SonarrUi; use sonarr_ui::SonarrUi;
use utils::layout_block; use utils::layout_block;
@@ -52,6 +52,7 @@ pub trait DrawUi {
} }
pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) { pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) {
app.on_ui_scroll_tick();
f.render_widget(background_block(), f.area()); f.render_widget(background_block(), f.area());
let [header_area, context_area, table_area] = if !app.error.text.is_empty() { let [header_area, context_area, table_area] = if !app.error.text.is_empty() {
let [header_area, error_area, context_area, table_area] = Layout::vertical([ let [header_area, error_area, context_area, table_area] = Layout::vertical([
@@ -124,11 +125,9 @@ fn draw_error(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.failure() .failure()
.bold(); .bold();
app.error.scroll_left_or_reset( app
area.width as usize, .error
true, .scroll_left_or_reset(area.width as usize, true, app.ui_scroll_tick_count == 0);
app.tick_count.is_multiple_of(app.ticks_until_scroll),
);
let paragraph = Paragraph::new(Text::from(app.error.to_string().failure())) let paragraph = Paragraph::new(Text::from(app.error.to_string().failure()))
.block(block) .block(block)
+4 -4
View File
@@ -1,19 +1,19 @@
use crate::app::App; use crate::app::App;
use crate::models::Route;
use crate::models::radarr_models::BlocklistItem; use crate::models::radarr_models::BlocklistItem;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::ui::DrawUi; use crate::models::Route;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::message::Message; use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use ratatui::Frame; use crate::ui::DrawUi;
use ratatui::layout::{Alignment, Constraint, Rect}; use ratatui::layout::{Alignment, Constraint, Rect};
use ratatui::style::{Style, Stylize}; use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Text}; use ratatui::text::{Line, Text};
use ratatui::widgets::{Cell, Row}; use ratatui::widgets::{Cell, Row};
use ratatui::Frame;
#[cfg(test)] #[cfg(test)]
#[path = "blocklist_ui_tests.rs"] #[path = "blocklist_ui_tests.rs"]
@@ -96,7 +96,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
let languages_string = languages let languages_string = languages
@@ -1,15 +1,15 @@
use ratatui::Frame;
use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::style::Stylize; use ratatui::style::Stylize;
use ratatui::text::{Line, Text}; use ratatui::text::{Line, Text};
use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
use ratatui::Frame;
use crate::app::App; use crate::app::App;
use crate::models::Route;
use crate::models::radarr_models::CollectionMovie; use crate::models::radarr_models::CollectionMovie;
use crate::models::servarr_data::radarr::radarr_data::{ use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS, ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
}; };
use crate::models::Route;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{ use crate::ui::utils::{
borderless_block, get_width_from_percentage, layout_block_top_border_with_title, title_block, borderless_block, get_width_from_percentage, layout_block_top_border_with_title, title_block,
@@ -17,7 +17,7 @@ use crate::ui::utils::{
}; };
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::Size; use crate::ui::widgets::popup::Size;
use crate::ui::{DrawUi, draw_popup}; use crate::ui::{draw_popup, DrawUi};
use crate::utils::convert_runtime; use crate::utils::convert_runtime;
#[cfg(test)] #[cfg(test)]
@@ -90,7 +90,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.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
+4 -4
View File
@@ -1,12 +1,11 @@
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect}; use ratatui::layout::{Constraint, Rect};
use ratatui::widgets::{Cell, Row}; use ratatui::widgets::{Cell, Row};
use ratatui::Frame;
use crate::app::App; use crate::app::App;
use crate::models::Route;
use crate::models::radarr_models::Collection; use crate::models::radarr_models::Collection;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, COLLECTIONS_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, COLLECTIONS_BLOCKS};
use crate::ui::DrawUi; use crate::models::Route;
use crate::ui::radarr_ui::collections::collection_details_ui::CollectionDetailsUi; use crate::ui::radarr_ui::collections::collection_details_ui::CollectionDetailsUi;
use crate::ui::radarr_ui::collections::edit_collection_ui::EditCollectionUi; use crate::ui::radarr_ui::collections::edit_collection_ui::EditCollectionUi;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
@@ -14,6 +13,7 @@ use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::DrawUi;
mod collection_details_ui; mod collection_details_ui;
#[cfg(test)] #[cfg(test)]
@@ -70,7 +70,7 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
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.tick_count.is_multiple_of(app.ticks_until_scroll), app.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 {
+3 -3
View File
@@ -1,17 +1,17 @@
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect}; use ratatui::layout::{Constraint, Rect};
use ratatui::widgets::{Cell, Row}; use ratatui::widgets::{Cell, Row};
use ratatui::Frame;
use crate::app::App; use crate::app::App;
use crate::models::radarr_models::DownloadRecord; use crate::models::radarr_models::DownloadRecord;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
use crate::models::{HorizontallyScrollableText, Route}; use crate::models::{HorizontallyScrollableText, Route};
use crate::ui::DrawUi;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::DrawUi;
use crate::utils::convert_to_gb; use crate::utils::convert_to_gb;
#[cfg(test)] #[cfg(test)]
@@ -87,7 +87,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
} }
@@ -1,15 +1,15 @@
use crate::app::App; use crate::app::App;
use crate::models::Route;
use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::Route;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{get_width_from_percentage, title_block}; use crate::ui::utils::{get_width_from_percentage, title_block};
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::Size; use crate::ui::widgets::popup::Size;
use crate::ui::{DrawUi, draw_popup}; use crate::ui::{draw_popup, DrawUi};
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect}; use ratatui::layout::{Constraint, Rect};
use ratatui::widgets::{Cell, Row}; use ratatui::widgets::{Cell, Row};
use ratatui::Frame;
#[cfg(test)] #[cfg(test)]
#[path = "test_all_indexers_ui_tests.rs"] #[path = "test_all_indexers_ui_tests.rs"]
@@ -45,7 +45,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.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![
+8 -16
View File
@@ -1,13 +1,13 @@
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::widgets::{Cell, ListItem, Row}; use ratatui::widgets::{Cell, ListItem, Row};
use ratatui::Frame;
use crate::models::Route;
use crate::models::radarr_models::AddMovieSearchResult; use crate::models::radarr_models::AddMovieSearchResult;
use crate::models::servarr_data::radarr::modals::AddMovieModal; use crate::models::servarr_data::radarr::modals::AddMovieModal;
use crate::models::servarr_data::radarr::radarr_data::{ADD_MOVIE_BLOCKS, ActiveRadarrBlock}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS};
use crate::models::Route;
use crate::ui::radarr_ui::collections::CollectionsUi; use crate::ui::radarr_ui::collections::CollectionsUi;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{ use crate::ui::utils::{
@@ -19,9 +19,9 @@ use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::message::Message; use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::widgets::selectable_list::SelectableList; use crate::ui::widgets::selectable_list::SelectableList;
use crate::ui::{DrawUi, draw_popup}; use crate::ui::{draw_popup, DrawUi};
use crate::utils::convert_runtime; use crate::utils::convert_runtime;
use crate::{App, render_selectable_input_box}; use crate::{render_selectable_input_box, App};
#[cfg(test)] #[cfg(test)]
#[path = "add_movie_ui_tests.rs"] #[path = "add_movie_ui_tests.rs"]
@@ -139,7 +139,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -307,16 +307,8 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(title_block_centered(&title), area); f.render_widget(title_block_centered(&title), area);
let [ let [paragraph_area, root_folder_area, monitor_area, min_availability_area, quality_profile_area, tags_area, _, buttons_area] =
paragraph_area, Layout::vertical([
root_folder_area,
monitor_area,
min_availability_area,
quality_profile_area,
tags_area,
_,
buttons_area,
] = Layout::vertical([
Constraint::Length(6), Constraint::Length(6),
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(3), Constraint::Length(3),
+4 -4
View File
@@ -1,12 +1,11 @@
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect}; use ratatui::layout::{Constraint, Rect};
use ratatui::widgets::{Cell, Row}; use ratatui::widgets::{Cell, Row};
use ratatui::Frame;
use crate::app::App; use crate::app::App;
use crate::models::Route;
use crate::models::radarr_models::Movie; use crate::models::radarr_models::Movie;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, LIBRARY_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, LIBRARY_BLOCKS};
use crate::ui::DrawUi; use crate::models::Route;
use crate::ui::radarr_ui::decorate_with_row_style; use crate::ui::radarr_ui::decorate_with_row_style;
use crate::ui::radarr_ui::library::add_movie_ui::AddMovieUi; use crate::ui::radarr_ui::library::add_movie_ui::AddMovieUi;
use crate::ui::radarr_ui::library::delete_movie_ui::DeleteMovieUi; use crate::ui::radarr_ui::library::delete_movie_ui::DeleteMovieUi;
@@ -16,6 +15,7 @@ use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::DrawUi;
use crate::utils::{convert_runtime, convert_to_gb}; use crate::utils::{convert_runtime, convert_to_gb};
mod add_movie_ui; mod add_movie_ui;
@@ -90,7 +90,7 @@ fn draw_library(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.tick_count.is_multiple_of(app.ticks_until_scroll), app.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();
+7 -13
View File
@@ -1,17 +1,17 @@
use std::iter; use std::iter;
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Style, Stylize}; use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Span, Text}; use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
use ratatui::Frame;
use serde_json::Number; use serde_json::Number;
use crate::app::App; use crate::app::App;
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::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::models::Route;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{ use crate::ui::utils::{
borderless_block, decorate_peer_style, get_width_from_percentage, layout_block_bottom_border, borderless_block, decorate_peer_style, get_width_from_percentage, layout_block_bottom_border,
@@ -21,7 +21,7 @@ use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::{DrawUi, draw_popup, draw_tabs}; use crate::ui::{draw_popup, draw_tabs, DrawUi};
use crate::utils::convert_to_gb; use crate::utils::convert_to_gb;
#[cfg(test)] #[cfg(test)]
@@ -116,14 +116,8 @@ fn draw_file_info(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
let file_info = movie_details_modal.file_details.to_owned(); let file_info = movie_details_modal.file_details.to_owned();
let audio_details = movie_details_modal.audio_details.to_owned(); let audio_details = movie_details_modal.audio_details.to_owned();
let video_details = movie_details_modal.video_details.to_owned(); let video_details = movie_details_modal.video_details.to_owned();
let [ let [file_details_title_area, file_details_area, audio_details_title_area, audio_details_area, video_details_title_area, video_details_area] =
file_details_title_area, Layout::vertical([
file_details_area,
audio_details_title_area,
audio_details_area,
video_details_title_area,
video_details_area,
] = Layout::vertical([
Constraint::Length(2), Constraint::Length(2),
Constraint::Length(5), Constraint::Length(5),
Constraint::Length(1), Constraint::Length(1),
@@ -246,7 +240,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -398,7 +392,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.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 { "" };
+3 -3
View File
@@ -1,17 +1,17 @@
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect}; use ratatui::layout::{Constraint, Rect};
use ratatui::widgets::{Cell, Row}; use ratatui::widgets::{Cell, Row};
use ratatui::Frame;
use crate::app::App; use crate::app::App;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS};
use crate::models::sonarr_models::DownloadRecord; use crate::models::sonarr_models::DownloadRecord;
use crate::models::{HorizontallyScrollableText, Route}; use crate::models::{HorizontallyScrollableText, Route};
use crate::ui::DrawUi;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::DrawUi;
use crate::utils::convert_f64_to_gb; use crate::utils::convert_f64_to_gb;
#[cfg(test)] #[cfg(test)]
@@ -88,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
} }
+4 -4
View File
@@ -1,19 +1,19 @@
use crate::app::App; use crate::app::App;
use crate::models::Route;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS};
use crate::models::servarr_models::Language; use crate::models::servarr_models::Language;
use crate::models::sonarr_models::{SonarrHistoryEventType, SonarrHistoryItem}; use crate::models::sonarr_models::{SonarrHistoryEventType, SonarrHistoryItem};
use crate::ui::DrawUi; use crate::models::Route;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::message::Message; use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use ratatui::Frame; use crate::ui::DrawUi;
use ratatui::layout::{Alignment, Constraint, Rect}; use ratatui::layout::{Alignment, Constraint, Rect};
use ratatui::style::Style; use ratatui::style::Style;
use ratatui::text::Text; use ratatui::text::Text;
use ratatui::widgets::{Cell, Row}; use ratatui::widgets::{Cell, Row};
use ratatui::Frame;
use super::sonarr_ui_utils::{ use super::sonarr_ui_utils::{
create_download_failed_history_event_details, create_download_failed_history_event_details,
@@ -69,7 +69,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -1,15 +1,15 @@
use crate::app::App; use crate::app::App;
use crate::models::Route;
use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::Route;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{get_width_from_percentage, title_block}; use crate::ui::utils::{get_width_from_percentage, title_block};
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::Size; use crate::ui::widgets::popup::Size;
use crate::ui::{DrawUi, draw_popup}; use crate::ui::{draw_popup, DrawUi};
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect}; use ratatui::layout::{Constraint, Rect};
use ratatui::widgets::{Cell, Row}; use ratatui::widgets::{Cell, Row};
use ratatui::Frame;
#[cfg(test)] #[cfg(test)]
#[path = "test_all_indexers_ui_tests.rs"] #[path = "test_all_indexers_ui_tests.rs"]
@@ -44,7 +44,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.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![
+8 -18
View File
@@ -1,13 +1,13 @@
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::widgets::{Cell, ListItem, Row}; use ratatui::widgets::{Cell, ListItem, Row};
use ratatui::Frame;
use crate::models::Route;
use crate::models::servarr_data::sonarr::modals::AddSeriesModal; use crate::models::servarr_data::sonarr::modals::AddSeriesModal;
use crate::models::servarr_data::sonarr::sonarr_data::{ADD_SERIES_BLOCKS, ActiveSonarrBlock}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS};
use crate::models::sonarr_models::AddSeriesSearchResult; use crate::models::sonarr_models::AddSeriesSearchResult;
use crate::models::Route;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{ use crate::ui::utils::{
get_width_from_percentage, layout_block, layout_paragraph_borderless, title_block_centered, get_width_from_percentage, layout_block, layout_paragraph_borderless, title_block_centered,
@@ -19,8 +19,8 @@ use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::message::Message; use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::widgets::selectable_list::SelectableList; use crate::ui::widgets::selectable_list::SelectableList;
use crate::ui::{DrawUi, draw_popup}; use crate::ui::{draw_popup, DrawUi};
use crate::{App, render_selectable_input_box}; use crate::{render_selectable_input_box, App};
#[cfg(test)] #[cfg(test)]
#[path = "add_series_ui_tests.rs"] #[path = "add_series_ui_tests.rs"]
@@ -119,7 +119,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -276,18 +276,8 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(title_block_centered(&title), area); f.render_widget(title_block_centered(&title), area);
let [ let [paragraph_area, root_folder_area, monitor_area, quality_profile_area, language_profile_area, series_type_area, season_folder_area, tags_area, _, buttons_area] =
paragraph_area, Layout::vertical([
root_folder_area,
monitor_area,
quality_profile_area,
language_profile_area,
series_type_area,
season_folder_area,
tags_area,
_,
buttons_area,
] = Layout::vertical([
Constraint::Length(6), Constraint::Length(6),
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(3), Constraint::Length(3),
@@ -281,7 +281,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -431,7 +431,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.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 { "" };
+4 -4
View File
@@ -2,9 +2,9 @@ use add_series_ui::AddSeriesUi;
use delete_series_ui::DeleteSeriesUi; use delete_series_ui::DeleteSeriesUi;
use edit_series_ui::EditSeriesUi; use edit_series_ui::EditSeriesUi;
use ratatui::{ use ratatui::{
Frame,
layout::{Constraint, Rect}, layout::{Constraint, Rect},
widgets::{Cell, Row}, widgets::{Cell, Row},
Frame,
}; };
use series_details_ui::SeriesDetailsUi; use series_details_ui::SeriesDetailsUi;
@@ -16,15 +16,15 @@ use crate::utils::convert_to_gb;
use crate::{ use crate::{
app::App, app::App,
models::{ models::{
Route,
servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, LIBRARY_BLOCKS}, servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, LIBRARY_BLOCKS},
sonarr_models::{Series, SeriesStatus}, sonarr_models::{Series, SeriesStatus},
Route,
}, },
ui::{ ui::{
DrawUi,
styles::ManagarrStyle, styles::ManagarrStyle,
utils::{get_width_from_percentage, layout_block_top_border}, utils::{get_width_from_percentage, layout_block_top_border},
widgets::managarr_table::ManagarrTable, widgets::managarr_table::ManagarrTable,
DrawUi,
}, },
}; };
@@ -95,7 +95,7 @@ fn draw_library(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, 23), get_width_from_percentage(area, 23),
*series == current_selection, *series == current_selection,
app.tick_count.is_multiple_of(app.ticks_until_scroll), app.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();
@@ -271,7 +271,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -382,7 +382,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.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 { "" };
@@ -1,18 +1,18 @@
use chrono::Utc; use chrono::Utc;
use deunicode::deunicode; use deunicode::deunicode;
use ratatui::Frame;
use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Style, Stylize}; use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Text}; use ratatui::text::{Line, Text};
use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
use ratatui::Frame;
use regex::Regex; use regex::Regex;
use crate::app::App; use crate::app::App;
use crate::models::Route;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SERIES_DETAILS_BLOCKS}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SERIES_DETAILS_BLOCKS};
use crate::models::sonarr_models::{ use crate::models::sonarr_models::{
Season, SeasonStatistics, SonarrHistoryEventType, SonarrHistoryItem, Season, SeasonStatistics, SonarrHistoryEventType, SonarrHistoryItem,
}; };
use crate::models::Route;
use crate::ui::sonarr_ui::library::episode_details_ui::EpisodeDetailsUi; use crate::ui::sonarr_ui::library::episode_details_ui::EpisodeDetailsUi;
use crate::ui::sonarr_ui::library::season_details_ui::SeasonDetailsUi; use crate::ui::sonarr_ui::library::season_details_ui::SeasonDetailsUi;
use crate::ui::sonarr_ui::sonarr_ui_utils::{ use crate::ui::sonarr_ui::sonarr_ui_utils::{
@@ -31,7 +31,7 @@ use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::message::Message; use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::{DrawUi, draw_popup, draw_tabs}; use crate::ui::{draw_popup, draw_tabs, DrawUi};
use crate::utils::convert_to_gb; use crate::utils::convert_to_gb;
#[cfg(test)] #[cfg(test)]
@@ -315,7 +315,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.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![