From 8ed46b12851159181c6a321ede2eacfe8f8786de Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Thu, 1 Jan 2026 04:15:14 +0200 Subject: [PATCH] feat: add confirmation dialog before deleting torrents --- src/app/action.rs | 2 ++ src/app/mod.rs | 32 ++++++++++++++++++--------- src/handler.rs | 14 ++++++++++-- src/ui/input.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 90 insertions(+), 13 deletions(-) diff --git a/src/app/action.rs b/src/app/action.rs index c860747..88a5455 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -34,6 +34,8 @@ pub enum Action { Select, #[display("Submit")] Submit, + #[display("Confirm Yes")] + ConfirmYes, #[display("Cancel")] Cancel, } diff --git a/src/app/mod.rs b/src/app/mod.rs index 0b7effd..7d3e778 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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(¤t_id) { diff --git a/src/handler.rs b/src/handler.rs index ce757df..886fbaf 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -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> { + // 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; diff --git a/src/ui/input.rs b/src/ui/input.rs index 7e2e558..1fec81b 100644 --- a/src/ui/input.rs +++ b/src/ui/input.rs @@ -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, + }), + ); +}