Files
simple-multimidia-track-aud…/src/ui/components/existing_track_list.rs
T
Felipe 1ef47b3a07 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.
2026-03-01 12:01:39 -03:00

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}")
}
}