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
**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
---
@@ -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
- 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)
- Cancelamento de geração em andamento via botão "Cancelar" — encerra o processo FFmpeg filho imediatamente
---
@@ -86,11 +87,11 @@ src/
├── infrastructure/
│ ├── mod.rs
│ └── process/
└── mod.rs — run_ffmpeg_async (tokio), validate_dependencies()
└── mod.rs — run_ffmpeg_async (tokio, cancel via oneshot), validate_dependencies()
└── ui/
├── 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/
├── mod.rs
├── video_selector.rs — seleção do vídeo base
@@ -99,7 +100,7 @@ src/
├── add_subtitle_form.rs — formulário: legenda externa
├── sync_offset_field.rs — campo de atraso em segundos
├── 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] 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] 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
### 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.
/// 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")
.args(&args)
.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() {
return Err(anyhow!(
"FFmpeg encerrou com código de saída: {:?}",
status.code()
));
}
Ok(())
}
_ = cancel_rx => {
let _ = child.kill().await;
Err(anyhow!("Cancelado pelo usuário"))
}
}
}
/// 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
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)
global_error: Option<String>,
}
@@ -68,6 +71,7 @@ impl App {
add_subtitle_form: AddSubtitleForm::new(),
execution_panel: ExecutionPanel::new(),
bg_rx: None,
cancel_tx: None,
global_error,
}
}
@@ -148,6 +152,9 @@ impl App {
self.execution_panel.state = ExecutionState::Running;
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 || {
// Canal std para receber linhas de log do run_ffmpeg_async
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 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
// antes de enviar Done/Error, garantindo ordem correta no log
@@ -180,6 +187,15 @@ impl App {
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 {
@@ -215,10 +231,14 @@ impl eframe::App for App {
.show(ctx, |ui| {
ui.add_space(4.0);
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();
self.start_generation(ctx_clone);
}
if cancel {
self.cancel_generation();
}
ui.add_space(4.0);
});
+22 -5
View File
@@ -7,6 +7,7 @@ pub enum ExecutionState {
Idle,
Running,
Success,
Cancelled,
Error(String),
}
@@ -32,15 +33,18 @@ impl ExecutionPanel {
}
}
/// Renderiza o painel. Retorna `true` se o botão "Gerar MKV" foi clicado.
pub fn ui(&mut self, ui: &mut egui::Ui, can_generate: bool) -> bool {
let mut clicked = false;
/// Renderiza o painel.
/// Retorna `(generate_clicked, cancel_clicked)`.
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.heading("Gerar arquivo");
let is_running = self.state == ExecutionState::Running;
ui.horizontal(|ui| {
if ui
.add_enabled(
can_generate && !is_running,
@@ -52,9 +56,19 @@ impl ExecutionPanel {
)
.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 {
ExecutionState::Idle => {}
ExecutionState::Running => {
@@ -64,6 +78,9 @@ impl ExecutionPanel {
ExecutionState::Success => {
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) => {
ui.colored_label(egui::Color32::RED, "✗ Erro durante o processamento:");
ui.add(
@@ -88,6 +105,6 @@ impl ExecutionPanel {
}
});
clicked
(generate_clicked, cancel_clicked)
}
}