feat: adiciona funcionalidade de cancelamento de geração de faixas de áudio e atualiza o painel de execução
This commit is contained in:
+6
-4
@@ -1,7 +1,7 @@
|
||||
# Progresso de Implementação
|
||||
|
||||
**Data:** 28/02/2026
|
||||
**Status:** Fases 1–6 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 1–6 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
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(anyhow!(
|
||||
"FFmpeg encerrou com código de saída: {:?}",
|
||||
status.code()
|
||||
));
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verifica se um binário está disponível no PATH.
|
||||
|
||||
+22
-2
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ pub enum ExecutionState {
|
||||
Idle,
|
||||
Running,
|
||||
Success,
|
||||
Cancelled,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
@@ -32,28 +33,41 @@ 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;
|
||||
|
||||
if ui
|
||||
.add_enabled(
|
||||
can_generate && !is_running,
|
||||
egui::Button::new(if is_running {
|
||||
"Processando…"
|
||||
} else {
|
||||
"Gerar MKV"
|
||||
}),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
clicked = true;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add_enabled(
|
||||
can_generate && !is_running,
|
||||
egui::Button::new(if is_running {
|
||||
"Processando…"
|
||||
} else {
|
||||
"Gerar MKV"
|
||||
}),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
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 => {}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user