diff --git a/rm-main/src/action.rs b/rm-main/src/action.rs index 07a5d05..7aa1468 100644 --- a/rm-main/src/action.rs +++ b/rm-main/src/action.rs @@ -29,6 +29,7 @@ pub(crate) enum Action { SwitchToNormalMode, ChangeFocus, AddMagnet, + MoveTorrent, ChangeTab(u8), Input(KeyEvent), Error(Box), @@ -104,6 +105,7 @@ fn keycode_to_action(key: KeyCode) -> Option { KeyCode::Char('f') => Some(A::ShowFiles), KeyCode::Char('/') => Some(A::Search), KeyCode::Char('a') => Some(A::AddMagnet), + KeyCode::Char('m') => Some(A::MoveTorrent), KeyCode::Char('p') => Some(A::Pause), KeyCode::Char('d') => Some(A::DeleteWithoutFiles), KeyCode::Char('D') => Some(A::DeleteWithFiles), diff --git a/rm-main/src/transmission/action.rs b/rm-main/src/transmission/action.rs index 6d49760..ca7666a 100644 --- a/rm-main/src/transmission/action.rs +++ b/rm-main/src/transmission/action.rs @@ -18,6 +18,8 @@ pub enum TorrentAction { GetTorrentInfo(Id, Arc>>), GetSessionGet(oneshot::Sender), SetArgs(Box, Option>), + // Torrent ID, Directory to move to + Move(Vec, String), } // TODO: make all the options use the same type of interface. Probably use a sender everywhere @@ -106,6 +108,23 @@ pub async fn action_handler(ctx: app::Ctx, mut trans_rx: UnboundedReceiver { + if let Err(e) = ctx + .client + .lock() + .await + .torrent_set_location(ids, new_directory.clone(), Option::from(true)) + .await + { + let error_title = "Failed to move torrent"; + let msg = "Failed to move torrent to new directory:\n\"".to_owned() + + new_directory.as_str() + + "\"\n" + + &e.to_string(); + let error_popup = Box::new(ErrorPopup::new(error_title, msg)); + ctx.send_action(Action::Error(error_popup)); + } + } } } } diff --git a/rm-main/src/transmission/fetchers.rs b/rm-main/src/transmission/fetchers.rs index db98d4e..aac7680 100644 --- a/rm-main/src/transmission/fetchers.rs +++ b/rm-main/src/transmission/fetchers.rs @@ -68,6 +68,7 @@ pub async fn torrents(ctx: app::Ctx, table_manager: Arc>) { TorrentGetField::RateUpload, TorrentGetField::RateDownload, TorrentGetField::Status, + TorrentGetField::DownloadDir, ]; let rpc_response = ctx .client diff --git a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs index 19c8cf3..a5828f3 100644 --- a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs @@ -18,6 +18,7 @@ pub struct RustmissionTorrent { status: TorrentStatus, pub style: Style, pub id: Id, + pub download_dir: String, } impl RustmissionTorrent { @@ -29,6 +30,7 @@ impl RustmissionTorrent { self.eta_secs.as_str(), self.download_speed.as_str(), self.upload_speed.as_str(), + self.download_dir.as_str(), ]) .style(self.style) } @@ -55,6 +57,7 @@ impl RustmissionTorrent { Line::from(self.eta_secs.as_str()), Line::from(self.download_speed.as_str()), Line::from(self.upload_speed.as_str()), + Line::from(self.download_dir.as_str()), ]) } @@ -109,6 +112,11 @@ impl From<&Torrent> for RustmissionTorrent { _ => Style::default(), }; + let download_dir = t + .download_dir + .clone() + .expect("torrent download directory requested"); + Self { torrent_name, size_when_done, @@ -119,6 +127,7 @@ impl From<&Torrent> for RustmissionTorrent { status, style, id, + download_dir, } } } diff --git a/rm-main/src/ui/tabs/torrents/table_manager.rs b/rm-main/src/ui/tabs/torrents/table_manager.rs index e3b596d..de704f5 100644 --- a/rm-main/src/ui/tabs/torrents/table_manager.rs +++ b/rm-main/src/ui/tabs/torrents/table_manager.rs @@ -9,7 +9,7 @@ use super::rustmission_torrent::RustmissionTorrent; pub struct TableManager { ctx: app::Ctx, pub table: GenericTable, - pub widths: [Constraint; 6], + pub widths: [Constraint; 7], pub filter: Arc>>, pub torrents_displaying_no: u16, header: Vec, @@ -31,6 +31,7 @@ impl TableManager { "ETA".to_owned(), "Download".to_owned(), "Upload".to_owned(), + "Directory".to_owned(), ], } } @@ -100,7 +101,7 @@ impl TableManager { rows } - const fn default_widths() -> [Constraint; 6] { + const fn default_widths() -> [Constraint; 7] { [ Constraint::Max(70), // Name Constraint::Length(10), // Size @@ -108,10 +109,11 @@ impl TableManager { Constraint::Length(10), // ETA Constraint::Length(10), // Download Constraint::Length(10), // Upload + Constraint::Length(20), // Download directory ] } - fn header_widths(&self, rows: &[RustmissionTorrent]) -> [Constraint; 6] { + fn header_widths(&self, rows: &[RustmissionTorrent]) -> [Constraint; 7] { if !self.ctx.config.general.auto_hide { return Self::default_widths(); } @@ -120,6 +122,7 @@ impl TableManager { let mut upload_width = 0; let mut progress_width = 0; let mut eta_width = 0; + let mut download_dir_width = 0; for row in rows { if !row.download_speed.is_empty() { @@ -135,15 +138,20 @@ impl TableManager { if !row.eta_secs.is_empty() { eta_width = 9; } + + if !row.download_dir.is_empty() { + download_dir_width = 18; + } } [ - Constraint::Max(70), // Name - Constraint::Length(9), // Size - Constraint::Length(progress_width), // Progress - Constraint::Length(eta_width), // ETA - Constraint::Length(download_width), // Download - Constraint::Length(upload_width), // Upload + Constraint::Max(70), // Name + Constraint::Length(9), // Size + Constraint::Length(progress_width), // Progress + Constraint::Length(eta_width), // ETA + Constraint::Length(download_width), // Download + Constraint::Length(upload_width), // Upload + Constraint::Length(download_dir_width), // Download directory ] } } diff --git a/rm-main/src/ui/tabs/torrents/task_manager.rs b/rm-main/src/ui/tabs/torrents/task_manager.rs index cbe4888..41b38a2 100644 --- a/rm-main/src/ui/tabs/torrents/task_manager.rs +++ b/rm-main/src/ui/tabs/torrents/task_manager.rs @@ -10,6 +10,7 @@ use super::{ default::DefaultBar, delete_torrent::{self, DeleteBar}, filter::FilterBar, + move_torrent::MoveBar, }, TableManager, }; @@ -34,6 +35,7 @@ enum CurrentTask { AddMagnetBar(AddMagnetBar), DeleteBar(DeleteBar), FilterBar(FilterBar), + MoveBar(MoveBar), Default(DefaultBar), } @@ -53,13 +55,16 @@ impl Component for TaskManager { Some(A::Render) => Some(A::Render), _ => None, }, - + CurrentTask::MoveBar(move_bar) => match move_bar.handle_actions(action) { + Some(A::Quit) => self.finish_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::Render) => Some(A::Render), _ => None, }, - CurrentTask::Default(_) => self.handle_events_to_manager(&action), } } @@ -68,6 +73,7 @@ impl Component for TaskManager { match &mut self.current_task { CurrentTask::AddMagnetBar(magnet_bar) => magnet_bar.render(f, rect), CurrentTask::DeleteBar(delete_bar) => delete_bar.render(f, rect), + CurrentTask::MoveBar(move_bar) => move_bar.render(f, rect), CurrentTask::FilterBar(filter_bar) => filter_bar.render(f, rect), CurrentTask::Default(default_bar) => default_bar.render(f, rect), } @@ -84,6 +90,7 @@ impl TaskManager { } Action::DeleteWithFiles => self.delete_torrent(delete_torrent::Mode::WithFiles), Action::DeleteWithoutFiles => self.delete_torrent(delete_torrent::Mode::WithoutFiles), + Action::MoveTorrent => self.move_torrent(), Action::Search => { self.current_task = CurrentTask::FilterBar(FilterBar::new( self.ctx.clone(), @@ -108,6 +115,16 @@ impl TaskManager { } } + fn move_torrent(&mut self) -> Option { + if let Some(torrent) = self.table_manager.lock().unwrap().current_torrent() { + self.current_task = + CurrentTask::MoveBar(MoveBar::new(self.ctx.clone(), vec![torrent.id.clone()])); + Some(Action::SwitchToInputMode) + } else { + None + } + } + fn finish_task(&mut self) -> Option { if !matches!(self.current_task, CurrentTask::Default(_)) { self.current_task = CurrentTask::Default(DefaultBar::new(self.ctx.clone())); diff --git a/rm-main/src/ui/tabs/torrents/tasks/mod.rs b/rm-main/src/ui/tabs/torrents/tasks/mod.rs index 269b3e5..8665510 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 move_torrent; diff --git a/rm-main/src/ui/tabs/torrents/tasks/move_torrent.rs b/rm-main/src/ui/tabs/torrents/tasks/move_torrent.rs new file mode 100644 index 0000000..6a74036 --- /dev/null +++ b/rm-main/src/ui/tabs/torrents/tasks/move_torrent.rs @@ -0,0 +1,62 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::prelude::*; +use transmission_rpc::types::Id; + +use crate::{ + action::Action, + app, + transmission::TorrentAction, + ui::{components::Component, tabs::torrents::input_manager::InputManager, to_input_request}, +}; + +pub struct MoveBar { + torrents_to_move: Vec, + ctx: app::Ctx, + input_mgr: InputManager, +} + +impl MoveBar { + pub fn new(ctx: app::Ctx, to_move: Vec) -> Self { + let prompt = format!("New directory: "); + + Self { + torrents_to_move: to_move, + input_mgr: InputManager::new(ctx.clone(), prompt), + ctx, + } + } + + fn handle_input(&mut self, input: KeyEvent) -> Option { + if input.code == KeyCode::Enter { + let new_location = self.input_mgr.text().to_lowercase(); + let torrents_to_move = self.torrents_to_move.clone(); + self.ctx + .send_torrent_action(TorrentAction::Move(torrents_to_move, new_location)); + return Some(Action::Quit); + } + + if input.code == KeyCode::Esc { + return Some(Action::Quit); + } + + if let Some(req) = to_input_request(input) { + self.input_mgr.handle(req); + return Some(Action::Render); + } + + None + } +} + +impl Component for MoveBar { + fn handle_actions(&mut self, action: Action) -> Option { + match action { + Action::Input(input) => self.handle_input(input), + _ => None, + } + } + + fn render(&mut self, f: &mut Frame, rect: Rect) { + self.input_mgr.render(f, rect) + } +}