feat: adiciona funcionalidade de cancelamento de geração de faixas de áudio e atualiza o painel de execução

This commit is contained in:
2026-02-28 14:57:11 -03:00
parent 02c6d8ede2
commit 4dec1ef8ed
4 changed files with 83 additions and 33 deletions
+6 -4
View File
@@ -1,7 +1,7 @@
# Progresso de Implementação # Progresso de Implementação
**Data:** 28/02/2026 **Data:** 28/02/2026
**Status:** Fases 16 concluídas + exportação de faixa + exibição/remoção de faixas adicionadas + layout responsivo + progresso em tempo real — compilando, testes passando, aplicação executável **Status:** Fases 16 concluídas + exportação de faixa + exibição/remoção de faixas adicionadas + layout responsivo + progresso em tempo real + cancelamento de geração — compilando, testes passando, aplicação executável
**Referência:** DEVELOPMENT_PLAN.md v1.0 **Referência:** DEVELOPMENT_PLAN.md v1.0
--- ---
@@ -16,6 +16,7 @@ Após a conclusão das fases, foram implementadas funcionalidades adicionais:
- Exibição das faixas externas adicionadas com opção de remoção individual - Exibição das faixas externas adicionadas com opção de remoção individual
- Layout responsivo com painéis fixos (cabeçalho, rodapé, barra lateral) e área central com scroll - Layout responsivo com painéis fixos (cabeçalho, rodapé, barra lateral) e área central com scroll
- Progresso em tempo real via `run_ffmpeg_async` (Opção A: `std::sync::mpsc` em toda a cadeia) - Progresso em tempo real via `run_ffmpeg_async` (Opção A: `std::sync::mpsc` em toda a cadeia)
- Cancelamento de geração em andamento via botão "Cancelar" — encerra o processo FFmpeg filho imediatamente
--- ---
@@ -86,11 +87,11 @@ src/
├── infrastructure/ ├── infrastructure/
│ ├── mod.rs │ ├── mod.rs
│ └── process/ │ └── process/
└── mod.rs — run_ffmpeg_async (tokio), validate_dependencies() └── mod.rs — run_ffmpeg_async (tokio, cancel via oneshot), validate_dependencies()
└── ui/ └── ui/
├── mod.rs ├── mod.rs
├── app.rs — eframe::App; Project como única fonte de verdade ├── app.rs — eframe::App; Project como única fonte de verdade; cancel_tx para interromper FFmpeg
└── components/ └── components/
├── mod.rs ├── mod.rs
├── video_selector.rs — seleção do vídeo base ├── video_selector.rs — seleção do vídeo base
@@ -99,7 +100,7 @@ src/
├── add_subtitle_form.rs — formulário: legenda externa ├── add_subtitle_form.rs — formulário: legenda externa
├── sync_offset_field.rs — campo de atraso em segundos ├── sync_offset_field.rs — campo de atraso em segundos
├── language_field.rs — campo de idioma ISO 639-2 ├── language_field.rs — campo de idioma ISO 639-2
└── execution_panel.rs — botão gerar, progresso, erros └── execution_panel.rs — botão gerar, botão cancelar, progresso, estados: Idle/Running/Success/Cancelled/Error
``` ```
--- ---
@@ -195,6 +196,7 @@ rfd = "0.14"
- [x] Exportar faixa de áudio ou legenda existente para arquivo separado - [x] Exportar faixa de áudio ou legenda existente para arquivo separado
- [x] Exibição de faixas externas adicionadas com opção de remover - [x] Exibição de faixas externas adicionadas com opção de remover
- [x] Progresso em tempo real via `run_ffmpeg_async``run_ffmpeg_async` usa `std::sync::mpsc::Sender<String>`; `start_generation` cria tokio Runtime + thread encaminhadora; cada linha de stderr do FFmpeg aparece no log antes do término - [x] Progresso em tempo real via `run_ffmpeg_async``run_ffmpeg_async` usa `std::sync::mpsc::Sender<String>`; `start_generation` cria tokio Runtime + thread encaminhadora; cada linha de stderr do FFmpeg aparece no log antes do término
- [x] Cancelamento de geração — botão "Cancelar" visível durante `Running`; sinal via `tokio::sync::oneshot`; `child.kill().await` no `tokio::select!`; estado `ExecutionState::Cancelled` exibido em amarelo
- [ ] Exibição do comando gerado em modo debug - [ ] Exibição do comando gerado em modo debug
### Qualidade ### Qualidade
+15 -4
View File
@@ -5,7 +5,12 @@ use tokio::process::Command;
/// Executa o FFmpeg de forma assíncrona, capturando stderr em tempo real. /// Executa o FFmpeg de forma assíncrona, capturando stderr em tempo real.
/// As linhas de saída são enviadas pelo canal `progress_tx` (std::sync::mpsc). /// As linhas de saída são enviadas pelo canal `progress_tx` (std::sync::mpsc).
pub async fn run_ffmpeg_async(args: Vec<String>, progress_tx: Sender<String>) -> Result<()> { /// Um sinal enviado em `cancel_rx` encerra o processo imediatamente.
pub async fn run_ffmpeg_async(
args: Vec<String>,
progress_tx: Sender<String>,
cancel_rx: tokio::sync::oneshot::Receiver<()>,
) -> Result<()> {
let mut child = Command::new("ffmpeg") let mut child = Command::new("ffmpeg")
.args(&args) .args(&args)
.stderr(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped())
@@ -25,16 +30,22 @@ pub async fn run_ffmpeg_async(args: Vec<String>, progress_tx: Sender<String>) ->
}); });
} }
let status = child.wait().await.context("Erro ao aguardar ffmpeg")?; tokio::select! {
result = child.wait() => {
let status = result.context("Erro ao aguardar ffmpeg")?;
if !status.success() { if !status.success() {
return Err(anyhow!( return Err(anyhow!(
"FFmpeg encerrou com código de saída: {:?}", "FFmpeg encerrou com código de saída: {:?}",
status.code() status.code()
)); ));
} }
Ok(()) Ok(())
}
_ = cancel_rx => {
let _ = child.kill().await;
Err(anyhow!("Cancelado pelo usuário"))
}
}
} }
/// Verifica se um binário está disponível no PATH. /// Verifica se um binário está disponível no PATH.
+22 -2
View File
@@ -45,6 +45,9 @@ pub struct App {
// Canal de comunicação do background thread // Canal de comunicação do background thread
bg_rx: Option<mpsc::Receiver<BackgroundMsg>>, bg_rx: Option<mpsc::Receiver<BackgroundMsg>>,
// Sender para cancelar a geração em andamento
cancel_tx: Option<tokio::sync::oneshot::Sender<()>>,
// Mensagem de erro global (ex: dependências ausentes) // Mensagem de erro global (ex: dependências ausentes)
global_error: Option<String>, global_error: Option<String>,
} }
@@ -68,6 +71,7 @@ impl App {
add_subtitle_form: AddSubtitleForm::new(), add_subtitle_form: AddSubtitleForm::new(),
execution_panel: ExecutionPanel::new(), execution_panel: ExecutionPanel::new(),
bg_rx: None, bg_rx: None,
cancel_tx: None,
global_error, global_error,
} }
} }
@@ -148,6 +152,9 @@ impl App {
self.execution_panel.state = ExecutionState::Running; self.execution_panel.state = ExecutionState::Running;
self.execution_panel.log_lines.clear(); self.execution_panel.log_lines.clear();
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
self.cancel_tx = Some(cancel_tx);
std::thread::spawn(move || { std::thread::spawn(move || {
// Canal std para receber linhas de log do run_ffmpeg_async // Canal std para receber linhas de log do run_ffmpeg_async
let (log_tx, log_rx) = std::sync::mpsc::channel::<String>(); let (log_tx, log_rx) = std::sync::mpsc::channel::<String>();
@@ -163,7 +170,7 @@ impl App {
}); });
let rt = tokio::runtime::Runtime::new().expect("Falha ao criar runtime tokio"); let rt = tokio::runtime::Runtime::new().expect("Falha ao criar runtime tokio");
let result = rt.block_on(run_ffmpeg_async(args, log_tx)); let result = rt.block_on(run_ffmpeg_async(args, log_tx, cancel_rx));
// Aguarda o encaminhador consumir todas as linhas pendentes // Aguarda o encaminhador consumir todas as linhas pendentes
// antes de enviar Done/Error, garantindo ordem correta no log // antes de enviar Done/Error, garantindo ordem correta no log
@@ -180,6 +187,15 @@ impl App {
ctx.request_repaint(); ctx.request_repaint();
}); });
} }
/// Cancela a geração em andamento, encerrando o processo FFmpeg.
fn cancel_generation(&mut self) {
if let Some(tx) = self.cancel_tx.take() {
let _ = tx.send(());
}
self.bg_rx = None;
self.execution_panel.state = ExecutionState::Cancelled;
}
} }
impl eframe::App for App { impl eframe::App for App {
@@ -215,10 +231,14 @@ impl eframe::App for App {
.show(ctx, |ui| { .show(ctx, |ui| {
ui.add_space(4.0); ui.add_space(4.0);
let can_generate = self.pending_source.is_some() && self.pending_output.is_some(); let can_generate = self.pending_source.is_some() && self.pending_output.is_some();
if self.execution_panel.ui(ui, can_generate) { let (generate, cancel) = self.execution_panel.ui(ui, can_generate);
if generate {
let ctx_clone = ctx.clone(); let ctx_clone = ctx.clone();
self.start_generation(ctx_clone); self.start_generation(ctx_clone);
} }
if cancel {
self.cancel_generation();
}
ui.add_space(4.0); ui.add_space(4.0);
}); });
+22 -5
View File
@@ -7,6 +7,7 @@ pub enum ExecutionState {
Idle, Idle,
Running, Running,
Success, Success,
Cancelled,
Error(String), Error(String),
} }
@@ -32,15 +33,18 @@ impl ExecutionPanel {
} }
} }
/// Renderiza o painel. Retorna `true` se o botão "Gerar MKV" foi clicado. /// Renderiza o painel.
pub fn ui(&mut self, ui: &mut egui::Ui, can_generate: bool) -> bool { /// Retorna `(generate_clicked, cancel_clicked)`.
let mut clicked = false; pub fn ui(&mut self, ui: &mut egui::Ui, can_generate: bool) -> (bool, bool) {
let mut generate_clicked = false;
let mut cancel_clicked = false;
ui.group(|ui| { ui.group(|ui| {
ui.heading("Gerar arquivo"); ui.heading("Gerar arquivo");
let is_running = self.state == ExecutionState::Running; let is_running = self.state == ExecutionState::Running;
ui.horizontal(|ui| {
if ui if ui
.add_enabled( .add_enabled(
can_generate && !is_running, can_generate && !is_running,
@@ -52,9 +56,19 @@ impl ExecutionPanel {
) )
.clicked() .clicked()
{ {
clicked = true; generate_clicked = true;
} }
if is_running
&& ui
.add(egui::Button::new("Cancelar").fill(egui::Color32::DARK_RED))
.on_hover_text("Interromper o processamento")
.clicked()
{
cancel_clicked = true;
}
});
match &self.state { match &self.state {
ExecutionState::Idle => {} ExecutionState::Idle => {}
ExecutionState::Running => { ExecutionState::Running => {
@@ -64,6 +78,9 @@ impl ExecutionPanel {
ExecutionState::Success => { ExecutionState::Success => {
ui.colored_label(egui::Color32::GREEN, "✓ Arquivo gerado com sucesso!"); ui.colored_label(egui::Color32::GREEN, "✓ Arquivo gerado com sucesso!");
} }
ExecutionState::Cancelled => {
ui.colored_label(egui::Color32::YELLOW, "⊘ Geração cancelada pelo usuário.");
}
ExecutionState::Error(msg) => { ExecutionState::Error(msg) => {
ui.colored_label(egui::Color32::RED, "✗ Erro durante o processamento:"); ui.colored_label(egui::Color32::RED, "✗ Erro durante o processamento:");
ui.add( ui.add(
@@ -88,6 +105,6 @@ impl ExecutionPanel {
} }
}); });
clicked (generate_clicked, cancel_clicked)
} }
} }