diff --git a/Cargo.lock b/Cargo.lock index 30fe6e7..ecf371a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,6 +198,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.0.83" @@ -244,6 +253,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "confy" version = "0.6.0" @@ -861,7 +883,7 @@ dependencies = [ [[package]] name = "managarr" -version = "0.0.31" +version = "0.0.32" dependencies = [ "anyhow", "backtrace", @@ -1199,12 +1221,13 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5659e52e4ba6e07b2dad9f1158f578ef84a73762625ddb51536019f34d180eb" +checksum = "154b85ef15a5d1719bcaa193c3c81fe645cd120c156874cd660fe49fd21d1373" dependencies = [ "bitflags 2.4.2", "cassowary", + "compact_str", "crossterm", "indoc", "itertools", @@ -1582,19 +1605,25 @@ dependencies = [ ] [[package]] -name = "strum" -version = "0.25.0" +name = "static_assertions" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" dependencies = [ "heck", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 5ece488..4dcc184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managarr" -version = "0.0.31" +version = "0.0.32" authors = ["Alex Clarke "] description = "A TUI to manage your Servarrs" keywords = ["managarr", "tui-rs", "dashboard", "servarr", "tui"] @@ -28,11 +28,11 @@ reqwest = { version = "0.11.14", features = ["json"] } serde_yaml = "0.9.16" serde_json = "1.0.91" serde = { version = "1.0", features = ["derive"] } -strum = {version = "0.25.0", features = ["derive"] } -strum_macros = "0.25.0" +strum = {version = "0.26.1", features = ["derive"] } +strum_macros = "0.26.1" tokio = { version = "1.29.0", features = ["full"] } tokio-util = "0.7.8" -ratatui = { version = "0.25.0", features = ["all-widgets"] } +ratatui = { version = "0.26.0", features = ["all-widgets"] } urlencoding = "2.1.2" [dev-dependencies] diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1c8965e..a57904c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,9 +1,9 @@ use std::iter; use std::rc::Rc; -use ratatui::layout::{Alignment, Constraint, Rect}; -use ratatui::style::Modifier; -use ratatui::text::{Line, Span, Text}; +use ratatui::layout::{Alignment, Constraint, Layout, Rect}; +use ratatui::style::{Modifier, Style, Stylize}; +use ratatui::text::{Line, Text}; use ratatui::widgets::Paragraph; use ratatui::widgets::Row; use ratatui::widgets::Table; @@ -15,16 +15,16 @@ use ratatui::Frame; use crate::app::App; use crate::models::{HorizontallyScrollableText, Route, StatefulList, StatefulTable, TabState}; use crate::ui::radarr_ui::RadarrUi; +use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ background_block, borderless_block, centered_rect, horizontal_chunks, horizontal_chunks_with_margin, layout_block, layout_block_top_border, layout_button_paragraph, layout_button_paragraph_borderless, layout_paragraph_borderless, logo_block, show_cursor, - style_block_highlight, style_default, style_default_bold, style_failure, style_help, - style_highlight, style_primary, style_secondary, style_system_function, title_block, - title_block_centered, vertical_chunks_with_margin, + style_block_highlight, title_block, title_block_centered, vertical_chunks_with_margin, }; mod radarr_ui; +mod styles; mod utils; static HIGHLIGHT_SYMBOL: &str = "=> "; @@ -37,59 +37,53 @@ pub trait DrawUi { pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) { f.render_widget(background_block(), f.size()); - let main_chunks = if !app.error.text.is_empty() { - let chunks = vertical_chunks_with_margin( - vec![ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(10), - Constraint::Length(0), - ], - f.size(), - 1, - ); + let [header, context, table] = if !app.error.text.is_empty() { + let [header, error, context, table] = Layout::vertical([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(10), + Constraint::Fill(1), + ]) + .margin(1) + .areas(f.size()); - draw_error(f, app, chunks[1]); + draw_error(f, app, error); - Rc::new([chunks[0], chunks[2], chunks[3]]) + [header, context, table] } else { - vertical_chunks_with_margin( - vec![ - Constraint::Length(3), - Constraint::Length(10), - Constraint::Length(0), - ], - f.size(), - 1, - ) + Layout::vertical([ + Constraint::Length(3), + Constraint::Length(10), + Constraint::Fill(1), + ]) + .margin(1) + .areas(f.size()) }; - draw_header_row(f, app, main_chunks[0]); + draw_header_row(f, app, header); if RadarrUi::accepts(*app.get_current_route()) { - RadarrUi::draw_context_row(f, app, main_chunks[1]); - RadarrUi::draw(f, app, main_chunks[2]); + RadarrUi::draw_context_row(f, app, context); + RadarrUi::draw(f, app, table); } } fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let chunks = horizontal_chunks_with_margin(vec![Constraint::Length(75), Constraint::Min(0)], area, 1); - let help_text = Text::from(app.server_tabs.get_active_tab_help()); + let help_text = Text::from(app.server_tabs.get_active_tab_help().help()); let titles = app .server_tabs .tabs .iter() - .map(|tab| Line::from(Span::styled(tab.title, style_default_bold()))) - .collect(); + .map(|tab| Line::from(tab.title.bold())); let tabs = Tabs::new(titles) .block(logo_block()) - .highlight_style(style_secondary()) + .highlight_style(Style::new().secondary()) .select(app.server_tabs.index); let help = Paragraph::new(help_text) .block(borderless_block()) - .style(style_help()) .alignment(Alignment::Right); f.render_widget(tabs, area); @@ -97,8 +91,7 @@ fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } fn draw_error(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let block = - title_block("Error | to close").style(style_failure().add_modifier(Modifier::BOLD)); + let block = title_block("Error | to close").failure().bold(); app.error.scroll_left_or_reset( area.width as usize, @@ -106,13 +99,10 @@ fn draw_error(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { app.tick_count % app.ticks_until_scroll == 0, ); - let mut text = Text::from(app.error.to_string()); - text.patch_style(style_failure()); - - let paragraph = Paragraph::new(text) + let paragraph = Paragraph::new(Text::from(app.error.to_string()).failure()) .block(block) .wrap(Wrap { trim: true }) - .style(style_primary()); + .primary(); f.render_widget(paragraph, area); } @@ -245,8 +235,9 @@ pub fn draw_error_popup(f: &mut Frame<'_>, message: &str) { f.render_widget(background_block(), prompt_area); let error_message = Paragraph::new(Text::from(message)) - .block(title_block_centered("Error").style(style_failure())) - .style(style_failure().add_modifier(Modifier::BOLD)) + .block(title_block_centered("Error").failure()) + .failure() + .bold() .wrap(Wrap { trim: false }) .alignment(Alignment::Center); @@ -262,24 +253,21 @@ fn draw_tabs<'a>( let chunks = vertical_chunks_with_margin(vec![Constraint::Length(2), Constraint::Min(0)], area, 1); let horizontal_chunks = horizontal_chunks_with_margin( - vec![Constraint::Percentage(10), Constraint::Min(0)], + vec![Constraint::Percentage(10), Constraint::Fill(1)], area, 1, ); let block = title_block(title); - let mut help_text = Text::from(tab_state.get_active_tab_help()); - help_text.patch_style(style_help()); let titles = tab_state .tabs .iter() - .map(|tab_route| Line::from(Span::styled(tab_route.title, style_default_bold()))) - .collect(); + .map(|tab_route| Line::from(tab_route.title.bold())); let tabs = Tabs::new(titles) .block(block) - .highlight_style(style_secondary()) + .highlight_style(Style::new().secondary()) .select(tab_state.index); - let help = Paragraph::new(help_text) + let help = Paragraph::new(Text::from(tab_state.get_active_tab_help().help())) .block(borderless_block()) .alignment(Alignment::Right); @@ -369,15 +357,13 @@ fn draw_table_contents<'a, T, F>( { let rows = content.items.iter().map(row_mapper); - let headers = Row::new(table_headers) - .style(style_default_bold()) - .bottom_margin(0); + let headers = Row::new(table_headers).default().bold().bottom_margin(0); let mut table = Table::new(rows, &constraints).header(headers).block(block); if highlight { table = table - .highlight_style(style_highlight()) + .highlight_style(Style::new().highlight()) .highlight_symbol(HIGHLIGHT_SYMBOL); } @@ -386,12 +372,8 @@ fn draw_table_contents<'a, T, F>( pub fn loading(f: &mut Frame<'_>, block: Block<'_>, area: Rect, is_loading: bool) { if is_loading { - let text = "\n\n Loading ...\n\n".to_owned(); - let mut text = Text::from(text); - text.patch_style(style_system_function()); - - let paragraph = Paragraph::new(text) - .style(style_system_function()) + let paragraph = Paragraph::new(Text::from("\n\n Loading ...\n\n")) + .system_function() .block(block); f.render_widget(paragraph, area); } else { @@ -538,7 +520,7 @@ pub fn draw_checkbox_with_label( let label_paragraph = Paragraph::new(Text::from(format!("\n{label}: "))) .block(borderless_block()) .alignment(Alignment::Right) - .style(style_primary()); + .primary(); f.render_widget(label_paragraph, horizontal_chunks[0]); @@ -598,7 +580,7 @@ pub fn draw_drop_down_menu_button( let description_paragraph = Paragraph::new(Text::from(format!("\n{description}: "))) .block(borderless_block()) .alignment(Alignment::Right) - .style(style_primary()); + .primary(); f.render_widget(description_paragraph, horizontal_chunks[0]); @@ -614,7 +596,7 @@ pub fn draw_selectable_list<'a, T>( let items: Vec> = content.items.iter().map(item_mapper).collect(); let list = List::new(items) .block(layout_block()) - .highlight_style(style_highlight()); + .highlight_style(Style::new().highlight()); f.render_stateful_widget(list, area, &mut content.state); } @@ -648,7 +630,7 @@ pub fn draw_list_box<'a, T>( let mut list = List::new(items).block(block); if is_popup { - list = list.highlight_style(style_highlight()); + list = list.highlight_style(Style::new().highlight()); } f.render_stateful_widget(list, content_area, &mut content.state); @@ -662,9 +644,7 @@ fn draw_help_and_get_content_rect(f: &mut Frame<'_>, area: Rect, help: Option, text_box_props: TextBoxProps<'_>) { cursor_after_string, } = text_box_props; let (block, style) = if let Some(title) = block_title { - (title_block_centered(title), style_default()) + (title_block_centered(title), Style::new().default()) } else { ( layout_block(), if should_show_cursor { - style_default() + Style::new().default() } else { style_block_highlight(is_selected) }, @@ -753,7 +733,7 @@ pub fn draw_text_box_with_label( let label_paragraph = Paragraph::new(Text::from(format!("\n{label}: "))) .block(borderless_block()) .alignment(Alignment::Right) - .style(style_primary()); + .primary(); f.render_widget(label_paragraph, horizontal_chunks[0]); @@ -801,7 +781,7 @@ pub fn draw_input_box_popup( ); let help = Paragraph::new(" cancel") - .style(style_help()) + .help() .alignment(Alignment::Center) .block(borderless_block()); f.render_widget(help, chunks[1]); @@ -809,7 +789,7 @@ pub fn draw_input_box_popup( pub fn draw_error_message_popup(f: &mut Frame<'_>, error_message_area: Rect, error_msg: &str) { let input = Paragraph::new(error_msg) - .style(style_failure()) + .failure() .alignment(Alignment::Center) .block(layout_block()); diff --git a/src/ui/radarr_ui/collections/collection_details_ui.rs b/src/ui/radarr_ui/collections/collection_details_ui.rs index 74a3c70..46a2f0a 100644 --- a/src/ui/radarr_ui/collections/collection_details_ui.rs +++ b/src/ui/radarr_ui/collections/collection_details_ui.rs @@ -1,5 +1,6 @@ use ratatui::layout::{Alignment, Constraint, Rect}; -use ratatui::text::Text; +use ratatui::style::Stylize; +use ratatui::text::{Line, Text}; use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; use ratatui::Frame; @@ -12,10 +13,10 @@ use crate::models::servarr_data::radarr::radarr_data::{ }; use crate::models::Route; use crate::ui::radarr_ui::collections::draw_collections; +use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ - borderless_block, get_width_from_percentage, layout_block_top_border_with_title, - line_info_primary, style_default, style_help, style_primary, title_block, title_style, - vertical_chunks_with_margin, + borderless_block, get_width_from_percentage, layout_block_top_border_with_title, title_block, + title_style, vertical_chunks_with_margin, }; use crate::ui::{draw_large_popup_over, draw_small_popup_over, draw_table, DrawUi, TableProps}; use crate::utils::convert_runtime; @@ -98,11 +99,13 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, content_are .current_selection() .clone() }; - let mut help_text = Text::from(format!( - "<↑↓> scroll table | {}", - build_context_clue_string(&COLLECTION_DETAILS_CONTEXT_CLUES) - )); - help_text.patch_style(style_help()); + let help_text = Text::from( + format!( + "<↑↓> scroll table | {}", + build_context_clue_string(&COLLECTION_DETAILS_CONTEXT_CLUES) + ) + .help(), + ); let monitored = if collection_selection.monitored { "Yes" } else { @@ -116,24 +119,35 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, content_are let minimum_availability = collection_selection.minimum_availability.to_display_str(); let collection_description = Text::from(vec![ - line_info_primary( - "Overview: ".to_owned(), - collection_selection.overview.clone().unwrap_or_default(), - ), - line_info_primary( - "Root Folder Path: ".to_owned(), + Line::from(vec![ + "Overview ".primary().bold(), + collection_selection + .overview + .clone() + .unwrap_or_default() + .default(), + ]), + Line::from(vec![ + "Root Folder Path: ".primary().bold(), collection_selection .root_folder_path .clone() - .unwrap_or_default(), - ), - line_info_primary("Quality Profile: ".to_owned(), quality_profile), - line_info_primary( - "Minimum Availability: ".to_owned(), - minimum_availability.to_owned(), - ), - line_info_primary("Monitored: ".to_owned(), monitored.to_owned()), - line_info_primary("Search on Add: ".to_owned(), search_on_add.to_owned()), + .unwrap_or_default() + .default(), + ]), + Line::from(vec![ + "Quality Profile: ".primary().bold(), + quality_profile.default(), + ]), + Line::from(vec![ + "Minimum Availability: ".primary().bold(), + minimum_availability.default(), + ]), + Line::from(vec!["Monitored: ".primary().bold(), monitored.default()]), + Line::from(vec![ + "Search on Add: ".primary().bold(), + search_on_add.default(), + ]), ]); let description_paragraph = Paragraph::new(collection_description) @@ -230,7 +244,7 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, content_are Cell::from(rotten_tomatoes_rating), Cell::from(movie.genres.join(", ")), ]) - .style(style_primary()) + .primary() }, app.is_loading, true, @@ -246,7 +260,7 @@ fn draw_movie_overview(f: &mut Frame<'_>, app: &mut App<'_>, content_area: Rect) content_area, 1, ); - let mut overview = Text::from( + let overview = Text::from( app .data .radarr_data @@ -254,10 +268,9 @@ fn draw_movie_overview(f: &mut Frame<'_>, app: &mut App<'_>, content_area: Rect) .current_selection() .clone() .overview, - ); - overview.patch_style(style_default()); - let mut help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES)); - help_text.patch_style(style_help()); + ) + .default(); + let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help()); let paragraph = Paragraph::new(overview) .block(borderless_block()) diff --git a/src/ui/radarr_ui/collections/mod.rs b/src/ui/radarr_ui/collections/mod.rs index 519afc5..875197c 100644 --- a/src/ui/radarr_ui/collections/mod.rs +++ b/src/ui/radarr_ui/collections/mod.rs @@ -10,11 +10,12 @@ use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, COLLEC use crate::models::Route; use crate::ui::radarr_ui::collections::collection_details_ui::CollectionDetailsUi; use crate::ui::radarr_ui::collections::edit_collection_ui::EditCollectionUi; -use crate::ui::utils::{get_width_from_percentage, layout_block_top_border, style_primary}; +use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::{ draw_error_message_popup, draw_input_box_popup, draw_popup_over, draw_prompt_box, draw_prompt_popup_over, draw_table, DrawUi, TableProps, }; +use crate::ui::styles::ManagarrStyle; mod collection_details_ui; #[cfg(test)] @@ -167,8 +168,7 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) ), Cell::from(search_on_add), Cell::from(monitored), - ]) - .style(style_primary()) + ]).primary() }, app.is_loading, true, diff --git a/src/ui/radarr_ui/downloads/mod.rs b/src/ui/radarr_ui/downloads/mod.rs index 43e61ed..a298af1 100644 --- a/src/ui/radarr_ui/downloads/mod.rs +++ b/src/ui/radarr_ui/downloads/mod.rs @@ -6,8 +6,9 @@ use crate::app::App; use crate::models::radarr_models::DownloadRecord; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS}; use crate::models::{HorizontallyScrollableText, Route}; -use crate::ui::utils::{get_width_from_percentage, layout_block_top_border, style_primary}; +use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::{draw_prompt_box, draw_prompt_popup_over, draw_table, DrawUi, TableProps}; +use crate::ui::styles::ManagarrStyle; use crate::utils::convert_to_gb; #[cfg(test)] @@ -119,8 +120,7 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ), Cell::from(indexer.to_owned()), Cell::from(download_client.to_owned()), - ]) - .style(style_primary()) + ]).primary() }, app.is_loading, true, diff --git a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs index 460a2ee..81a4c91 100644 --- a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs +++ b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs @@ -10,7 +10,7 @@ use crate::ui::{ draw_button, draw_checkbox_with_label, draw_popup_over, draw_text_box_with_label, loading, DrawUi, LabeledTextBoxProps, }; -use ratatui::layout::{Constraint, Rect}; +use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::Frame; use std::iter; @@ -66,16 +66,14 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: R 1, ); - let left_chunks = vertical_chunks( - vec![ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Min(0), - ], - split_chunks[0], - ); + let [name, rss, auto_search, interactive_search, _] = Layout::vertical([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Min(0), + ]) + .areas(split_chunks[0]); let right_chunks = vertical_chunks( vec![ Constraint::Length(3), @@ -91,7 +89,7 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: R draw_text_box_with_label( f, LabeledTextBoxProps { - area: left_chunks[0], + area: name, label: "Name", text: &edit_indexer_modal.name.text, offset: *edit_indexer_modal.name.offset.borrow(), @@ -166,14 +164,14 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: R draw_checkbox_with_label( f, - left_chunks[1], + rss, "Enable RSS", edit_indexer_modal.enable_rss.unwrap_or_default(), selected_block == &ActiveRadarrBlock::EditIndexerToggleEnableRss, ); draw_checkbox_with_label( f, - left_chunks[2], + auto_search, "Enable Automatic Search", edit_indexer_modal .enable_automatic_search @@ -182,7 +180,7 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: R ); draw_checkbox_with_label( f, - left_chunks[3], + interactive_search, "Enable Interactive Search", edit_indexer_modal .enable_interactive_search diff --git a/src/ui/radarr_ui/indexers/mod.rs b/src/ui/radarr_ui/indexers/mod.rs index 1d5c660..defcbd7 100644 --- a/src/ui/radarr_ui/indexers/mod.rs +++ b/src/ui/radarr_ui/indexers/mod.rs @@ -10,7 +10,8 @@ use crate::models::Route; use crate::ui::radarr_ui::indexers::edit_indexer_ui::EditIndexerUi; use crate::ui::radarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi; use crate::ui::radarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi; -use crate::ui::utils::{layout_block_top_border, style_failure, style_primary, style_success}; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::layout_block_top_border; use crate::ui::{draw_prompt_box, draw_prompt_popup_over, draw_table, DrawUi, TableProps}; mod edit_indexer_ui; @@ -103,24 +104,15 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } = indexer; let bool_to_text = |flag: bool| { if flag { - return ("Enabled", style_success()); + return Text::from("Enabled").success(); } - ("Disabled", style_failure()) + Text::from("Disabled").failure() }; - let (rss_text, rss_style) = bool_to_text(*enable_rss); - let mut rss = Text::from(rss_text); - rss.patch_style(rss_style); - - let (auto_search_text, auto_search_style) = bool_to_text(*enable_automatic_search); - let mut automatic_search = Text::from(auto_search_text); - automatic_search.patch_style(auto_search_style); - - let (interactive_search_text, interactive_search_style) = - bool_to_text(*enable_interactive_search); - let mut interactive_search = Text::from(interactive_search_text); - interactive_search.patch_style(interactive_search_style); + let rss = bool_to_text(*enable_rss); + let automatic_search = bool_to_text(*enable_automatic_search); + let interactive_search = bool_to_text(*enable_interactive_search); let tags: String = tags .iter() .map(|tag_id| { @@ -143,7 +135,7 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Cell::from(priority.to_string()), Cell::from(tags), ]) - .style(style_primary()) + .primary() }, app.is_loading, true, diff --git a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs index f7d3649..85efab6 100644 --- a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs +++ b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs @@ -4,9 +4,8 @@ use crate::models::servarr_data::radarr::modals::IndexerTestResultModalItem; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::Route; use crate::ui::radarr_ui::indexers::draw_indexers; -use crate::ui::utils::{ - borderless_block, get_width_from_percentage, style_failure, style_success, title_block, -}; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{borderless_block, get_width_from_percentage, title_block}; use crate::ui::{ draw_help_and_get_content_rect, draw_large_popup_over, draw_table, DrawUi, TableProps, }; @@ -76,18 +75,17 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are app.tick_count % app.ticks_until_scroll == 0, ); let pass_fail = if result.is_valid { "✔" } else { "❌" }; - let row_style = if result.is_valid { - style_success() - } else { - style_failure() - }; - - Row::new(vec![ + let row = Row::new(vec![ Cell::from(result.name.to_owned()), Cell::from(pass_fail.to_owned()), Cell::from(result.validation_failures.to_string()), - ]) - .style(row_style) + ]); + + if result.is_valid { + row.success() + } else { + row.failure() + } }, app.is_loading, true, diff --git a/src/ui/radarr_ui/library/add_movie_ui.rs b/src/ui/radarr_ui/library/add_movie_ui.rs index c239eca..d0ad6db 100644 --- a/src/ui/radarr_ui/library/add_movie_ui.rs +++ b/src/ui/radarr_ui/library/add_movie_ui.rs @@ -11,10 +11,10 @@ use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MO use crate::models::Route; use crate::ui::radarr_ui::collections::{draw_collection_details, draw_collections}; use crate::ui::radarr_ui::library::draw_library; +use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ borderless_block, get_width_from_percentage, horizontal_chunks, layout_block, - layout_paragraph_borderless, style_help, style_primary, title_block_centered, - vertical_chunks_with_margin, + layout_paragraph_borderless, title_block_centered, vertical_chunks_with_margin, }; use crate::ui::{ draw_button, draw_drop_down_menu_button, draw_drop_down_popup, draw_error_popup, @@ -148,8 +148,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ); f.render_widget(layout_block(), chunks[1]); - let mut help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES)); - help_text.patch_style(style_help()); + let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help()); let help_paragraph = Paragraph::new(help_text) .block(borderless_block()) .alignment(Alignment::Center); @@ -167,10 +166,8 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { | ActiveRadarrBlock::AddMovieSelectRootFolder | ActiveRadarrBlock::AddMovieAlreadyInLibrary | ActiveRadarrBlock::AddMovieTagsInput => { - let mut help_text = Text::from(build_context_clue_string( - &ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, - )); - help_text.patch_style(style_help()); + let help_text = + Text::from(build_context_clue_string(&ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES).help()); let help_paragraph = Paragraph::new(help_text) .block(borderless_block()) .alignment(Alignment::Center); @@ -259,7 +256,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Cell::from(rotten_tomatoes_rating), Cell::from(movie.genres.join(", ")), ]) - .style(style_primary()) + .primary() }, is_loading, true, diff --git a/src/ui/radarr_ui/library/mod.rs b/src/ui/radarr_ui/library/mod.rs index f4f2147..4db6da5 100644 --- a/src/ui/radarr_ui/library/mod.rs +++ b/src/ui/radarr_ui/library/mod.rs @@ -6,7 +6,7 @@ use crate::app::App; use crate::models::radarr_models::Movie; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, LIBRARY_BLOCKS}; use crate::models::Route; -use crate::ui::radarr_ui::determine_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::delete_movie_ui::DeleteMovieUi; use crate::ui::radarr_ui::library::edit_movie_ui::EditMovieUi; @@ -186,19 +186,22 @@ pub(super) fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .collect::>() .join(", "); - Row::new(vec![ - Cell::from(movie.title.to_string()), - Cell::from(movie.year.to_string()), - Cell::from(movie.studio.to_string()), - Cell::from(format!("{hours}h {minutes}m")), - Cell::from(certification), - Cell::from(movie.original_language.name.to_owned()), - Cell::from(format!("{file_size:.2} GB")), - Cell::from(quality_profile), - Cell::from(monitored.to_owned()), - Cell::from(tags), - ]) - .style(determine_row_style(downloads_vec, movie)) + decorate_with_row_style( + downloads_vec, + movie, + Row::new(vec![ + Cell::from(movie.title.to_string()), + Cell::from(movie.year.to_string()), + Cell::from(movie.studio.to_string()), + Cell::from(format!("{hours}h {minutes}m")), + Cell::from(certification), + Cell::from(movie.original_language.name.to_owned()), + Cell::from(format!("{file_size:.2} GB")), + Cell::from(quality_profile), + Cell::from(monitored.to_owned()), + Cell::from(tags), + ]), + ) }, app.is_loading, true, diff --git a/src/ui/radarr_ui/library/movie_details_ui.rs b/src/ui/radarr_ui/library/movie_details_ui.rs index 2cfd55f..fe03739 100644 --- a/src/ui/radarr_ui/library/movie_details_ui.rs +++ b/src/ui/radarr_ui/library/movie_details_ui.rs @@ -1,7 +1,7 @@ use std::iter; use ratatui::layout::{Alignment, Constraint, Rect}; -use ratatui::style::{Modifier, Style}; +use ratatui::style::{Style, Stylize}; use ratatui::text::{Line, Span, Text}; use ratatui::widgets::{Cell, ListItem, Paragraph, Row, Wrap}; use ratatui::Frame; @@ -11,10 +11,10 @@ use crate::models::radarr_models::{Credit, MovieHistoryItem, Release, ReleaseFie use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS}; use crate::models::Route; use crate::ui::radarr_ui::library::draw_library; +use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ borderless_block, get_width_from_percentage, layout_block_bottom_border, layout_block_top_border, - line_info_default, style_awaiting_import, style_bold, style_failure, style_primary, - style_success, style_warning, vertical_chunks, + vertical_chunks, }; use crate::ui::{ draw_drop_down_popup, draw_large_popup_over, draw_prompt_box, draw_prompt_box_with_content, @@ -162,19 +162,13 @@ fn draw_file_info(f: &mut Frame<'_>, app: &App<'_>, content_area: Rect) { ], content_area, ); - let mut file_details_title = Text::from("File Details"); - let mut audio_details_title = Text::from("Audio Details"); - let mut video_details_title = Text::from("Video Details"); - file_details_title.patch_style(style_bold()); - audio_details_title.patch_style(style_bold()); - video_details_title.patch_style(style_bold()); let file_details_title_paragraph = - Paragraph::new(file_details_title).block(layout_block_top_border()); + Paragraph::new("File Details".bold()).block(layout_block_top_border()); let audio_details_title_paragraph = - Paragraph::new(audio_details_title).block(borderless_block()); + Paragraph::new("Audio Details".bold()).block(borderless_block()); let video_details_title_paragraph = - Paragraph::new(video_details_title).block(borderless_block()); + Paragraph::new("Video Details".bold()).block(borderless_block()); let file_details = Text::from(file_info); let audio_details = Text::from(audio_details); @@ -214,19 +208,22 @@ fn draw_movie_details(f: &mut Frame<'_>, app: &App<'_>, content_area: Rect) { .unwrap() .split(": ") .collect::>()[1]; - let mut text = Text::from( + let text = Text::from( movie_details .items .iter() .map(|line| { let split = line.split(':').collect::>(); let title = format!("{}:", split[0]); + let style = style_from_download_status(download_status); - line_info_default(title, split[1..].join(":")) + Line::from(vec![ + title.bold().style(style), + Span::styled(split[1..].join(":"), style), + ]) }) .collect::>>(), ); - text.patch_style(determine_style_from_download_status(download_status)); let paragraph = Paragraph::new(text) .block(block) @@ -304,7 +301,7 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, content_area: Rect) Cell::from(quality.quality.name.to_owned()), Cell::from(date.to_string()), ]) - .style(style_success()) + .success() }, app.is_loading, true, @@ -347,7 +344,7 @@ fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, content_area: Rect) { Cell::from(person_name.to_owned()), Cell::from(character.clone().unwrap_or_default()), ]) - .style(style_success()) + .success() }, app.is_loading, true, @@ -391,7 +388,7 @@ fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, content_area: Rect) { Cell::from(job.clone().unwrap_or_default()), Cell::from(department.clone().unwrap_or_default()), ]) - .style(style_success()) + .success() }, app.is_loading, true, @@ -505,14 +502,16 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, content_area: Rect) let size = convert_to_gb(*size); let rejected_str = if *rejected { "⛔" } else { "" }; let peers = if seeders.is_none() || leechers.is_none() { - Text::default() + Text::from("") } else { let seeders = seeders.clone().unwrap().as_u64().unwrap(); let leechers = leechers.clone().unwrap().as_u64().unwrap(); - let mut text = Text::from(format!("{seeders} / {leechers}")); - text.patch_style(determine_peer_style(seeders, leechers)); - text + decorate_peer_style( + seeders, + leechers, + Text::from(format!("{seeders} / {leechers}")), + ) }; let language = if languages.is_some() { @@ -533,7 +532,7 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, content_area: Rect) Cell::from(language), Cell::from(quality), ]) - .style(style_primary()) + .primary() }, app.is_loading || is_empty, true, @@ -567,16 +566,13 @@ fn draw_manual_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>, promp }; if current_selection.rejected { - let mut lines_vec = vec![Line::from(vec![Span::styled( - "Rejection reasons: ", - style_primary().add_modifier(Modifier::BOLD), - )])]; + let mut lines_vec = vec![Line::from("Rejection reasons: ".primary().bold())]; let mut rejections_spans = current_selection .rejections .clone() .unwrap_or_default() .iter() - .map(|item| Line::from(vec![Span::styled(format!("• {item}"), style_primary())])) + .map(|item| Line::from(format!("• {item}").primary().bold())) .collect::>>(); lines_vec.append(&mut rejections_spans); @@ -604,22 +600,22 @@ fn draw_manual_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>, promp } } -fn determine_style_from_download_status(download_status: &str) -> Style { +fn style_from_download_status(download_status: &str) -> Style { match download_status { - "Downloaded" => style_success(), - "Awaiting Import" => style_awaiting_import(), - "Downloading" => style_warning(), - "Missing" => style_failure(), - _ => style_success(), + "Downloaded" => Style::new().success(), + "Awaiting Import" => Style::new().awaiting_import(), + "Downloading" => Style::new().warning(), + "Missing" => Style::new().failure(), + _ => Style::new().success(), } } -fn determine_peer_style(seeders: u64, leechers: u64) -> Style { +fn decorate_peer_style(seeders: u64, leechers: u64, text: Text<'_>) -> Text<'_> { if seeders == 0 { - style_failure() + text.failure() } else if seeders < leechers { - style_warning() + text.warning() } else { - style_success() + text.success() } } diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index cfa77c9..a44fdf8 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -1,10 +1,10 @@ use std::iter; use chrono::{Duration, Utc}; -use ratatui::layout::{Alignment, Constraint, Rect}; -use ratatui::style::{Color, Style}; +use ratatui::layout::{Alignment, Constraint, Layout, Rect}; +use ratatui::prelude::Stylize; use ratatui::text::Text; -use ratatui::widgets::Paragraph; +use ratatui::widgets::{Paragraph, Row}; use ratatui::Frame; use crate::app::App; @@ -20,10 +20,10 @@ use crate::ui::radarr_ui::indexers::IndexersUi; use crate::ui::radarr_ui::library::LibraryUi; use crate::ui::radarr_ui::root_folders::RootFoldersUi; use crate::ui::radarr_ui::system::SystemUi; +use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ - borderless_block, horizontal_chunks, layout_block, line_gauge_with_label, line_gauge_with_title, - style_awaiting_import, style_bold, style_default, style_failure, style_success, - style_unmonitored, style_warning, title_block, vertical_chunks_with_margin, + borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block, + vertical_chunks_with_margin, }; use crate::ui::DrawUi; use crate::utils::convert_to_gb; @@ -63,16 +63,14 @@ impl DrawUi for RadarrUi { } fn draw_context_row(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { - let chunks = horizontal_chunks(vec![Constraint::Min(0), Constraint::Length(20)], area); + let [main, logo] = Layout::horizontal([Constraint::Min(0), Constraint::Length(20)]).areas(area); - let context_chunks = horizontal_chunks( - vec![Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)], - chunks[0], - ); + let [stats, downloads] = + Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(main); - draw_stats_context(f, app, context_chunks[0]); - draw_downloads_context(f, app, context_chunks[1]); - draw_radarr_logo(f, chunks[1]); + draw_stats_context(f, app, stats); + draw_downloads_context(f, app, downloads); + draw_radarr_logo(f, logo); } } @@ -107,7 +105,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { app.data.radarr_data.version ))) .block(borderless_block()) - .style(style_bold()); + .bold(); let uptime = Utc::now() - start_time.to_owned(); let days = uptime.num_days(); @@ -122,12 +120,10 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { width = 2 ))) .block(borderless_block()) - .style(style_bold()); + .bold(); - let storage = - Paragraph::new(Text::from("Storage:")).block(borderless_block().style(style_bold())); - let folders = - Paragraph::new(Text::from("Root Folders:")).block(borderless_block().style(style_bold())); + let storage = Paragraph::new(Text::from("Storage:")).block(borderless_block().bold()); + let folders = Paragraph::new(Text::from("Root Folders:")).block(borderless_block().bold()); f.render_widget(version_paragraph, chunks[0]); f.render_widget(uptime_paragraph, chunks[1]); @@ -159,7 +155,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { let space: f64 = convert_to_gb(*free_space); let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) .block(borderless_block()) - .style(style_default()); + .default(); f.render_widget(root_folder_space, chunks[i + disk_space_vec.len() + 4]) } @@ -198,36 +194,40 @@ fn draw_downloads_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { } } -fn determine_row_style(downloads_vec: &[DownloadRecord], movie: &Movie) -> Style { +fn decorate_with_row_style<'a>( + downloads_vec: &[DownloadRecord], + movie: &Movie, + row: Row<'a>, +) -> Row<'a> { if !movie.has_file { if let Some(download) = downloads_vec .iter() .find(|&download| download.movie_id == movie.id) { if download.status == "downloading" { - return style_warning(); + return row.warning(); } if download.status == "completed" { - return style_awaiting_import(); + return row.awaiting_import(); } } - return style_failure(); + return row.failure(); } if !movie.monitored { - style_unmonitored() + row.unmonitored() } else { - style_success() + row.success() } } fn draw_radarr_logo(f: &mut Frame<'_>, area: Rect) { - let mut logo_text = Text::from(RADARR_LOGO); - logo_text.patch_style(Style::default().fg(Color::LightYellow)); + let logo_text = Text::from(RADARR_LOGO); let logo = Paragraph::new(logo_text) - .block(layout_block()) + .light_yellow() + .block(layout_block().default()) .alignment(Alignment::Center); f.render_widget(logo, area); } diff --git a/src/ui/radarr_ui/radarr_ui_utils.rs b/src/ui/radarr_ui/radarr_ui_utils.rs index 17de238..9cbadeb 100644 --- a/src/ui/radarr_ui/radarr_ui_utils.rs +++ b/src/ui/radarr_ui/radarr_ui_utils.rs @@ -1,19 +1,21 @@ -use crate::ui::utils::{style_default, style_failure, style_secondary}; -use ratatui::style::{Color, Modifier, Style}; +use ratatui::style::{Style, Stylize}; +use ratatui::widgets::ListItem; + +use crate::ui::styles::ManagarrStyle; #[cfg(test)] #[path = "radarr_ui_utils_tests.rs"] mod radarr_ui_utils_tests; -pub(super) fn determine_log_style_by_level(level: &str) -> Style { +pub(super) fn style_log_list_item(list_item: ListItem<'_>, level: String) -> ListItem<'_> { match level.to_lowercase().as_str() { - "trace" => Style::default().fg(Color::Gray), - "debug" => Style::default().fg(Color::Blue), - "info" => style_default(), - "warn" => style_secondary(), - "error" => style_failure(), - "fatal" => style_failure().add_modifier(Modifier::BOLD), - _ => style_default(), + "trace" => list_item.gray(), + "debug" => list_item.blue(), + "info" => list_item.style(Style::new().default()), + "warn" => list_item.style(Style::new().secondary()), + "error" => list_item.style(Style::new().failure()), + "fatal" => list_item.style(Style::new().failure().bold()), + _ => list_item.style(Style::new().default()), } } diff --git a/src/ui/radarr_ui/radarr_ui_utils_tests.rs b/src/ui/radarr_ui/radarr_ui_utils_tests.rs index 279905d..ddcecf8 100644 --- a/src/ui/radarr_ui/radarr_ui_utils_tests.rs +++ b/src/ui/radarr_ui/radarr_ui_utils_tests.rs @@ -2,32 +2,50 @@ mod tests { use super::super::*; use pretty_assertions::assert_str_eq; + use ratatui::prelude::Text; + use ratatui::text::Span; #[test] fn test_determine_log_style_by_level() { + let list_item = ListItem::new(Text::from(Span::raw("test"))); + assert_eq!( - determine_log_style_by_level("trace"), - Style::default().fg(Color::Gray) + style_log_list_item(list_item.clone(), "trace".to_string()), + list_item.clone().gray() ); assert_eq!( - determine_log_style_by_level("debug"), - Style::default().fg(Color::Blue) + style_log_list_item(list_item.clone(), "debug".to_string()), + list_item.clone().blue() ); - assert_eq!(determine_log_style_by_level("info"), style_default()); - assert_eq!(determine_log_style_by_level("warn"), style_secondary()); - assert_eq!(determine_log_style_by_level("error"), style_failure()); assert_eq!( - determine_log_style_by_level("fatal"), - style_failure().add_modifier(Modifier::BOLD) + style_log_list_item(list_item.clone(), "info".to_string()), + list_item.clone().style(Style::new().default()) + ); + assert_eq!( + style_log_list_item(list_item.clone(), "warn".to_string()), + list_item.clone().style(Style::new().secondary()) + ); + assert_eq!( + style_log_list_item(list_item.clone(), "error".to_string()), + list_item.clone().style(Style::new().failure()) + ); + assert_eq!( + style_log_list_item(list_item.clone(), "fatal".to_string()), + list_item.clone().style(Style::new().failure().bold()) + ); + assert_eq!( + style_log_list_item(list_item.clone(), "".to_string()), + list_item.style(Style::new().default()) ); - assert_eq!(determine_log_style_by_level(""), style_default()); } #[test] fn test_determine_log_style_by_level_case_insensitive() { + let list_item = ListItem::new(Text::from(Span::raw("test"))); + assert_eq!( - determine_log_style_by_level("TrAcE"), - Style::default().fg(Color::Gray) + style_log_list_item(list_item.clone(), "TrAcE".to_string()), + list_item.gray() ); } diff --git a/src/ui/radarr_ui/root_folders/mod.rs b/src/ui/radarr_ui/root_folders/mod.rs index 47c1cdf..906cfd1 100644 --- a/src/ui/radarr_ui/root_folders/mod.rs +++ b/src/ui/radarr_ui/root_folders/mod.rs @@ -6,11 +6,12 @@ use crate::app::App; use crate::models::radarr_models::RootFolder; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS}; use crate::models::Route; -use crate::ui::utils::{layout_block_top_border, style_primary}; +use crate::ui::utils::{layout_block_top_border}; use crate::ui::{ draw_input_box_popup, draw_popup_over, draw_prompt_box, draw_prompt_popup_over, draw_table, DrawUi, TableProps, }; +use crate::ui::styles::ManagarrStyle; use crate::utils::convert_to_gb; #[cfg(test)] @@ -94,8 +95,7 @@ fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .len() .to_string(), ), - ]) - .style(style_primary()) + ]).primary() }, app.is_loading, true, diff --git a/src/ui/radarr_ui/system/mod.rs b/src/ui/radarr_ui/system/mod.rs index 523dc28..2a9c860 100644 --- a/src/ui/radarr_ui/system/mod.rs +++ b/src/ui/radarr_ui/system/mod.rs @@ -13,11 +13,10 @@ use ratatui::{ use crate::app::App; use crate::models::radarr_models::Task; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; -use crate::ui::radarr_ui::radarr_ui_utils::{ - convert_to_minutes_hours_days, determine_log_style_by_level, -}; +use crate::ui::radarr_ui::radarr_ui_utils::{convert_to_minutes_hours_days, style_log_list_item}; use crate::ui::radarr_ui::system::system_details_ui::SystemDetailsUi; -use crate::ui::utils::{layout_block_top_border, style_help, style_primary}; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::layout_block_top_border; use crate::ui::{draw_table, ListProps, TableProps}; use crate::{ models::Route, @@ -117,7 +116,7 @@ fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Cell::from(task_props.last_duration), Cell::from(task_props.next_execution), ]) - .style(style_primary()) + .primary() }, app.is_loading, false, @@ -177,7 +176,7 @@ pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rec Cell::from(started_string), Cell::from(duration.to_owned()), ]) - .style(style_primary()) + .primary() }, app.is_loading, false, @@ -190,10 +189,9 @@ fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { area, |log| { let log_line = log.to_string(); - let level = log_line.split('|').collect::>()[1]; - let style = determine_log_style_by_level(level); + let level = log_line.split('|').collect::>()[1].to_string(); - ListItem::new(Text::from(Span::raw(log_line))).style(style) + style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) }, ListProps { content: &mut app.data.radarr_data.logs, @@ -206,16 +204,18 @@ fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } fn draw_help(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let mut help_text = Text::from(format!( - " {}", - app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help() - .unwrap() - )); - help_text.patch_style(style_help()); + let help_text = Text::from( + format!( + " {}", + app + .data + .radarr_data + .main_tabs + .get_active_tab_contextual_help() + .unwrap() + ) + .help(), + ); let help_paragraph = Paragraph::new(help_text) .block(layout_block_top_border()) .alignment(Alignment::Left); diff --git a/src/ui/radarr_ui/system/system_details_ui.rs b/src/ui/radarr_ui/system/system_details_ui.rs index 82d6b34..a8454be 100644 --- a/src/ui/radarr_ui/system/system_details_ui.rs +++ b/src/ui/radarr_ui/system/system_details_ui.rs @@ -8,12 +8,13 @@ use crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES; use crate::app::App; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS}; use crate::models::Route; -use crate::ui::radarr_ui::radarr_ui_utils::determine_log_style_by_level; +use crate::ui::radarr_ui::radarr_ui_utils::style_log_list_item; use crate::ui::radarr_ui::system::{ draw_queued_events, draw_system_ui_layout, extract_task_props, TASK_TABLE_CONSTRAINTS, TASK_TABLE_HEADERS, }; -use crate::ui::utils::{borderless_block, style_primary, title_block}; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{borderless_block, title_block}; use crate::ui::{ draw_help_and_get_content_rect, draw_large_popup_over, draw_list_box, draw_medium_popup_over, draw_prompt_box, draw_prompt_popup_over, draw_table, loading, DrawUi, ListProps, TableProps, @@ -75,10 +76,9 @@ fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { area, |log| { let log_line = log.to_string(); - let level = log.text.split('|').collect::>()[1]; - let style = determine_log_style_by_level(level); + let level = log.text.split('|').collect::>()[1].to_string(); - ListItem::new(Text::from(Span::raw(log_line))).style(style) + style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) }, ListProps { content: &mut app.data.radarr_data.log_details, @@ -124,7 +124,7 @@ fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Cell::from(task_props.last_duration), Cell::from(task_props.next_execution), ]) - .style(style_primary()) + .primary() }, app.is_loading, true, diff --git a/src/ui/styles.rs b/src/ui/styles.rs new file mode 100644 index 0000000..4452fd8 --- /dev/null +++ b/src/ui/styles.rs @@ -0,0 +1,81 @@ +use ratatui::prelude::{Color, Styled}; +use ratatui::style::Stylize; + +pub const COLOR_ORANGE: Color = Color::Rgb(255, 170, 66); + +#[cfg(test)] +#[path = "styles_tests.rs"] +mod styles_tests; + +pub trait ManagarrStyle<'a, T>: Stylize<'a, T> +where + T: Default, +{ + #[allow(clippy::new_ret_no_self)] + fn new() -> T; + fn awaiting_import(self) -> T; + fn default(self) -> T; + fn failure(self) -> T; + fn help(self) -> T; + fn highlight(self) -> T; + fn primary(self) -> T; + fn secondary(self) -> T; + fn success(self) -> T; + fn system_function(self) -> T; + fn unmonitored(self) -> T; + fn warning(self) -> T; +} + +impl<'a, T, U> ManagarrStyle<'a, T> for U +where + U: Styled, + T: Default, +{ + fn new() -> T { + T::default() + } + + fn awaiting_import(self) -> T { + self.fg(COLOR_ORANGE) + } + + fn default(self) -> T { + self.white() + } + + fn failure(self) -> T { + self.red() + } + + fn help(self) -> T { + self.light_blue() + } + + fn highlight(self) -> T { + self.reversed() + } + + fn primary(self) -> T { + self.cyan() + } + + fn secondary(self) -> T { + self.yellow() + } + + fn success(self) -> T { + self.green() + } + + fn system_function(self) -> T { + self.yellow() + } + + fn unmonitored(self) -> T { + self.white() + } + + fn warning(self) -> T { + self.magenta() + } +} diff --git a/src/ui/styles_tests.rs b/src/ui/styles_tests.rs new file mode 100644 index 0000000..4507c49 --- /dev/null +++ b/src/ui/styles_tests.rs @@ -0,0 +1,73 @@ +#[cfg(test)] +mod test { + use crate::ui::styles::{ManagarrStyle, COLOR_ORANGE}; + use pretty_assertions::assert_eq; + use ratatui::prelude::Modifier; + use ratatui::style::{Style, Stylize}; + + #[test] + fn test_new() { + assert_eq!(Style::new(),