From c8f157b28cbc116e69b361bfb67e15164e70ccf9 Mon Sep 17 00:00:00 2001 From: aidanaden Date: Thu, 27 Jun 2024 12:22:15 +0800 Subject: [PATCH] feat: torrent tab status bar --- rm-main/src/action.rs | 1 + rm-main/src/transmission/action.rs | 24 ++-- rm-main/src/ui/tabs/torrents/task_manager.rs | 129 +++++++++++++++++- .../src/ui/tabs/torrents/tasks/add_magnet.rs | 2 +- .../ui/tabs/torrents/tasks/delete_torrent.rs | 2 +- rm-main/src/ui/tabs/torrents/tasks/mod.rs | 1 + rm-main/src/ui/tabs/torrents/tasks/status.rs | 119 ++++++++++++++++ 7 files changed, 263 insertions(+), 15 deletions(-) create mode 100644 rm-main/src/ui/tabs/torrents/tasks/status.rs diff --git a/rm-main/src/action.rs b/rm-main/src/action.rs index d3a8a76..9e9f3d5 100644 --- a/rm-main/src/action.rs +++ b/rm-main/src/action.rs @@ -33,6 +33,7 @@ pub(crate) enum Action { ChangeTab(u8), Input(KeyEvent), Error(Box), + Success, } impl Action { diff --git a/rm-main/src/transmission/action.rs b/rm-main/src/transmission/action.rs index 6d49760..3833f35 100644 --- a/rm-main/src/transmission/action.rs +++ b/rm-main/src/transmission/action.rs @@ -31,14 +31,19 @@ pub async fn action_handler(ctx: app::Ctx, mut trans_rx: UnboundedReceiver { + ctx.send_action(Action::Success); + } + Err(e) => { + let error_title = "Failed to add a torrent"; + let msg = "Failed to add torrent with URL/Path:\n\"".to_owned() + + url + + "\"\n" + + &e.to_string(); + let error_popup = Box::new(ErrorPopup::new(error_title, msg)); + ctx.send_action(Action::Error(error_popup)); + } } } TorrentAction::Stop(ids) => { @@ -64,6 +69,7 @@ pub async fn action_handler(ctx: app::Ctx, mut trans_rx: UnboundedReceiver { ctx.client @@ -72,6 +78,8 @@ pub async fn action_handler(ctx: app::Ctx, mut trans_rx: UnboundedReceiver { let new_torrent_info = ctx diff --git a/rm-main/src/ui/tabs/torrents/task_manager.rs b/rm-main/src/ui/tabs/torrents/task_manager.rs index cbe4888..ec24979 100644 --- a/rm-main/src/ui/tabs/torrents/task_manager.rs +++ b/rm-main/src/ui/tabs/torrents/task_manager.rs @@ -1,6 +1,7 @@ use std::sync::{Arc, Mutex}; use ratatui::prelude::*; +use throbber_widgets_tui::ThrobberState; use crate::{action::Action, app, ui::components::Component}; @@ -10,6 +11,7 @@ use super::{ default::DefaultBar, delete_torrent::{self, DeleteBar}, filter::FilterBar, + status::{CurrentTaskState, StatusBar, StatusTask}, }, TableManager, }; @@ -30,11 +32,12 @@ impl TaskManager { } } -enum CurrentTask { +pub enum CurrentTask { AddMagnetBar(AddMagnetBar), DeleteBar(DeleteBar), FilterBar(FilterBar), Default(DefaultBar), + Status(StatusBar), } impl Component for TaskManager { @@ -43,23 +46,31 @@ impl Component for TaskManager { use Action as A; match &mut self.current_task { CurrentTask::AddMagnetBar(magnet_bar) => match magnet_bar.handle_actions(action) { - Some(A::Quit) => self.finish_task(), + Some(A::Confirm) => self.pending_task(StatusTask::Add), + Some(A::Quit) => self.cancel_task(), Some(A::Render) => Some(A::Render), _ => None, }, CurrentTask::DeleteBar(delete_bar) => match delete_bar.handle_actions(action) { - Some(A::Quit) => self.finish_task(), + Some(A::Confirm) => self.pending_task(StatusTask::Delete), + Some(A::Quit) => self.cancel_task(), Some(A::Render) => Some(A::Render), _ => None, }, CurrentTask::FilterBar(filter_bar) => match filter_bar.handle_actions(action) { - Some(A::Quit) => self.finish_task(), + Some(A::Quit) => self.cancel_task(), Some(A::Render) => Some(A::Render), _ => None, }, + CurrentTask::Status(status_bar) => match status_bar.handle_actions(action) { + Some(A::Render) => Some(A::Render), + Some(action) => self.handle_events_to_manager(&action), + _ => None, + }, + CurrentTask::Default(_) => self.handle_events_to_manager(&action), } } @@ -70,6 +81,14 @@ impl Component for TaskManager { CurrentTask::DeleteBar(delete_bar) => delete_bar.render(f, rect), CurrentTask::FilterBar(filter_bar) => filter_bar.render(f, rect), CurrentTask::Default(default_bar) => default_bar.render(f, rect), + CurrentTask::Status(status_bar) => status_bar.render(f, rect), + } + } + + fn tick(&mut self) -> Option { + match &mut self.current_task { + CurrentTask::Status(status_bar) => status_bar.tick(), + _ => None, } } } @@ -108,7 +127,18 @@ impl TaskManager { } } - fn finish_task(&mut self) -> Option { + fn pending_task(&mut self, task: StatusTask) -> Option { + if !matches!(self.current_task, CurrentTask::Status(_)) { + let state = Arc::new(Mutex::new(ThrobberState::default())); + self.current_task = + CurrentTask::Status(StatusBar::new(task, CurrentTaskState::Loading(state))); + Some(Action::SwitchToNormalMode) + } else { + None + } + } + + fn cancel_task(&mut self) -> Option { if !matches!(self.current_task, CurrentTask::Default(_)) { self.current_task = CurrentTask::Default(DefaultBar::new(self.ctx.clone())); Some(Action::SwitchToNormalMode) @@ -117,3 +147,92 @@ impl TaskManager { } } } + +// enum CurrentTaskState { +// Nothing, +// Loading(Arc>), +// Success(), +// Failure(), +// } +// +// impl CurrentTaskState { +// fn new() -> Self { +// Self::Nothing +// } +// +// fn loading(&mut self, state: Arc>) { +// *self = Self::Loading(task, state); +// } +// +// fn failure(&mut self) { +// *self = Self::Failure(task); +// } +// +// fn success(&mut self, task: CurrentTask) { +// *self = Self::Success(task); +// } +// } +// +// impl Component for CurrentTaskState { +// fn render(&mut self, f: &mut Frame, rect: Rect) { +// match self { +// CurrentTaskState::Nothing => return, +// CurrentTaskState::Loading(task, state) => { +// let label = match task { +// CurrentTask::DeleteBar(_) => Some("Deleting..."), +// CurrentTask::AddMagnetBar(_) => Some("Adding..."), +// _ => None, +// }; +// +// if let Some(text) = label { +// let default_throbber = throbber_widgets_tui::Throbber::default() +// .label(text) +// .style(ratatui::style::Style::default().fg(ratatui::style::Color::Yellow)); +// f.render_stateful_widget( +// default_throbber.clone(), +// rect, +// &mut state.lock().unwrap(), +// ); +// } +// } +// CurrentTaskState::Failure(task) => { +// let label = match task { +// CurrentTask::DeleteBar(_) => Some(" Error deleting torrent"), +// CurrentTask::AddMagnetBar(_) => Some(" Error adding torrent"), +// _ => None, +// }; +// if let Some(text) = label { +// let mut line = Line::default(); +// line.push_span(Span::styled("", Style::default().red())); +// line.push_span(Span::raw(text)); +// let paragraph = Paragraph::new(line); +// f.render_widget(paragraph, rect); +// } +// } +// CurrentTaskState::Success(task) => { +// let label = match task { +// CurrentTask::DeleteBar(_) => Some(" Deleted torrent"), +// CurrentTask::AddMagnetBar(_) => Some(" Added torrent"), +// _ => None, +// }; +// if let Some(text) = label { +// let mut line = Line::default(); +// line.push_span(Span::styled("", Style::default().green())); +// line.push_span(Span::raw(text)); +// let paragraph = Paragraph::new(line); +// f.render_widget(paragraph, rect); +// } +// } +// } +// } +// +// fn tick(&mut self) -> Option { +// match self { +// CurrentTaskState::Loading(_, state) => { +// state.lock().unwrap().calc_next(); +// Some(Action::Render) +// } +// _ => None, +// } +// } +// } diff --git a/rm-main/src/ui/tabs/torrents/tasks/add_magnet.rs b/rm-main/src/ui/tabs/torrents/tasks/add_magnet.rs index 4840752..2fec980 100644 --- a/rm-main/src/ui/tabs/torrents/tasks/add_magnet.rs +++ b/rm-main/src/ui/tabs/torrents/tasks/add_magnet.rs @@ -66,7 +66,7 @@ impl AddMagnetBar { self.input_magnet_mgr.text(), Some(self.input_location_mgr.text()), )); - return Some(Action::Quit); + return Some(Action::Confirm); } if input.code == KeyCode::Esc { return Some(Action::Quit); diff --git a/rm-main/src/ui/tabs/torrents/tasks/delete_torrent.rs b/rm-main/src/ui/tabs/torrents/tasks/delete_torrent.rs index 5a1dc59..bcd9198 100644 --- a/rm-main/src/ui/tabs/torrents/tasks/delete_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/tasks/delete_torrent.rs @@ -62,7 +62,7 @@ impl Component for DeleteBar { )) } } - return Some(Action::Quit); + return Some(Action::Confirm); } else if text == "n" || text == "no" { return Some(Action::Quit); } diff --git a/rm-main/src/ui/tabs/torrents/tasks/mod.rs b/rm-main/src/ui/tabs/torrents/tasks/mod.rs index 269b3e5..9dbc1de 100644 --- a/rm-main/src/ui/tabs/torrents/tasks/mod.rs +++ b/rm-main/src/ui/tabs/torrents/tasks/mod.rs @@ -2,3 +2,4 @@ pub mod add_magnet; pub mod default; pub mod delete_torrent; pub mod filter; +pub mod status; diff --git a/rm-main/src/ui/tabs/torrents/tasks/status.rs b/rm-main/src/ui/tabs/torrents/tasks/status.rs new file mode 100644 index 0000000..44a8d4a --- /dev/null +++ b/rm-main/src/ui/tabs/torrents/tasks/status.rs @@ -0,0 +1,119 @@ +use std::sync::{Arc, Mutex}; + +use crate::{action::Action, ui::components::Component}; + +use ratatui::{prelude::*, widgets::Paragraph}; +use throbber_widgets_tui::ThrobberState; + +pub struct StatusBar { + task: StatusTask, + task_status: CurrentTaskState, +} + +impl StatusBar { + pub const fn new(task: StatusTask, task_status: CurrentTaskState) -> Self { + Self { task, task_status } + } +} + +impl Component for StatusBar { + fn render(&mut self, f: &mut Frame, rect: Rect) { + match &self.task_status { + CurrentTaskState::Nothing => return, + CurrentTaskState::Loading(state) => { + let title = match self.task { + StatusTask::Add => "Adding torrent...", + StatusTask::Delete => "Deleting torrent...", + }; + let default_throbber = throbber_widgets_tui::Throbber::default() + .label(title) + .style(ratatui::style::Style::default().fg(ratatui::style::Color::Yellow)); + f.render_stateful_widget( + default_throbber.clone(), + rect, + &mut state.lock().unwrap(), + ); + } + CurrentTaskState::Failure() => { + let title = match self.task { + StatusTask::Add => " Error adding torrent", + StatusTask::Delete => " Error deleting torrent", + }; + let mut line = Line::default(); + line.push_span(Span::styled("", Style::default().red())); + line.push_span(Span::raw(title)); + let paragraph = Paragraph::new(line); + f.render_widget(paragraph, rect); + } + CurrentTaskState::Success() => { + let title = match self.task { + StatusTask::Add => " Added torrent", + StatusTask::Delete => " Deleted torrent", + }; + let mut line = Line::default(); + line.push_span(Span::styled("", Style::default().green())); + line.push_span(Span::raw(title)); + let paragraph = Paragraph::new(line); + f.render_widget(paragraph, rect); + } + } + } + + fn handle_actions(&mut self, _action: Action) -> Option { + match _action { + Action::Tick => self.tick(), + Action::Success => { + self.task_status.success(); + Some(Action::Render) + } + Action::Error(_) => { + self.task_status.failure(); + Some(Action::Render) + } + _ => Some(_action), + } + } + + fn tick(&mut self) -> Option { + match &self.task_status { + CurrentTaskState::Loading(state) => { + state.lock().unwrap().calc_next(); + Some(Action::Render) + } + _ => None, + } + } +} + +pub enum StatusTask { + Add, + Delete, +} + +#[derive(Clone)] +pub enum CurrentTaskState { + Nothing, + Loading(Arc>), + Success(), + Failure(), +} + +impl CurrentTaskState { + fn new() -> Self { + Self::Nothing + } + + fn loading(&mut self, state: Arc>) { + *self = Self::Loading(state); + } + + fn failure(&mut self) { + *self = Self::Failure(); + } + + fn success(&mut self) { + *self = Self::Success(); + } +} + +impl Component for CurrentTaskState {}