diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 776f9d3..fe098c7 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,25 +3,23 @@ use std::iter; use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::{Line, Text}; +use ratatui::widgets::Clear; use ratatui::widgets::Paragraph; use ratatui::widgets::Tabs; use ratatui::widgets::Wrap; -use ratatui::widgets::{Clear, List, ListItem}; use ratatui::Frame; use crate::app::App; -use crate::models::stateful_list::StatefulList; use crate::models::{HorizontallyScrollableText, Route, TabState}; use crate::ui::radarr_ui::RadarrUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ - background_block, borderless_block, centered_rect, layout_block_top_border, - layout_paragraph_borderless, logo_block, title_block, title_block_centered, + background_block, borderless_block, centered_rect, layout_paragraph_borderless, logo_block, + title_block, title_block_centered, }; use crate::ui::widgets::button::Button; use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::input_box::InputBox; -use crate::ui::widgets::loading_block::LoadingBlock; mod radarr_ui; mod styles; @@ -238,14 +236,6 @@ fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) - content_area } -pub struct ListProps<'a, T> { - pub content: &'a mut StatefulList, - pub title: &'static str, - pub is_loading: bool, - pub is_popup: bool, - pub help: Option, -} - pub fn draw_prompt_box( f: &mut Frame<'_>, area: Rect, @@ -351,67 +341,6 @@ pub fn draw_prompt_box_with_checkboxes( f.render_widget(no_button, no_area); } -pub fn draw_list_box<'a, T>( - f: &mut Frame<'_>, - area: Rect, - item_mapper: impl Fn(&T) -> ListItem<'a>, - list_props: ListProps<'a, T>, -) { - let ListProps { - content, - title, - is_loading, - is_popup, - help, - } = list_props; - - let (content_area, block) = if is_popup { - f.render_widget(title_block(title), area); - ( - draw_help_footer_and_get_content_area(f, area, help), - borderless_block(), - ) - } else { - (area, title_block(title)) - }; - - if !content.items.is_empty() { - let items: Vec> = content.items.iter().map(item_mapper).collect(); - let mut list = List::new(items).block(block); - - if is_popup { - list = list.highlight_style(Style::new().highlight()); - } - - f.render_stateful_widget(list, content_area, &mut content.state); - } else { - f.render_widget(LoadingBlock::new(is_loading, block), content_area); - } -} - -fn draw_help_footer_and_get_content_area( - f: &mut Frame<'_>, - area: Rect, - help: Option, -) -> Rect { - if let Some(help_string) = help { - let [content_area, help_footer_area] = - Layout::vertical([Constraint::Fill(0), Constraint::Length(2)]) - .margin(1) - .areas(area); - - let help_paragraph = Paragraph::new(Text::from(format!(" {help_string}").help())) - .block(layout_block_top_border()) - .alignment(Alignment::Left); - - f.render_widget(help_paragraph, help_footer_area); - - content_area - } else { - area - } -} - pub fn draw_input_box_popup( f: &mut Frame<'_>, area: Rect, diff --git a/src/ui/radarr_ui/system/mod.rs b/src/ui/radarr_ui/system/mod.rs index 1fafb27..e472901 100644 --- a/src/ui/radarr_ui/system/mod.rs +++ b/src/ui/radarr_ui/system/mod.rs @@ -2,6 +2,7 @@ use std::ops::Sub; use chrono::Utc; use ratatui::layout::{Alignment, Layout}; +use ratatui::style::Style; use ratatui::text::{Span, Text}; use ratatui::widgets::{Cell, Paragraph, Row}; use ratatui::{ @@ -17,11 +18,12 @@ use crate::ui::radarr_ui::radarr_ui_utils::{convert_to_minutes_hours_days, style use crate::ui::radarr_ui::system::system_details_ui::SystemDetailsUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::layout_block_top_border; +use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::managarr_table::ManagarrTable; -use crate::ui::ListProps; +use crate::ui::widgets::selectable_list::SelectableList; use crate::{ models::Route, - ui::{draw_list_box, utils::title_block, DrawUi}, + ui::{utils::title_block, DrawUi}, }; mod system_details_ui; @@ -168,23 +170,23 @@ pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rec } fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_list_box( - f, - area, - |log| { - let log_line = log.to_string(); - let level = log_line.split('|').collect::>()[1].to_string(); + let block = title_block("Logs"); - style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) - }, - ListProps { - content: &mut app.data.radarr_data.logs, - title: "Logs", - is_loading: app.is_loading, - is_popup: false, - help: None, - }, - ); + if app.data.radarr_data.logs.items.is_empty() { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + return; + } + + let logs_box = SelectableList::new(&mut app.data.radarr_data.logs, |log| { + let log_line = log.to_string(); + let level = log_line.split('|').collect::>()[1].to_string(); + + style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) + }) + .block(block) + .highlight_style(Style::new().default()); + + f.render_widget(logs_box, area); } fn draw_help(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { diff --git a/src/ui/radarr_ui/system/system_details_ui.rs b/src/ui/radarr_ui/system/system_details_ui.rs index 0f1d013..8043645 100644 --- a/src/ui/radarr_ui/system/system_details_ui.rs +++ b/src/ui/radarr_ui/system/system_details_ui.rs @@ -18,9 +18,10 @@ use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{borderless_block, title_block}; use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::Popup; +use crate::ui::widgets::selectable_list::SelectableList; use crate::ui::{ - draw_help_footer_and_get_content_area, draw_large_popup_over, draw_list_box, - draw_medium_popup_over, draw_prompt_box, draw_prompt_popup_over, DrawUi, ListProps, + draw_large_popup_over, draw_medium_popup_over, draw_prompt_box, draw_prompt_popup_over, DrawUi, }; #[cfg(test)] @@ -42,7 +43,8 @@ impl DrawUi for SystemDetailsUi { if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { match active_radarr_block { ActiveRadarrBlock::SystemLogs => { - draw_large_popup_over(f, app, area, draw_system_ui_layout, draw_logs_popup) + draw_system_ui_layout(f, app, area); + draw_logs_popup(f, app); } ActiveRadarrBlock::SystemTasks | ActiveRadarrBlock::SystemTaskStartConfirmPrompt => { draw_large_popup_over(f, app, area, draw_system_ui_layout, draw_tasks_popup) @@ -51,7 +53,8 @@ impl DrawUi for SystemDetailsUi { draw_medium_popup_over(f, app, area, draw_system_ui_layout, draw_queued_events) } ActiveRadarrBlock::SystemUpdates => { - draw_large_popup_over(f, app, area, draw_system_ui_layout, draw_updates_popup) + draw_system_ui_layout(f, app, area); + draw_updates_popup(f, app); } _ => (), } @@ -59,27 +62,35 @@ impl DrawUi for SystemDetailsUi { } } -fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_list_box( - f, - area, - |log| { - let log_line = log.to_string(); - let level = log.text.split('|').collect::>()[1].to_string(); - - style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) - }, - ListProps { - content: &mut app.data.radarr_data.log_details, - title: "Log Details", - is_loading: app.is_loading, - is_popup: true, - help: Some(format!( - "<↑↓←→> scroll | {}", - build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) - )), - }, +fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let block = title_block("Log Details"); + let help_footer = format!( + "<↑↓←→> scroll | {}", + build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) ); + + if app.data.radarr_data.log_details.items.is_empty() { + let loading = LoadingBlock::new(app.is_loading, borderless_block()); + let popup = Popup::new(loading, 75, 75) + .block(block) + .footer(&help_footer); + + f.render_widget(popup, f.size()); + return; + } + + let logs_list = SelectableList::new(&mut app.data.radarr_data.log_details, |log| { + let log_line = log.to_string(); + let level = log.text.split('|').collect::>()[1].to_string(); + + style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) + }) + .block(borderless_block()); + let popup = Popup::new(logs_list, 75, 75) + .block(block) + .footer(&help_footer); + + f.render_widget(popup, f.size()); } fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { @@ -134,27 +145,29 @@ fn draw_start_task_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ); } -fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - f.render_widget(title_block("Updates"), area); - - let content_area = draw_help_footer_and_get_content_area( - f, - area, - Some(format!( - "<↑↓> scroll | {}", - build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) - )), +fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let help_footer = format!( + "<↑↓> scroll | {}", + build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) ); let updates = app.data.radarr_data.updates.get_text(); - let block = borderless_block(); + let block = title_block("Updates"); if !updates.is_empty() { let updates_paragraph = Paragraph::new(Text::from(updates)) - .block(block) + .block(borderless_block()) .scroll((app.data.radarr_data.updates.offset, 0)); + let popup = Popup::new(updates_paragraph, 75, 75) + .block(block) + .footer(&help_footer); - f.render_widget(updates_paragraph, content_area); + f.render_widget(popup, f.size()); } else { - f.render_widget(LoadingBlock::new(app.is_loading, block), content_area); + let loading = LoadingBlock::new(app.is_loading, borderless_block()); + let popup = Popup::new(loading, 75, 75) + .block(block) + .footer(&help_footer); + + f.render_widget(popup, f.size()); } } diff --git a/src/ui/widgets/popup.rs b/src/ui/widgets/popup.rs index 15cb788..7df1dc3 100644 --- a/src/ui/widgets/popup.rs +++ b/src/ui/widgets/popup.rs @@ -1,37 +1,80 @@ -use crate::ui::utils::{background_block, centered_rect}; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{background_block, centered_rect, layout_block_top_border}; use ratatui::buffer::Buffer; -use ratatui::layout::Rect; -use ratatui::widgets::{Clear, Widget}; +use ratatui::layout::{Alignment, Constraint, Layout, Rect}; +use ratatui::prelude::Text; +use ratatui::widgets::{Block, Clear, Paragraph, Widget}; #[cfg(test)] #[path = "popup_tests.rs"] mod popup_tests; -pub struct Popup { +pub struct Popup<'a, T: Widget> { widget: T, percent_x: u16, percent_y: u16, + block: Option>, + footer: Option<&'a str>, + footer_alignment: Alignment, } -impl Popup { +impl<'a, T: Widget> Popup<'a, T> { pub fn new(widget: T, percent_x: u16, percent_y: u16) -> Self { Self { widget, percent_x, percent_y, + block: None, + footer: None, + footer_alignment: Alignment::Left, } } + pub fn block(mut self, block: Block<'a>) -> Self { + self.block = Some(block); + self + } + + pub fn footer(mut self, footer: &'a str) -> Self { + self.footer = Some(footer); + self + } + + pub fn footer_alignment(mut self, alignment: Alignment) -> Self { + self.footer_alignment = alignment; + self + } + fn render_popup(self, area: Rect, buf: &mut Buffer) { let popup_area = centered_rect(self.percent_x, self.percent_y, area); - Clear.render(popup_area, buf); background_block().render(popup_area, buf); - self.widget.render(popup_area, buf); + + if let Some(block) = self.block { + block.render(popup_area, buf); + } + + let content_area = if let Some(footer) = self.footer { + let [content_area, help_footer_area] = + Layout::vertical([Constraint::Fill(0), Constraint::Length(2)]) + .margin(1) + .areas(popup_area); + + Paragraph::new(Text::from(format!(" {footer}").help())) + .block(layout_block_top_border()) + .alignment(self.footer_alignment) + .render(help_footer_area, buf); + + content_area + } else { + popup_area + }; + + self.widget.render(content_area, buf); } } -impl Widget for Popup { +impl<'a, T: Widget> Widget for Popup<'a, T> { fn render(self, area: Rect, buf: &mut Buffer) { self.render_popup(area, buf); } diff --git a/src/ui/widgets/popup_tests.rs b/src/ui/widgets/popup_tests.rs index 856c5c8..293c12c 100644 --- a/src/ui/widgets/popup_tests.rs +++ b/src/ui/widgets/popup_tests.rs @@ -2,6 +2,7 @@ mod tests { use crate::ui::widgets::popup::Popup; use pretty_assertions::assert_eq; + use ratatui::layout::Alignment; use ratatui::widgets::Block; #[test] @@ -11,5 +12,44 @@ mod tests { assert_eq!(popup.widget, Block::new()); assert_eq!(popup.percent_x, 50); assert_eq!(popup.percent_y, 50); + assert_eq!(popup.block, None); + assert_eq!(popup.footer, None); + assert_eq!(popup.footer_alignment, Alignment::Left); + } + + #[test] + fn test_popup_block() { + let popup = Popup::new(Block::new(), 50, 50).block(Block::new()); + + assert_eq!(popup.block, Some(Block::new())); + assert_eq!(popup.widget, Block::new()); + assert_eq!(popup.percent_x, 50); + assert_eq!(popup.percent_y, 50); + assert_eq!(popup.footer, None); + assert_eq!(popup.footer_alignment, Alignment::Left); + } + + #[test] + fn test_popup_footer() { + let popup = Popup::new(Block::new(), 50, 50).footer("footer"); + + assert_eq!(popup.footer, Some("footer")); + assert_eq!(popup.widget, Block::new()); + assert_eq!(popup.percent_x, 50); + assert_eq!(popup.percent_y, 50); + assert_eq!(popup.block, None); + assert_eq!(popup.footer_alignment, Alignment::Left); + } + + #[test] + fn test_popup_footer_alignment() { + let popup = Popup::new(Block::new(), 50, 50).footer_alignment(Alignment::Center); + + assert_eq!(popup.footer_alignment, Alignment::Center); + assert_eq!(popup.widget, Block::new()); + assert_eq!(popup.percent_x, 50); + assert_eq!(popup.percent_y, 50); + assert_eq!(popup.block, None); + assert_eq!(popup.footer, None); } } diff --git a/src/ui/widgets/selectable_list.rs b/src/ui/widgets/selectable_list.rs index 91187a5..cf11d09 100644 --- a/src/ui/widgets/selectable_list.rs +++ b/src/ui/widgets/selectable_list.rs @@ -5,7 +5,7 @@ use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::prelude::Widget; use ratatui::style::Style; -use ratatui::widgets::{List, ListItem, StatefulWidget}; +use ratatui::widgets::{Block, List, ListItem, StatefulWidget}; #[cfg(test)] #[path = "selectable_list_tests.rs"] @@ -17,6 +17,8 @@ where { content: &'a mut StatefulList, row_mapper: F, + highlight_style: Style, + block: Block<'a>, } impl<'a, T, F> SelectableList<'a, T, F> @@ -27,15 +29,27 @@ where Self { content, row_mapper, + highlight_style: Style::new().highlight(), + block: layout_block(), } } + pub fn highlight_style(mut self, style: Style) -> Self { + self.highlight_style = style; + self + } + + pub fn block(mut self, block: Block<'a>) -> Self { + self.block = block; + self + } + fn render_list(self, area: Rect, buf: &mut Buffer) { let items: Vec> = self.content.items.iter().map(&self.row_mapper).collect(); let selectable_list = List::new(items) - .block(layout_block()) - .highlight_style(Style::new().highlight()); + .block(self.block) + .highlight_style(self.highlight_style); StatefulWidget::render(selectable_list, area, buf, &mut self.content.state); } diff --git a/src/ui/widgets/selectable_list_tests.rs b/src/ui/widgets/selectable_list_tests.rs index 6afff81..eb1385d 100644 --- a/src/ui/widgets/selectable_list_tests.rs +++ b/src/ui/widgets/selectable_list_tests.rs @@ -1,8 +1,11 @@ #[cfg(test)] mod tests { use crate::models::stateful_list::StatefulList; + use crate::ui::styles::ManagarrStyle; + use crate::ui::utils::{layout_block, title_block}; use crate::ui::widgets::selectable_list::SelectableList; use pretty_assertions::assert_eq; + use ratatui::style::{Style, Stylize}; use ratatui::widgets::ListItem; #[test] @@ -17,5 +20,41 @@ mod tests { let row_mapper = selectable_list.row_mapper; assert_eq!(selectable_list.content.items, items); assert_eq!(row_mapper(&"test"), ListItem::new("test")); + assert_eq!(selectable_list.highlight_style, Style::new().highlight()); + assert_eq!(selectable_list.block, layout_block()); + } + + #[test] + fn test_selectable_list_highlight_style() { + let items = vec!["test"]; + let mut stateful_list = StatefulList::default(); + stateful_list.set_items(items.clone()); + + let selectable_list = + SelectableList::new(&mut stateful_list, |item| ListItem::new(item.to_string())) + .highlight_style(Style::new().bold()); + + let row_mapper = selectable_list.row_mapper; + assert_eq!(selectable_list.highlight_style, Style::new().bold()); + assert_eq!(selectable_list.content.items, items); + assert_eq!(row_mapper(&"test"), ListItem::new("test")); + assert_eq!(selectable_list.block, layout_block()); + } + + #[test] + fn test_selectable_list_block() { + let items = vec!["test"]; + let mut stateful_list = StatefulList::default(); + stateful_list.set_items(items.clone()); + + let selectable_list = + SelectableList::new(&mut stateful_list, |item| ListItem::new(item.to_string())) + .block(title_block("test")); + + let row_mapper = selectable_list.row_mapper; + assert_eq!(selectable_list.block, title_block("test")); + assert_eq!(selectable_list.content.items, items); + assert_eq!(row_mapper(&"test"), ListItem::new("test")); + assert_eq!(selectable_list.highlight_style, Style::new().highlight()); } }