feat: add AdjustExistingTrackDrift use case to adjust drift scale of existing tracks

- Implemented AdjustExistingTrackDrift struct with execute method to modify drift scale of existing tracks in a project.
- Added tests for adjusting drift scale, handling non-existent track IDs, and invalid scale values.
- Updated MediaTrackInfo and AudioTrack entities to include drift_scale field.
- Enhanced Project entity with needs_mkvmerge method to determine if mkvmerge is required based on drift scale.
- Integrated drift scale adjustments into existing track list UI, allowing users to modify drift values.
- Updated various use cases and UI components to support drift scale functionality, including add audio/subtitle forms and batch processing.
- Implemented mkvmerge availability check and integrated it into the application workflow for conditional processing.
This commit is contained in:
2026-03-01 12:01:39 -03:00
parent 285f22e2d1
commit 1ef47b3a07
31 changed files with 1081 additions and 62 deletions
+43 -14
View File
@@ -1,13 +1,15 @@
use crate::adapters::ffmpeg::{FfmpegCommandBuilder, FfmpegGateway, FfprobeGateway};
use crate::adapters::mkvmerge::MkvmergeCommandBuilder;
use crate::adapters::filesystem::file_picker::FilePickerAdapter;
use crate::application::use_cases::{
add_audio_track::AddAudioTrack, add_subtitle::AddSubtitle,
adjust_existing_track_drift::AdjustExistingTrackDrift,
edit_existing_track_sync::EditExistingTrackSync, export_track::ExportTrack,
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::infrastructure::process::{run_ffmpeg_async, run_mkvmerge_async};
use crate::ui::components::{
add_audio_track_form::AddAudioTrackForm,
add_subtitle_form::AddSubtitleForm,
@@ -70,6 +72,8 @@ pub struct App {
batch_panel: BatchPanel,
// Índice do item sendo processado no lote (None = nenhum)
batch_processing_index: Option<usize>,
// mkvmerge disponível no PATH (verificação soft na inicialização)
mkvmerge_available: bool,
}
impl App {
@@ -109,6 +113,7 @@ impl App {
batch_items: Vec::new(),
batch_panel: BatchPanel::new(),
batch_processing_index: None,
mkvmerge_available: crate::infrastructure::process::mkvmerge_available(),
}
}
@@ -221,13 +226,19 @@ impl App {
}
/// Inicia geração do MKV em background thread com progresso em tempo real.
/// Despacha para FFmpeg ou mkvmerge dependendo de `project.needs_mkvmerge()`.
fn start_generation(&mut self, ctx: egui::Context) {
let project = match &self.project {
Some(p) => p.clone(),
None => return,
};
let args = FfmpegCommandBuilder::build(&project);
let uses_mkvmerge = project.needs_mkvmerge();
let args = if uses_mkvmerge {
MkvmergeCommandBuilder::build(&project)
} else {
FfmpegCommandBuilder::build(&project)
};
let (tx, rx) = mpsc::channel::<BackgroundMsg>();
self.bg_rx = Some(rx);
self.execution_panel.state = ExecutionState::Running;
@@ -237,7 +248,7 @@ impl App {
self.cancel_tx = Some(cancel_tx);
std::thread::spawn(move || {
// Canal std para receber linhas de log do run_ffmpeg_async
// Canal std para receber linhas de log
let (log_tx, log_rx) = std::sync::mpsc::channel::<String>();
// Thread auxiliar: encaminha cada linha de log para o canal da UI
@@ -251,10 +262,12 @@ 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, cancel_rx));
let result = if uses_mkvmerge {
rt.block_on(run_mkvmerge_async(args, log_tx, cancel_rx))
} else {
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
let _ = fwd_handle.join();
match result {
@@ -279,13 +292,19 @@ impl App {
}
/// Inicia o processamento de um item específico do carrinho de lote.
/// Despacha para FFmpeg ou mkvmerge dependendo de `project.needs_mkvmerge()`.
fn start_batch_item(&mut self, index: usize, ctx: egui::Context) {
let project = self.batch_items[index].project.clone();
self.batch_items[index].state = ExecutionState::Running;
self.batch_items[index].log_lines.clear();
self.batch_processing_index = Some(index);
let args = FfmpegCommandBuilder::build(&project);
let uses_mkvmerge = project.needs_mkvmerge();
let args = if uses_mkvmerge {
MkvmergeCommandBuilder::build(&project)
} else {
FfmpegCommandBuilder::build(&project)
};
let (tx, rx) = mpsc::channel::<BackgroundMsg>();
self.bg_rx = Some(rx);
@@ -305,7 +324,11 @@ 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, cancel_rx));
let result = if uses_mkvmerge {
rt.block_on(run_mkvmerge_async(args, log_tx, cancel_rx))
} else {
rt.block_on(run_ffmpeg_async(args, log_tx, cancel_rx))
};
let _ = fwd_handle.join();
match result {
@@ -577,7 +600,7 @@ impl eframe::App for App {
.auto_shrink([false, false])
.show(ui, |ui| {
let is_processing = self.batch_processing_index.is_some();
let events = self.batch_panel.ui(ui, &self.batch_items, is_processing);
let events = self.batch_panel.ui(ui, &self.batch_items, is_processing, self.mkvmerge_available);
for event in events {
match event {
BatchPanelEvent::AddItem(mut project) => {
@@ -621,7 +644,8 @@ impl eframe::App for App {
.show(ui, |ui| {
if let Some(project) = &mut self.project {
// ── Faixas existentes ─────────────────────────────────
let events = self.existing_track_list.ui(ui, &project.existing_tracks);
let mkvmerge_available = self.mkvmerge_available;
let events = self.existing_track_list.ui(ui, &project.existing_tracks, mkvmerge_available);
for event in events {
use crate::domain::entities::TrackKind;
use crate::ui::components::existing_track_list::ExistingTrackEvent;
@@ -629,6 +653,9 @@ impl eframe::App for App {
ExistingTrackEvent::OffsetChanged(id, offset) => {
let _ = EditExistingTrackSync::execute(project, id, offset);
}
ExistingTrackEvent::DriftChanged(id, scale) => {
let _ = AdjustExistingTrackDrift::execute(project, id, scale);
}
ExistingTrackEvent::ExportRequested(id) => {
if let Some(track) =
project.existing_tracks.iter().find(|t| t.id == id).cloned()
@@ -697,7 +724,7 @@ impl eframe::App for App {
.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),
|ui| self.add_audio_form.ui(ui, mkvmerge_available),
)
.inner;
@@ -705,15 +732,15 @@ impl eframe::App for App {
.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),
|ui| self.add_subtitle_form.ui(ui, mkvmerge_available),
)
.inner;
(audio_req, subtitle_req)
} else {
let ar = self.add_audio_form.ui(ui);
let ar = self.add_audio_form.ui(ui, mkvmerge_available);
ui.add_space(4.0);
let sr = self.add_subtitle_form.ui(ui);
let sr = self.add_subtitle_form.ui(ui, mkvmerge_available);
(ar, sr)
};
@@ -725,6 +752,7 @@ impl eframe::App for App {
req.language,
req.is_default,
req.title,
req.drift_scale,
);
}
if let Some(req) = subtitle_req {
@@ -735,6 +763,7 @@ impl eframe::App for App {
req.language,
req.is_default,
req.title,
req.drift_scale,
);
}