mirror of
https://github.com/kristoferssolo/traxor.git
synced 2026-01-14 20:46:14 +00:00
feat: add torrent rename functionality
This commit is contained in:
parent
baab8a1984
commit
33b446d440
@ -23,7 +23,9 @@ impl Torrents {
|
|||||||
|
|
||||||
if !to_start.is_empty() {
|
if !to_start.is_empty() {
|
||||||
let ids: Vec<_> = to_start.into_iter().map(|(id, _)| id).collect();
|
let ids: Vec<_> = to_start.into_iter().map(|(id, _)| id).collect();
|
||||||
self.client.torrent_action(TorrentAction::Start, ids).await?;
|
self.client
|
||||||
|
.torrent_action(TorrentAction::Start, ids)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
if !to_stop.is_empty() {
|
if !to_stop.is_empty() {
|
||||||
let ids: Vec<_> = to_stop.into_iter().map(|(id, _)| id).collect();
|
let ids: Vec<_> = to_stop.into_iter().map(|(id, _)| id).collect();
|
||||||
@ -133,7 +135,11 @@ impl Torrents {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
self.client
|
self.client
|
||||||
.torrent_rename_path(vec![id], old_name.clone(), name.to_string_lossy().into_owned())
|
.torrent_rename_path(
|
||||||
|
vec![id],
|
||||||
|
old_name.clone(),
|
||||||
|
name.to_string_lossy().into_owned(),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,15 @@ use std::path::PathBuf;
|
|||||||
use types::Selected;
|
use types::Selected;
|
||||||
pub use {tab::Tab, torrent::Torrents};
|
pub use {tab::Tab, torrent::Torrents};
|
||||||
|
|
||||||
|
/// Input mode type for the application.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum InputMode {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Move,
|
||||||
|
Rename,
|
||||||
|
}
|
||||||
|
|
||||||
/// Main Application.
|
/// Main Application.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
@ -25,7 +34,7 @@ pub struct App {
|
|||||||
pub show_help: bool,
|
pub show_help: bool,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
pub input_handler: InputHandler,
|
pub input_handler: InputHandler,
|
||||||
pub input_mode: bool,
|
pub input_mode: InputMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@ -41,11 +50,11 @@ impl App {
|
|||||||
tabs: vec![Tab::All, Tab::Active, Tab::Downloading],
|
tabs: vec![Tab::All, Tab::Active, Tab::Downloading],
|
||||||
index: 0,
|
index: 0,
|
||||||
state: TableState::default(),
|
state: TableState::default(),
|
||||||
torrents: Torrents::new()?, // Handle the Result here
|
torrents: Torrents::new()?,
|
||||||
show_help: false,
|
show_help: false,
|
||||||
config,
|
config,
|
||||||
input_handler: InputHandler::new(),
|
input_handler: InputHandler::new(),
|
||||||
input_mode: false,
|
input_mode: InputMode::None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,18 +187,49 @@ impl App {
|
|||||||
self.torrents
|
self.torrents
|
||||||
.move_torrents(ids, &self.input_handler.text)
|
.move_torrents(ids, &self.input_handler.text)
|
||||||
.await?;
|
.await?;
|
||||||
self.input_handler.clear();
|
self.clear_input();
|
||||||
self.input_mode = false;
|
|
||||||
self.close_help();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepare move action by pre-filling current download directory.
|
||||||
pub fn prepare_move_action(&mut self) {
|
pub fn prepare_move_action(&mut self) {
|
||||||
if let Some(download_dir) = self.get_current_download_dir() {
|
if let Some(download_dir) = self.get_current_download_dir() {
|
||||||
self.input_handler
|
self.input_handler
|
||||||
.set_text(download_dir.to_string_lossy().into_owned());
|
.set_text(download_dir.to_string_lossy().into_owned());
|
||||||
}
|
}
|
||||||
self.input_mode = true;
|
self.input_mode = InputMode::Move;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rename the highlighted torrent.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the RPC call fails.
|
||||||
|
pub async fn rename_torrent(&mut self) -> Result<()> {
|
||||||
|
let Some(torrent) = self.get_current_torrent() else {
|
||||||
|
self.clear_input();
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
self.torrents
|
||||||
|
.rename(&torrent, std::path::Path::new(&self.input_handler.text))
|
||||||
|
.await?;
|
||||||
|
self.clear_input();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare rename action by pre-filling current torrent name.
|
||||||
|
pub fn prepare_rename_action(&mut self) {
|
||||||
|
if let Some(name) = self.get_current_torrent_name() {
|
||||||
|
self.input_handler.set_text(name);
|
||||||
|
}
|
||||||
|
self.input_mode = InputMode::Rename;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear input and reset input mode.
|
||||||
|
fn clear_input(&mut self) {
|
||||||
|
self.input_handler.clear();
|
||||||
|
self.input_mode = InputMode::None;
|
||||||
|
self.close_help();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select(&mut self) {
|
pub fn select(&mut self) {
|
||||||
@ -222,6 +262,16 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_download_dir(&self) -> Option<PathBuf> {
|
fn get_current_download_dir(&self) -> Option<PathBuf> {
|
||||||
|
self.get_current_torrent()
|
||||||
|
.and_then(|t| t.download_dir)
|
||||||
|
.map(PathBuf::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_torrent_name(&self) -> Option<String> {
|
||||||
|
self.get_current_torrent().and_then(|t| t.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_torrent(&self) -> Option<transmission_rpc::types::Torrent> {
|
||||||
let Selected::Current(current_id) = self.selected(true) else {
|
let Selected::Current(current_id) = self.selected(true) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
@ -229,7 +279,6 @@ impl App {
|
|||||||
.torrents
|
.torrents
|
||||||
.iter()
|
.iter()
|
||||||
.find(|t| t.id == Some(current_id))
|
.find(|t| t.id == Some(current_id))
|
||||||
.and_then(|t| t.download_dir.as_deref())
|
.cloned()
|
||||||
.map(PathBuf::from)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,7 +75,6 @@ impl Torrents {
|
|||||||
.torrents;
|
.torrents;
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Torrents {
|
impl Debug for Torrents {
|
||||||
|
|||||||
@ -32,4 +32,6 @@ pub struct KeybindsConfig {
|
|||||||
pub toggle_help: String,
|
pub toggle_help: String,
|
||||||
#[from_file(default = "m")]
|
#[from_file(default = "m")]
|
||||||
pub move_torrent: String,
|
pub move_torrent: String,
|
||||||
|
#[from_file(default = "r")]
|
||||||
|
pub rename_torrent: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::app::{App, action::Action};
|
use crate::app::{App, InputMode, action::Action};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@ -29,10 +29,10 @@ async fn handle_input(key_event: KeyEvent, app: &mut App) -> Result<Option<Actio
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// TODO: add error types
|
/// Returns an error if input handling fails.
|
||||||
#[tracing::instrument(name = "Getting action", skip(app))]
|
#[tracing::instrument(name = "Getting action", skip(app))]
|
||||||
pub async fn get_action(key_event: KeyEvent, app: &mut App) -> Result<Option<Action>> {
|
pub async fn get_action(key_event: KeyEvent, app: &mut App) -> Result<Option<Action>> {
|
||||||
if app.input_mode {
|
if app.input_mode != InputMode::None {
|
||||||
return handle_input(key_event, app).await;
|
return handle_input(key_event, app).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +56,7 @@ pub async fn get_action(key_event: KeyEvent, app: &mut App) -> Result<Option<Act
|
|||||||
(Action::Select, &keybinds.select),
|
(Action::Select, &keybinds.select),
|
||||||
(Action::ToggleHelp, &keybinds.toggle_help),
|
(Action::ToggleHelp, &keybinds.toggle_help),
|
||||||
(Action::Move, &keybinds.move_torrent),
|
(Action::Move, &keybinds.move_torrent),
|
||||||
|
(Action::Rename, &keybinds.rename_torrent),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find_map(|(action, keybind)| matches_keybind(&key_event, keybind).then_some(action)))
|
.find_map(|(action, keybind)| matches_keybind(&key_event, keybind).then_some(action)))
|
||||||
@ -65,7 +66,7 @@ pub async fn get_action(key_event: KeyEvent, app: &mut App) -> Result<Option<Act
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// TODO: add error types
|
/// Returns an error if the action fails.
|
||||||
#[tracing::instrument(name = "Update", skip(app))]
|
#[tracing::instrument(name = "Update", skip(app))]
|
||||||
pub async fn update(app: &mut App, action: Action) -> Result<()> {
|
pub async fn update(app: &mut App, action: Action) -> Result<()> {
|
||||||
info!("updating app with action: {}", action);
|
info!("updating app with action: {}", action);
|
||||||
@ -82,13 +83,17 @@ pub async fn update(app: &mut App, action: Action) -> Result<()> {
|
|||||||
Action::PauseAll => app.torrents.stop_all().await?,
|
Action::PauseAll => app.torrents.stop_all().await?,
|
||||||
Action::StartAll => app.torrents.start_all().await?,
|
Action::StartAll => app.torrents.start_all().await?,
|
||||||
Action::Move => app.prepare_move_action(),
|
Action::Move => app.prepare_move_action(),
|
||||||
|
Action::Rename => app.prepare_rename_action(),
|
||||||
Action::Delete(x) => app.delete(x).await?,
|
Action::Delete(x) => app.delete(x).await?,
|
||||||
Action::Rename => unimplemented!(),
|
|
||||||
Action::Select => app.select(),
|
Action::Select => app.select(),
|
||||||
Action::Submit => app.move_torrent().await?,
|
Action::Submit => match app.input_mode {
|
||||||
|
InputMode::Move => app.move_torrent().await?,
|
||||||
|
InputMode::Rename => app.rename_torrent().await?,
|
||||||
|
InputMode::None => {}
|
||||||
|
},
|
||||||
Action::Cancel => {
|
Action::Cancel => {
|
||||||
app.input_handler.clear();
|
app.input_handler.clear();
|
||||||
app.input_mode = false;
|
app.input_mode = InputMode::None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -162,11 +167,10 @@ fn parse_keybind(key_str: &str) -> std::result::Result<KeyEvent, ParseKeybindErr
|
|||||||
|
|
||||||
// function keys F1...F<N>
|
// function keys F1...F<N>
|
||||||
f if f.starts_with('f') => {
|
f if f.starts_with('f') => {
|
||||||
key_code = Some(KeyCode::F(
|
key_code =
|
||||||
f[1..]
|
Some(KeyCode::F(f[1..].parse().map_err(|_| {
|
||||||
.parse()
|
ParseKeybindError::UnknownPart(part.to_owned())
|
||||||
.map_err(|_| ParseKeybindError::UnknownPart(part.to_owned()))?,
|
})?));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// single-character fallback
|
// single-character fallback
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::app::App;
|
use crate::app::{App, InputMode};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
widgets::{Block, Borders, Clear, Paragraph},
|
widgets::{Block, Borders, Clear, Paragraph},
|
||||||
@ -9,7 +9,13 @@ pub fn render(f: &mut Frame, app: &App) {
|
|||||||
let size = f.area();
|
let size = f.area();
|
||||||
let input_area = Rect::new(size.width / 4, size.height / 2 - 1, size.width / 2, 3);
|
let input_area = Rect::new(size.width / 4, size.height / 2 - 1, size.width / 2, 3);
|
||||||
|
|
||||||
let block = Block::default().title("Move to").borders(Borders::ALL);
|
let title = match app.input_mode {
|
||||||
|
InputMode::Move => "Move to",
|
||||||
|
InputMode::Rename => "Rename",
|
||||||
|
InputMode::None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let block = Block::default().title(title).borders(Borders::ALL);
|
||||||
f.render_widget(Clear, input_area);
|
f.render_widget(Clear, input_area);
|
||||||
f.render_widget(block, input_area);
|
f.render_widget(block, input_area);
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ mod input;
|
|||||||
mod table;
|
mod table;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{App, Tab},
|
app::{App, InputMode, Tab},
|
||||||
config::color::ColorConfig,
|
config::color::ColorConfig,
|
||||||
};
|
};
|
||||||
use help::render_help;
|
use help::render_help;
|
||||||
@ -58,7 +58,7 @@ pub fn render(app: &mut App, frame: &mut Frame) {
|
|||||||
render_help(frame, app);
|
render_help(frame, app);
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.input_mode {
|
if app.input_mode != InputMode::None {
|
||||||
input::render(frame, app);
|
input::render(frame, app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
use traxor::{app::App, app::action::Action, config::Config, handler::get_action};
|
use traxor::{app::App, app::InputMode, app::action::Action, config::Config, handler::get_action};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_get_action_quit() {
|
async fn test_get_action_quit() {
|
||||||
@ -137,7 +137,7 @@ async fn test_get_action_toggle_help() {
|
|||||||
async fn test_get_action_input_mode() {
|
async fn test_get_action_input_mode() {
|
||||||
let config = Config::load().unwrap();
|
let config = Config::load().unwrap();
|
||||||
let mut app = App::new(config).unwrap();
|
let mut app = App::new(config).unwrap();
|
||||||
app.input_mode = true;
|
app.input_mode = InputMode::Move;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Enter), &mut app)
|
get_action(KeyEvent::from(KeyCode::Enter), &mut app)
|
||||||
.await
|
.await
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user