feat: adiciona funcionalidade de remoção de faixas externas e atualiza o layout da UI
This commit is contained in:
+222
-119
@@ -1,20 +1,24 @@
|
||||
use eframe::egui;
|
||||
use std::sync::mpsc;
|
||||
use crate::adapters::ffmpeg::{FfmpegCommandBuilder, FfmpegGateway, FfprobeGateway};
|
||||
use crate::adapters::filesystem::file_picker::FilePickerAdapter;
|
||||
use crate::application::ports::MediaProcessorPort;
|
||||
use crate::application::use_cases::{
|
||||
add_audio_track::AddAudioTrack, add_subtitle::AddSubtitle,
|
||||
edit_existing_track_sync::EditExistingTrackSync, export_track::ExportTrack,
|
||||
load_media_info::LoadMediaInfo,
|
||||
load_media_info::LoadMediaInfo, remove_track::RemoveTrack,
|
||||
};
|
||||
use crate::domain::entities::{MkvOutput, Project, VideoFile};
|
||||
use crate::domain::value_objects::FilePath;
|
||||
use crate::infrastructure::process::run_ffmpeg_async;
|
||||
use crate::ui::components::{
|
||||
add_audio_track_form::AddAudioTrackForm, add_subtitle_form::AddSubtitleForm,
|
||||
execution_panel::{ExecutionPanel, ExecutionState}, existing_track_list::ExistingTrackList,
|
||||
output_selector::OutputSelector, video_selector::VideoSelector,
|
||||
add_audio_track_form::AddAudioTrackForm,
|
||||
add_subtitle_form::AddSubtitleForm,
|
||||
added_track_list::AddedTrackList,
|
||||
execution_panel::{ExecutionPanel, ExecutionState},
|
||||
existing_track_list::ExistingTrackList,
|
||||
output_selector::OutputSelector,
|
||||
video_selector::VideoSelector,
|
||||
};
|
||||
use eframe::egui;
|
||||
use std::sync::mpsc;
|
||||
|
||||
/// Mensagens enviadas do background thread para a UI.
|
||||
enum BackgroundMsg {
|
||||
@@ -33,6 +37,7 @@ pub struct App {
|
||||
video_selector: VideoSelector,
|
||||
output_selector: OutputSelector,
|
||||
existing_track_list: ExistingTrackList,
|
||||
added_track_list: AddedTrackList,
|
||||
add_audio_form: AddAudioTrackForm,
|
||||
add_subtitle_form: AddSubtitleForm,
|
||||
execution_panel: ExecutionPanel,
|
||||
@@ -47,7 +52,8 @@ pub struct App {
|
||||
impl App {
|
||||
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
// Verifica dependências na inicialização
|
||||
let global_error = crate::infrastructure::process::validate_dependencies().err()
|
||||
let global_error = crate::infrastructure::process::validate_dependencies()
|
||||
.err()
|
||||
.map(|e| e.to_string());
|
||||
|
||||
App {
|
||||
@@ -57,6 +63,7 @@ impl App {
|
||||
video_selector: VideoSelector::new(),
|
||||
output_selector: OutputSelector::new(),
|
||||
existing_track_list: ExistingTrackList::new(),
|
||||
added_track_list: AddedTrackList::new(),
|
||||
add_audio_form: AddAudioTrackForm::new(),
|
||||
add_subtitle_form: AddSubtitleForm::new(),
|
||||
execution_panel: ExecutionPanel::new(),
|
||||
@@ -81,7 +88,8 @@ impl App {
|
||||
let probe = FfprobeGateway;
|
||||
match LoadMediaInfo::execute(&mut project, &probe) {
|
||||
Ok(_) => {
|
||||
self.existing_track_list.sync_tracks(&project.existing_tracks);
|
||||
self.existing_track_list
|
||||
.sync_tracks(&project.existing_tracks);
|
||||
}
|
||||
Err(e) => {
|
||||
self.execution_panel.state = ExecutionState::Error(e.to_string());
|
||||
@@ -127,7 +135,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inicia geração do MKV em background thread.
|
||||
/// Inicia geração do MKV em background thread com progresso em tempo real.
|
||||
fn start_generation(&mut self, ctx: egui::Context) {
|
||||
let project = match &self.project {
|
||||
Some(p) => p.clone(),
|
||||
@@ -141,8 +149,27 @@ impl App {
|
||||
self.execution_panel.log_lines.clear();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let gateway = FfmpegGateway;
|
||||
match gateway.execute(args) {
|
||||
// Canal std para receber linhas de log do run_ffmpeg_async
|
||||
let (log_tx, log_rx) = std::sync::mpsc::channel::<String>();
|
||||
|
||||
// Thread auxiliar: encaminha cada linha de log para o canal da UI
|
||||
let fwd_tx = tx.clone();
|
||||
let fwd_ctx = ctx.clone();
|
||||
let fwd_handle = std::thread::spawn(move || {
|
||||
for line in log_rx {
|
||||
let _ = fwd_tx.send(BackgroundMsg::LogLine(line));
|
||||
fwd_ctx.request_repaint();
|
||||
}
|
||||
});
|
||||
|
||||
let rt = tokio::runtime::Runtime::new().expect("Falha ao criar runtime tokio");
|
||||
let result = rt.block_on(run_ffmpeg_async(args, log_tx));
|
||||
|
||||
// Aguarda o encaminhador consumir todas as linhas pendentes
|
||||
// antes de enviar Done/Error, garantindo ordem correta no log
|
||||
let _ = fwd_handle.join();
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let _ = tx.send(BackgroundMsg::Done);
|
||||
}
|
||||
@@ -163,125 +190,201 @@ impl eframe::App for App {
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
// Erro global de dependências
|
||||
if let Some(ref err) = self.global_error {
|
||||
ui.colored_label(
|
||||
egui::Color32::RED,
|
||||
format!("⚠ Dependência ausente: {}", err),
|
||||
);
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
ui.heading("Editor de Faixas de Mídia");
|
||||
ui.add_space(8.0);
|
||||
|
||||
// ── Seleção de vídeo ──────────────────────────────────────────────
|
||||
if let Some(path) = self.video_selector.ui(ui) {
|
||||
self.pending_source = Some(path);
|
||||
if self.pending_output.is_some() {
|
||||
self.try_build_project();
|
||||
// ── Cabeçalho (fixo no topo) ──────────────────────────────────────────
|
||||
egui::TopBottomPanel::top("header_panel")
|
||||
.exact_height(if self.global_error.is_some() {
|
||||
58.0
|
||||
} else {
|
||||
38.0
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.heading("Editor de Faixas de Mídia");
|
||||
if let Some(ref err) = self.global_error {
|
||||
ui.add_space(2.0);
|
||||
ui.colored_label(
|
||||
egui::Color32::RED,
|
||||
format!("⚠ Dependência ausente: {}", err),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(4.0);
|
||||
|
||||
// ── Seleção de saída ──────────────────────────────────────────────
|
||||
if let Some(path) = self.output_selector.ui(ui) {
|
||||
self.pending_output = Some(path);
|
||||
if self.pending_source.is_some() {
|
||||
self.try_build_project();
|
||||
// ── Painel de execução (fixo no rodapé) ───────────────────────────────
|
||||
egui::TopBottomPanel::bottom("execution_panel")
|
||||
.resizable(false)
|
||||
.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 ctx_clone = ctx.clone();
|
||||
self.start_generation(ctx_clone);
|
||||
}
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
// ── Barra lateral esquerda: seleção de arquivos ───────────────────────
|
||||
egui::SidePanel::left("sidebar_panel")
|
||||
.min_width(220.0)
|
||||
.max_width(400.0)
|
||||
.resizable(true)
|
||||
.show(ctx, |ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(8.0);
|
||||
|
||||
if let Some(project) = &mut self.project {
|
||||
// ── Faixas existentes ─────────────────────────────────────────
|
||||
let events =
|
||||
self.existing_track_list.ui(ui, &project.existing_tracks);
|
||||
for event in events {
|
||||
use crate::ui::components::existing_track_list::ExistingTrackEvent;
|
||||
use crate::domain::entities::TrackKind;
|
||||
match event {
|
||||
ExistingTrackEvent::OffsetChanged(id, offset) => {
|
||||
let _ = EditExistingTrackSync::execute(project, id, offset);
|
||||
// ── Seleção de vídeo ──────────────────────────────────
|
||||
if let Some(path) = self.video_selector.ui(ui) {
|
||||
self.pending_source = Some(path);
|
||||
if self.pending_output.is_some() {
|
||||
self.try_build_project();
|
||||
}
|
||||
}
|
||||
ExistingTrackEvent::ExportRequested(id) => {
|
||||
// Localiza a faixa pelo id
|
||||
if let Some(track) = project
|
||||
.existing_tracks
|
||||
.iter()
|
||||
.find(|t| t.id == id)
|
||||
.cloned()
|
||||
{
|
||||
let maybe_output = match track.kind {
|
||||
TrackKind::Audio => FilePickerAdapter::save_audio(&track.codec),
|
||||
TrackKind::Subtitle => FilePickerAdapter::save_subtitle(&track.codec),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(output_path) = maybe_output {
|
||||
let gateway = FfmpegGateway;
|
||||
match ExportTrack::execute(
|
||||
&project.source.path,
|
||||
&track,
|
||||
&output_path,
|
||||
&gateway,
|
||||
) {
|
||||
Ok(_) => {
|
||||
self.execution_panel.state =
|
||||
ExecutionState::Success;
|
||||
self.execution_panel.add_log(format!(
|
||||
"Faixa exportada para: {}",
|
||||
output_path.to_string_lossy()
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
self.execution_panel.state =
|
||||
ExecutionState::Error(e.to_string());
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// ── Seleção de saída ──────────────────────────────────
|
||||
if let Some(path) = self.output_selector.ui(ui) {
|
||||
self.pending_output = Some(path);
|
||||
if self.pending_source.is_some() {
|
||||
self.try_build_project();
|
||||
}
|
||||
}
|
||||
|
||||
if self.project.is_none() {
|
||||
ui.add_space(16.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Selecione o arquivo de vídeo e o destino \
|
||||
de saída para começar.",
|
||||
)
|
||||
.italics()
|
||||
.weak(),
|
||||
);
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Painel central: faixas e formulários ─────────────────────────────
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| {
|
||||
if let Some(project) = &mut self.project {
|
||||
// ── Faixas existentes ─────────────────────────────────
|
||||
let events = self.existing_track_list.ui(ui, &project.existing_tracks);
|
||||
for event in events {
|
||||
use crate::domain::entities::TrackKind;
|
||||
use crate::ui::components::existing_track_list::ExistingTrackEvent;
|
||||
match event {
|
||||
ExistingTrackEvent::OffsetChanged(id, offset) => {
|
||||
let _ = EditExistingTrackSync::execute(project, id, offset);
|
||||
}
|
||||
ExistingTrackEvent::ExportRequested(id) => {
|
||||
if let Some(track) =
|
||||
project.existing_tracks.iter().find(|t| t.id == id).cloned()
|
||||
{
|
||||
let maybe_output = match track.kind {
|
||||
TrackKind::Audio => {
|
||||
FilePickerAdapter::save_audio(&track.codec)
|
||||
}
|
||||
TrackKind::Subtitle => {
|
||||
FilePickerAdapter::save_subtitle(&track.codec)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(output_path) = maybe_output {
|
||||
let gateway = FfmpegGateway;
|
||||
match ExportTrack::execute(
|
||||
&project.source.path,
|
||||
&track,
|
||||
&output_path,
|
||||
&gateway,
|
||||
) {
|
||||
Ok(_) => {
|
||||
self.execution_panel.state =
|
||||
ExecutionState::Success;
|
||||
self.execution_panel.add_log(format!(
|
||||
"Faixa exportada para: {}",
|
||||
output_path.to_string_lossy()
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
self.execution_panel.state =
|
||||
ExecutionState::Error(e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// ── Formulários: lado a lado se largura >= 600 px ─────
|
||||
// Executa o render fora do bloco de borrow de `project`
|
||||
// e coleta as requisições para aplicar em seguida.
|
||||
let wide = ui.available_width() >= 600.0;
|
||||
|
||||
let (audio_req, subtitle_req) = if wide {
|
||||
// Renderiza dois painéis horizontais de largura igual
|
||||
let available = ui.available_width();
|
||||
let col_w = (available - ui.spacing().item_spacing.x) / 2.0;
|
||||
|
||||
let audio_req = ui
|
||||
.allocate_ui_with_layout(
|
||||
egui::Vec2::new(col_w, 0.0),
|
||||
egui::Layout::top_down(egui::Align::Min),
|
||||
|ui| self.add_audio_form.ui(ui),
|
||||
)
|
||||
.inner;
|
||||
|
||||
let subtitle_req = ui
|
||||
.allocate_ui_with_layout(
|
||||
egui::Vec2::new(col_w, 0.0),
|
||||
egui::Layout::top_down(egui::Align::Min),
|
||||
|ui| self.add_subtitle_form.ui(ui),
|
||||
)
|
||||
.inner;
|
||||
|
||||
(audio_req, subtitle_req)
|
||||
} else {
|
||||
let ar = self.add_audio_form.ui(ui);
|
||||
ui.add_space(4.0);
|
||||
let sr = self.add_subtitle_form.ui(ui);
|
||||
(ar, sr)
|
||||
};
|
||||
|
||||
if let Some(req) = audio_req {
|
||||
let _ =
|
||||
AddAudioTrack::execute(project, req.path, req.offset, req.language);
|
||||
}
|
||||
if let Some(req) = subtitle_req {
|
||||
let _ =
|
||||
AddSubtitle::execute(project, req.path, req.offset, req.language);
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// ── Faixas externas adicionadas ───────────────────────
|
||||
let remove_events = self.added_track_list.ui(ui, &project.tracks);
|
||||
for event in remove_events {
|
||||
use crate::ui::components::added_track_list::AddedTrackEvent;
|
||||
match event {
|
||||
AddedTrackEvent::RemoveRequested(id) => {
|
||||
let _ = RemoveTrack::execute(project, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// ── Adicionar áudio ───────────────────────────────────────────
|
||||
if let Some(req) = self.add_audio_form.ui(ui) {
|
||||
let _ = AddAudioTrack::execute(project, req.path, req.offset, req.language);
|
||||
}
|
||||
|
||||
ui.add_space(4.0);
|
||||
|
||||
// ── Adicionar legenda ─────────────────────────────────────────
|
||||
if let Some(req) = self.add_subtitle_form.ui(ui) {
|
||||
let _ = AddSubtitle::execute(project, req.path, req.offset, req.language);
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// ── Painel de execução ────────────────────────────────────────
|
||||
let can_generate =
|
||||
self.pending_source.is_some() && self.pending_output.is_some();
|
||||
if self.execution_panel.ui(ui, can_generate) {
|
||||
let ctx_clone = ctx.clone();
|
||||
self.start_generation(ctx_clone);
|
||||
}
|
||||
} else {
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Selecione o arquivo de vídeo e o destino de saída para começar.",
|
||||
)
|
||||
.italics(),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
let can_generate = self.pending_source.is_some() && self.pending_output.is_some();
|
||||
self.execution_panel.ui(ui, can_generate);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user