This commit is contained in:
2025-12-19 13:09:26 -07:00
parent d69056e5d6
commit ea73f1d3d4
10 changed files with 212 additions and 45 deletions
Generated
+120 -8
View File
@@ -41,6 +41,12 @@ dependencies = [
"libc",
]
[[package]]
name = "anpa"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d032745fe46100dbcb28ee6e30f12c4b148786f8889e07cd0a3445eeb54970f"
[[package]]
name = "anstream"
version = "0.6.21"
@@ -217,6 +223,31 @@ dependencies = [
"objc2",
]
[[package]]
name = "bon"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1"
dependencies = [
"bon-macros",
"rustversion",
]
[[package]]
name = "bon-macros"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645"
dependencies = [
"darling 0.21.3",
"ident_case",
"prettyplease",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.111",
]
[[package]]
name = "bstr"
version = "1.12.1"
@@ -376,6 +407,20 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "compact_str"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]]
name = "confy"
version = "0.6.1"
@@ -459,8 +504,18 @@ version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
"darling_core 0.20.11",
"darling_macro 0.20.11",
]
[[package]]
name = "darling"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core 0.21.3",
"darling_macro 0.21.3",
]
[[package]]
@@ -477,13 +532,38 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "darling_core"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.111",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"darling_core 0.20.11",
"quote",
"syn 2.0.111",
]
[[package]]
name = "darling_macro"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core 0.21.3",
"quote",
"syn 2.0.111",
]
@@ -536,7 +616,7 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9"
dependencies = [
"darling",
"darling 0.20.11",
"proc-macro2",
"quote",
"syn 2.0.111",
@@ -664,7 +744,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583f1f514d2754010ff71ed6853068cacbe43cc142cc076aa1b871d9754efc48"
dependencies = [
"darling",
"darling 0.20.11",
"quote",
"syn 2.0.111",
]
@@ -673,7 +753,7 @@ dependencies = [
name = "enum_display_style_derive"
version = "0.6.1"
dependencies = [
"darling",
"darling 0.20.11",
"quote",
"syn 2.0.111",
]
@@ -1249,7 +1329,7 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c"
dependencies = [
"darling",
"darling 0.20.11",
"indoc",
"proc-macro2",
"quote",
@@ -1454,6 +1534,7 @@ dependencies = [
"serial_test",
"strum",
"strum_macros",
"tachyonfx",
"tokio",
"tokio-util",
"urlencoding",
@@ -1477,6 +1558,12 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "micromath"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815"
[[package]]
name = "mime"
version = "0.3.17"
@@ -2005,6 +2092,16 @@ dependencies = [
"yansi",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.111",
]
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
@@ -2109,7 +2206,7 @@ checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
dependencies = [
"bitflags",
"cassowary",
"compact_str",
"compact_str 0.8.1",
"crossterm",
"indoc",
"instability",
@@ -2704,6 +2801,21 @@ dependencies = [
"libc",
]
[[package]]
name = "tachyonfx"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de9d53c5afe979f7de9d5ba856c9829b118d6aca6335750201f1ada46bc198e2"
dependencies = [
"anpa",
"bon",
"compact_str 0.9.0",
"micromath",
"ratatui",
"thiserror 2.0.17",
"unicode-width 0.2.0",
]
[[package]]
name = "tempfile"
version = "3.23.0"
+1
View File
@@ -67,6 +67,7 @@ openssl = { version = "0.10.70", features = ["vendored"] }
veil = "0.2.0"
validate_theme_derive = "0.1.0"
enum_display_style_derive = "0.1.0"
tachyonfx = { version = "0.21.0", features = ["sendable"] }
[dev-dependencies]
assert_cmd = "2.0.16"
+8
View File
@@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::{fs, process};
use tachyonfx::{Duration, EffectManager};
use tokio::sync::mpsc::Sender;
use tokio_util::sync::CancellationToken;
use veil::Redact;
@@ -40,6 +41,9 @@ pub struct App<'a> {
pub ticks_until_scroll: u64,
pub tick_count: u64,
pub ui_scroll_tick_count: u64,
pub last_tick: Duration,
pub effects: EffectManager<()>,
pub has_active_effect: bool,
pub is_routing: bool,
pub is_loading: bool,
pub should_refresh: bool,
@@ -189,6 +193,7 @@ impl App<'_> {
pub fn push_navigation_stack(&mut self, route: Route) {
self.navigation_stack.push(route);
self.is_routing = true;
self.has_active_effect = false;
}
pub fn pop_navigation_stack(&mut self) {
@@ -237,6 +242,9 @@ impl Default for App<'_> {
ticks_until_scroll: 4,
tick_count: 0,
ui_scroll_tick_count: 0,
last_tick: Duration::ZERO,
effects: EffectManager::default(),
has_active_effect: false,
is_loading: false,
is_routing: false,
should_refresh: false,
+12 -9
View File
@@ -3,37 +3,38 @@
extern crate assertables;
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 crossterm::execute;
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 network::NetworkTrait;
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use reqwest::Client;
use std::panic::PanicHookInfo;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Instant;
use std::{io, panic, process};
use tokio::select;
use tokio::sync::mpsc::Receiver;
use tokio::sync::{Mutex, mpsc};
use tokio::sync::{mpsc, Mutex};
use tokio_util::sync::CancellationToken;
use utils::{
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::event::Key;
use crate::event::input_event::{Events, InputEvent};
use crate::event::Key;
use crate::network::{Network, NetworkEvent};
use crate::ui::theme::{Theme, ThemeDefinitionsWrapper};
use crate::ui::{THEME, ui};
use crate::ui::{ui, THEME};
use crate::utils::load_theme_config;
mod app;
@@ -242,9 +243,12 @@ async fn start_ui(
terminal.hide_cursor()?;
let input_events = Events::new();
let mut last_frame_instant = Instant::now();
loop {
let mut app = app.lock().await;
app.last_tick = last_frame_instant.elapsed().into();
last_frame_instant = Instant::now();
terminal.draw(|f| ui(f, &mut app))?;
@@ -256,7 +260,6 @@ async fn start_ui(
handlers::handle_events(key, &mut app);
}
Some(InputEvent::Tick) => app.on_tick().await,
_ => {}
}
+5
View File
@@ -92,6 +92,11 @@ pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) {
if app.keymapping_table.is_some() {
draw_help_popup(f, app);
}
let area = f.area();
app
.effects
.process_effects(app.last_tick, f.buffer_mut(), area);
}
fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
+6 -4
View File
@@ -1,12 +1,11 @@
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect};
use ratatui::widgets::{Cell, Row};
use ratatui::Frame;
use crate::app::App;
use crate::models::Route;
use crate::models::radarr_models::Movie;
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::library::add_movie_ui::AddMovieUi;
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::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::DrawUi;
use crate::utils::{convert_runtime, convert_to_gb};
mod add_movie_ui;
@@ -64,7 +64,9 @@ impl DrawUi for LibraryUi {
.yes_no_value(app.data.radarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
Popup::new(confirmation_prompt)
.size(Size::MediumPrompt)
.app(app),
f.area(),
);
}
+5 -4
View File
@@ -1,19 +1,19 @@
use crate::app::App;
use crate::models::Route;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS};
use crate::models::servarr_models::Language;
use crate::models::sonarr_models::{SonarrHistoryEventType, SonarrHistoryItem};
use crate::ui::DrawUi;
use crate::models::Route;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size};
use ratatui::Frame;
use crate::ui::DrawUi;
use ratatui::layout::{Alignment, Constraint, Rect};
use ratatui::style::Style;
use ratatui::text::Text;
use ratatui::widgets::{Cell, Row};
use ratatui::Frame;
use super::sonarr_ui_utils::{
create_download_failed_history_event_details,
@@ -154,5 +154,6 @@ fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
.style(Style::new().secondary())
.alignment(Alignment::Left);
f.render_widget(Popup::new(message).size(Size::NarrowMessage), f.area());
let widget = Popup::new(message).size(Size::NarrowMessage).app(app);
f.render_widget(widget, f.area());
}
+3 -3
View File
@@ -2,9 +2,9 @@ use add_series_ui::AddSeriesUi;
use delete_series_ui::DeleteSeriesUi;
use edit_series_ui::EditSeriesUi;
use ratatui::{
Frame,
layout::{Constraint, Rect},
widgets::{Cell, Row},
Frame,
};
use series_details_ui::SeriesDetailsUi;
@@ -16,15 +16,15 @@ use crate::utils::convert_to_gb;
use crate::{
app::App,
models::{
Route,
servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, LIBRARY_BLOCKS},
sonarr_models::{Series, SeriesStatus},
Route,
},
ui::{
DrawUi,
styles::ManagarrStyle,
utils::{get_width_from_percentage, layout_block_top_border},
widgets::managarr_table::ManagarrTable,
DrawUi,
},
};
+26 -4
View File
@@ -1,21 +1,33 @@
use crate::app::App;
use crate::ui::styles::ManagarrStyle;
use derive_setters::Setters;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::Text;
use ratatui::style::Style;
use ratatui::widgets::{Block, Paragraph, Widget};
use tachyonfx::pattern::SweepPattern;
use tachyonfx::{fx, Interpolation};
#[cfg(test)]
#[path = "loading_block_tests.rs"]
mod loading_block_tests;
pub struct LoadingBlock<'a> {
#[derive(Setters)]
pub struct LoadingBlock<'a, 'b> {
is_loading: bool,
block: Block<'a>,
#[setters(strip_option)]
app: Option<&'a mut App<'b>>,
}
impl<'a> LoadingBlock<'a> {
impl<'a, 'b> LoadingBlock<'a, 'b> {
pub fn new(is_loading: bool, block: Block<'a>) -> Self {
Self { is_loading, block }
Self {
is_loading,
block,
app: None,
}
}
fn render_loading_block(self, area: Rect, buf: &mut Buffer) {
@@ -27,10 +39,20 @@ impl<'a> LoadingBlock<'a> {
} else {
self.block.render(area, buf);
}
if let Some(app) = self.app
&& !app.has_active_effect {
let color = Style::new().failure().fg.expect("primary fg color is unset");
let fx =
fx::repeating(fx::paint_fg(color, 1000)
.with_pattern(SweepPattern::left_to_right(10))
.with_area(area));
app.effects.add_effect(fx);
app.has_active_effect = true;
}
}
}
impl Widget for LoadingBlock<'_> {
impl Widget for LoadingBlock<'_, '_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_loading_block(area, buf);
}
+26 -13
View File
@@ -1,7 +1,10 @@
use crate::app::App;
use crate::ui::utils::{background_block, centered_rect};
use derive_setters::Setters;
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::widgets::{Block, Clear, Widget};
use tachyonfx::{fx, Interpolation};
#[cfg(test)]
#[path = "popup_tests.rs"]
@@ -49,15 +52,25 @@ impl Size {
}
}
pub struct Popup<'a, T: Widget> {
#[derive(Setters)]
pub struct Popup<'a, 'b, T: Widget> {
#[setters(skip)]
widget: T,
margin: u16,
#[setters(skip)]
percent_x: u16,
#[setters(skip)]
percent_y: u16,
#[setters(strip_option)]
block: Option<Block<'a>>,
#[setters(strip_option)]
app: Option<&'a mut App<'b>>,
}
impl<'a, T: Widget> Popup<'a, T> {
impl<T> Popup<'_, '_, T>
where
T: Widget,
{
pub fn new(widget: T) -> Self {
Self {
widget,
@@ -65,6 +78,7 @@ impl<'a, T: Widget> Popup<'a, T> {
percent_y: 0,
margin: 0,
block: None,
app: None
}
}
@@ -81,16 +95,6 @@ impl<'a, T: Widget> Popup<'a, T> {
self
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn margin(mut self, margin: u16) -> Self {
self.margin = margin;
self
}
fn render_popup(self, area: Rect, buf: &mut Buffer) {
let mut popup_area = centered_rect(self.percent_x, self.percent_y, area);
let height = if popup_area.height < 3 {
@@ -114,10 +118,19 @@ impl<'a, T: Widget> Popup<'a, T> {
.areas(popup_area);
self.widget.render(content_area, buf);
if let Some(app) = self.app
&& !app.has_active_effect {
let timer = (100, Interpolation::Linear);
let fx =
fx::coalesce(timer)
.with_area(content_area);
app.effects.add_effect(fx);
app.has_active_effect = true;
}
}
}
impl<T: Widget> Widget for Popup<'_, T> {
impl<T: Widget> Widget for Popup<'_, '_, T> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_popup(area, buf);
}