diff --git a/rm-config/defaults/keymap.toml b/rm-config/defaults/keymap.toml index 26272db..b69e7a6 100644 --- a/rm-config/defaults/keymap.toml +++ b/rm-config/defaults/keymap.toml @@ -37,6 +37,7 @@ keybindings = [ [torrents_tab] keybindings = [ { on = "a", action = "AddMagnet" }, + { on = "m", action = "MoveTorrent" }, { on = "p", action = "Pause" }, { on = "f", action = "ShowFiles" }, { on = "s", action = "ShowStats" }, diff --git a/rm-config/src/keymap/actions/torrents_tab.rs b/rm-config/src/keymap/actions/torrents_tab.rs index c1b39a7..ecef13b 100644 --- a/rm-config/src/keymap/actions/torrents_tab.rs +++ b/rm-config/src/keymap/actions/torrents_tab.rs @@ -6,6 +6,7 @@ use super::UserAction; #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum TorrentsAction { AddMagnet, + MoveTorrent, Pause, DeleteWithFiles, DeleteWithoutFiles, @@ -17,6 +18,7 @@ impl UserAction for TorrentsAction { fn desc(&self) -> &'static str { match self { TorrentsAction::AddMagnet => "add a magnet", + TorrentsAction::MoveTorrent => "move torrent download directory", TorrentsAction::Pause => "pause/unpause", TorrentsAction::DeleteWithFiles => "delete with files", TorrentsAction::DeleteWithoutFiles => "delete without files", @@ -30,6 +32,7 @@ impl From for Action { fn from(value: TorrentsAction) -> Self { match value { TorrentsAction::AddMagnet => Action::AddMagnet, + TorrentsAction::MoveTorrent => Action::MoveTorrent, TorrentsAction::Pause => Action::Pause, TorrentsAction::DeleteWithFiles => Action::DeleteWithFiles, TorrentsAction::DeleteWithoutFiles => Action::DeleteWithoutFiles, diff --git a/rm-main/src/transmission/action.rs b/rm-main/src/transmission/action.rs index 8266593..4e8b956 100644 --- a/rm-main/src/transmission/action.rs +++ b/rm-main/src/transmission/action.rs @@ -20,6 +20,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 @@ -124,6 +126,26 @@ 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_message = ErrorMessage { + title: error_title.to_string(), + message: msg, + }; + ctx.send_action(Action::Error(Box::new(error_message))); + } + } } } } diff --git a/rm-main/src/transmission/fetchers.rs b/rm-main/src/transmission/fetchers.rs index 51c0029..45fc025 100644 --- a/rm-main/src/transmission/fetchers.rs +++ b/rm-main/src/transmission/fetchers.rs @@ -71,6 +71,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 50532c0..d5b5e6a 100644 --- a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs @@ -20,6 +20,7 @@ pub struct RustmissionTorrent { status: TorrentStatus, pub style: Style, pub id: Id, + pub download_dir: String, } impl RustmissionTorrent { @@ -32,6 +33,7 @@ impl RustmissionTorrent { Line::from(self.eta_secs.as_str()), Line::from(download_speed_format(&self.download_speed)), Line::from(upload_speed_format(&self.upload_speed)), + Line::from(self.download_dir.as_str()), ]) .style(self.style) } @@ -59,6 +61,7 @@ impl RustmissionTorrent { Line::from(self.eta_secs.as_str()), Line::from(download_speed_format(&self.download_speed)), Line::from(upload_speed_format(&self.upload_speed)), + Line::from(self.download_dir.as_str()), ]) } @@ -113,6 +116,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, @@ -123,6 +131,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 2d4563f..f340efa 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; 7], + pub widths: [Constraint; 8], pub filter: Arc>>, pub torrents_displaying_no: u16, header: Vec, @@ -32,6 +32,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; 7] { + const fn default_widths() -> [Constraint; 8] { [ Constraint::Max(70), // Name Constraint::Length(5), // @@ -109,10 +110,11 @@ impl TableManager { Constraint::Length(12), // ETA Constraint::Length(12), // Download Constraint::Length(12), // Upload + Constraint::Max(70), // Download directory ] } - fn header_widths(&self, rows: &[RustmissionTorrent]) -> [Constraint; 7] { + fn header_widths(&self, rows: &[RustmissionTorrent]) -> [Constraint; 8] { if !self.ctx.config.general.auto_hide { return Self::default_widths(); } @@ -146,6 +148,7 @@ impl TableManager { Constraint::Length(eta_width), // ETA Constraint::Length(download_width), // Download Constraint::Length(upload_width), // Upload + Constraint::Max(70), // 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 eab8171..09648f6 100644 --- a/rm-main/src/ui/tabs/torrents/task_manager.rs +++ b/rm-main/src/ui/tabs/torrents/task_manager.rs @@ -12,6 +12,7 @@ use super::{ default::DefaultBar, delete_torrent::{self, DeleteBar, TorrentInfo}, filter::FilterBar, + move_torrent::MoveBar, status::{CurrentTaskState, StatusBar}, }, TableManager, @@ -37,6 +38,7 @@ pub enum CurrentTask { AddMagnetBar(AddMagnetBar), DeleteBar(DeleteBar), FilterBar(FilterBar), + MoveBar(MoveBar), Default(DefaultBar), Status(StatusBar), } @@ -86,7 +88,11 @@ 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.cancel_task(), + Some(A::Render) => Some(A::Render), + _ => None, + }, CurrentTask::FilterBar(filter_bar) => match filter_bar.handle_actions(action) { Some(A::Quit) => self.cancel_task(), Some(A::Render) => Some(A::Render), @@ -99,7 +105,6 @@ impl Component for TaskManager { Some(action) => self.handle_events_to_manager(&action), _ => None, }, - CurrentTask::Default(_) => self.handle_events_to_manager(&action), } } @@ -108,6 +113,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), CurrentTask::Status(status_bar) => status_bar.render(f, rect), @@ -129,6 +135,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(), @@ -156,6 +163,19 @@ 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()], + torrent.download_dir.to_string(), + )); + Some(Action::SwitchToInputMode) + } else { + None + } + } + fn pending_task(&mut self, task: StatusTask) -> Option { if matches!(self.current_task, CurrentTask::Status(_)) { return None; diff --git a/rm-main/src/ui/tabs/torrents/tasks/mod.rs b/rm-main/src/ui/tabs/torrents/tasks/mod.rs index 9dbc1de..a00c00a 100644 --- a/rm-main/src/ui/tabs/torrents/tasks/mod.rs +++ b/rm-main/src/ui/tabs/torrents/tasks/mod.rs @@ -2,4 +2,5 @@ pub mod add_magnet; pub mod default; pub mod delete_torrent; pub mod filter; +pub mod move_torrent; pub mod status; 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..a947ffc --- /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 rm_shared::action::Action; +use transmission_rpc::types::Id; + +use crate::{ + 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, torrents_to_move: Vec, existing_location: String) -> Self { + let prompt = format!("New directory: "); + + Self { + torrents_to_move, + input_mgr: InputManager::new_with_value(ctx.clone(), prompt, existing_location), + ctx, + } + } + + fn handle_input(&mut self, input: KeyEvent) -> Option { + if input.code == KeyCode::Enter { + let new_location = self.input_mgr.text(); + 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) + } +} diff --git a/rm-shared/src/action.rs b/rm-shared/src/action.rs index 867f135..56cfa1a 100644 --- a/rm-shared/src/action.rs +++ b/rm-shared/src/action.rs @@ -32,6 +32,7 @@ pub enum Action { SwitchToNormalMode, ChangeFocus, AddMagnet, + MoveTorrent, ChangeTab(u8), Input(KeyEvent), Error(Box),