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
+67 -3
View File
@@ -7,19 +7,24 @@ use std::collections::HashMap;
/// Evento emitido por interações com a lista de faixas existentes.
pub enum ExistingTrackEvent {
OffsetChanged(TrackId, SyncOffset),
/// O fator de escala temporal de uma faixa existente foi alterado.
DriftChanged(TrackId, f64),
/// O usuário solicitou exportar a faixa identificada por `TrackId`.
ExportRequested(TrackId),
}
/// Lista as faixas detectadas no arquivo de vídeo e permite editar o offset de cada uma.
/// Lista as faixas detectadas no arquivo de vídeo e permite editar o offset e o drift de cada uma.
pub struct ExistingTrackList {
offset_fields: HashMap<u32, SyncOffsetField>,
/// Campos de drift raw por track id (percentual digitado pelo usuário).
drift_fields: HashMap<u32, String>,
}
impl ExistingTrackList {
pub fn new() -> Self {
ExistingTrackList {
offset_fields: HashMap::new(),
drift_fields: HashMap::new(),
}
}
@@ -29,14 +34,20 @@ impl ExistingTrackList {
self.offset_fields
.entry(track.id.val())
.or_insert_with(|| SyncOffsetField::new(track.offset));
self.drift_fields
.entry(track.id.val())
.or_insert_with(|| format!("{:.5}", track.drift_scale * 100.0));
}
}
/// Renderiza a lista. Retorna eventos de alteração de offset.
/// Renderiza a lista. Retorna eventos de alteração de offset e drift.
///
/// `mkvmerge_available`: quando `true`, exibe a coluna de velocidade original (%).
pub fn ui(
&mut self,
ui: &mut egui::Ui,
tracks: &[MediaTrackInfo],
mkvmerge_available: bool,
) -> Vec<ExistingTrackEvent> {
let mut events = Vec::new();
@@ -48,18 +59,24 @@ impl ExistingTrackList {
return;
}
let num_cols = if mkvmerge_available { 7 } else { 6 };
egui::Grid::new("existing_tracks_grid")
.num_columns(5)
.num_columns(num_cols)
.striped(true)
.show(ui, |ui| {
ui.strong("#");
ui.strong("Tipo");
ui.strong("Codec");
ui.strong("Duração");
ui.strong("Atraso");
if mkvmerge_available {
ui.strong("Velocidade (%)");
}
ui.strong("");
ui.end_row();
for track in tracks {
let editable = matches!(track.kind, TrackKind::Audio | TrackKind::Subtitle);
let kind_label = match track.kind {
TrackKind::Video => "Vídeo",
TrackKind::Audio => "Áudio",
@@ -76,6 +93,12 @@ impl ExistingTrackList {
ui.label(format!("{}{}", kind_label, lang));
ui.label(&track.codec);
// Duração
match track.duration_ms {
Some(ms) => ui.label(format_duration(ms)),
None => ui.label("-"),
};
// Offset field
let field = self
.offset_fields
@@ -89,6 +112,33 @@ impl ExistingTrackList {
}
}
// Drift field (somente para áudio e legenda quando mkvmerge disponível)
if mkvmerge_available {
if editable {
let drift_raw = self
.drift_fields
.entry(track.id.val())
.or_insert_with(|| format!("{:.5}", track.drift_scale * 100.0));
let resp = ui.add(
egui::TextEdit::singleline(drift_raw).desired_width(80.0),
);
if resp.changed() {
if let Ok(pct) = drift_raw.trim().parse::<f64>() {
if pct >= 1.0 && pct <= 999.99 {
let scale = pct / 100.0;
if (scale - track.drift_scale).abs() > 1e-9 {
events.push(ExistingTrackEvent::DriftChanged(
track.id, scale,
));
}
}
}
}
} else {
ui.label(""); // célula vazia para vídeo/dados
}
}
// Botão de exportar (somente Áudio e Legenda)
match track.kind {
TrackKind::Audio | TrackKind::Subtitle => {
@@ -109,3 +159,17 @@ impl ExistingTrackList {
events
}
}
/// Formata duração em ms para exibição: "3:45" ou "1:23:45".
fn format_duration(ms: u64) -> String {
let total_secs = ms / 1000;
let h = total_secs / 3600;
let m = (total_secs % 3600) / 60;
let s = total_secs % 60;
if h > 0 {
format!("{h}:{m:02}:{s:02}")
} else {
format!("{m}:{s:02}")
}
}