- 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.
176 lines
6.9 KiB
Rust
176 lines
6.9 KiB
Rust
use eframe::egui;
|
|
use crate::domain::entities::{MediaTrackInfo, TrackKind};
|
|
use crate::domain::value_objects::{SyncOffset, TrackId};
|
|
use crate::ui::components::sync_offset_field::SyncOffsetField;
|
|
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 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(),
|
|
}
|
|
}
|
|
|
|
/// Sincroniza os campos de offset com as faixas atuais do projeto.
|
|
pub fn sync_tracks(&mut self, tracks: &[MediaTrackInfo]) {
|
|
for track in tracks {
|
|
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 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();
|
|
|
|
ui.group(|ui| {
|
|
ui.heading("Faixas existentes");
|
|
|
|
if tracks.is_empty() {
|
|
ui.label("Nenhuma faixa detectada.");
|
|
return;
|
|
}
|
|
|
|
let num_cols = if mkvmerge_available { 7 } else { 6 };
|
|
egui::Grid::new("existing_tracks_grid")
|
|
.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",
|
|
TrackKind::Subtitle => "Legenda",
|
|
TrackKind::Data => "Dados",
|
|
};
|
|
let lang = track
|
|
.language
|
|
.as_ref()
|
|
.map(|l| format!(" ({})", l))
|
|
.unwrap_or_default();
|
|
|
|
ui.label(format!("{}", track.stream_index));
|
|
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
|
|
.entry(track.id.val())
|
|
.or_insert_with(|| SyncOffsetField::new(track.offset));
|
|
|
|
field.ui(ui, "");
|
|
if let Some(offset) = field.parse() {
|
|
if offset != track.offset {
|
|
events.push(ExistingTrackEvent::OffsetChanged(track.id, offset));
|
|
}
|
|
}
|
|
|
|
// 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 => {
|
|
if ui.small_button("⬇ Exportar").clicked() {
|
|
events.push(ExistingTrackEvent::ExportRequested(track.id));
|
|
}
|
|
}
|
|
_ => {
|
|
ui.label(""); // célula vazia para alinhamento
|
|
}
|
|
}
|
|
|
|
ui.end_row();
|
|
}
|
|
});
|
|
});
|
|
|
|
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}")
|
|
}
|
|
}
|
|
|