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
|
# Progresso de Implementação
|
||||||
|
|
||||||
**Data:** 28/02/2026
|
**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
|
**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
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user