feat: add confirmation dialog before deleting torrents

This commit is contained in:
Kristofers Solo 2026-01-01 04:15:14 +02:00
parent 33b446d440
commit 8ed46b1285
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
4 changed files with 90 additions and 13 deletions

View File

@ -34,6 +34,8 @@ pub enum Action {
Select,
#[display("Submit")]
Submit,
#[display("Confirm Yes")]
ConfirmYes,
#[display("Cancel")]
Cancel,
}

View File

@ -21,6 +21,8 @@ pub enum InputMode {
None,
Move,
Rename,
/// Confirm delete dialog. Bool indicates whether to delete local data.
ConfirmDelete(bool),
}
/// Main Application.
@ -167,16 +169,6 @@ impl App {
Ok(())
}
/// # Errors
///
/// TODO: add error types
pub async fn delete(&mut self, delete_local_data: bool) -> Result<()> {
let ids = self.selected(false);
self.torrents.delete(ids, delete_local_data).await?;
self.close_help();
Ok(())
}
/// Move selected or highlighted torrent(s) to a new location.
///
/// # Errors
@ -232,6 +224,26 @@ impl App {
self.close_help();
}
/// Prepare delete confirmation dialog.
pub const fn prepare_delete(&mut self, delete_local_data: bool) {
self.input_mode = InputMode::ConfirmDelete(delete_local_data);
}
/// Execute the confirmed delete action.
///
/// # Errors
///
/// Returns an error if the RPC call fails.
pub async fn confirm_delete(&mut self) -> Result<()> {
let InputMode::ConfirmDelete(delete_local_data) = self.input_mode else {
return Ok(());
};
let ids = self.selected(false);
self.torrents.delete(ids, delete_local_data).await?;
self.clear_input();
Ok(())
}
pub fn select(&mut self) {
if let Selected::Current(current_id) = self.selected(true) {
if self.torrents.selected.contains(&current_id) {

View File

@ -6,6 +6,15 @@ use tracing::{debug, info};
#[tracing::instrument(name = "Handling input", skip(app))]
async fn handle_input(key_event: KeyEvent, app: &mut App) -> Result<Option<Action>> {
// Handle confirmation dialogs separately
if matches!(app.input_mode, InputMode::ConfirmDelete(_)) {
return match key_event.code {
KeyCode::Char('y' | 'Y') => Ok(Some(Action::ConfirmYes)),
KeyCode::Char('n' | 'N') | KeyCode::Esc => Ok(Some(Action::Cancel)),
_ => Ok(None),
};
}
match key_event.code {
KeyCode::Enter => Ok(Some(Action::Submit)),
KeyCode::Tab => {
@ -84,13 +93,14 @@ pub async fn update(app: &mut App, action: Action) -> Result<()> {
Action::StartAll => app.torrents.start_all().await?,
Action::Move => app.prepare_move_action(),
Action::Rename => app.prepare_rename_action(),
Action::Delete(x) => app.delete(x).await?,
Action::Delete(delete_local_data) => app.prepare_delete(delete_local_data),
Action::Select => app.select(),
Action::Submit => match app.input_mode {
InputMode::Move => app.move_torrent().await?,
InputMode::Rename => app.rename_torrent().await?,
InputMode::None => {}
InputMode::None | InputMode::ConfirmDelete(_) => {}
},
Action::ConfirmYes => app.confirm_delete().await?,
Action::Cancel => {
app.input_handler.clear();
app.input_mode = InputMode::None;

View File

@ -1,18 +1,27 @@
use crate::app::{App, InputMode};
use ratatui::{
prelude::*,
text::Line,
widgets::{Block, Borders, Clear, Paragraph},
};
use tracing::warn;
pub fn render(f: &mut Frame, app: &App) {
match app.input_mode {
InputMode::Move | InputMode::Rename => render_text_input(f, app),
InputMode::ConfirmDelete(delete_local_data) => render_confirm_delete(f, delete_local_data),
InputMode::None => {}
}
}
fn render_text_input(f: &mut Frame, app: &App) {
let size = f.area();
let input_area = Rect::new(size.width / 4, size.height / 2 - 1, size.width / 2, 3);
let title = match app.input_mode {
InputMode::Move => "Move to",
InputMode::Rename => "Rename",
InputMode::None => return,
_ => return,
};
let block = Block::default().title(title).borders(Borders::ALL);
@ -38,3 +47,47 @@ pub fn render(f: &mut Frame, app: &App) {
input_area.y + 1,
));
}
fn render_confirm_delete(f: &mut Frame, delete_local_data: bool) {
let size = f.area();
let dialog_width = 40;
let dialog_height = 5;
let dialog_area = Rect::new(
(size.width.saturating_sub(dialog_width)) / 2,
(size.height.saturating_sub(dialog_height)) / 2,
dialog_width.min(size.width),
dialog_height.min(size.height),
);
let title = if delete_local_data {
"Delete with data?"
} else {
"Delete torrent?"
};
let block = Block::default()
.title(title)
.title_style(Style::default().fg(Color::Red).bold())
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Red));
f.render_widget(Clear, dialog_area);
f.render_widget(block, dialog_area);
let first_line = if delete_local_data {
"This will delete local files!"
} else {
"Remove from list?"
};
let text = Paragraph::new(vec![Line::from(first_line), Line::from("(y)es / (n)o")])
.alignment(Alignment::Center);
f.render_widget(
text,
dialog_area.inner(Margin {
vertical: 1,
horizontal: 1,
}),
);
}