diff --git a/PROGRESS.md b/PROGRESS.md index e08dc14..63c4535 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1,7 +1,7 @@ # Progresso de Implementação **Data:** 28/02/2026 -**Status:** Fases 1–6 concluídas + exportação de faixa implementada — compilando, testes passando, aplicação executável +**Status:** Fases 1–6 concluídas + exportação de faixa + exibição/remoção de faixas adicionadas + layout responsivo + progresso em tempo real — compilando, testes passando, aplicação executável **Referência:** DEVELOPMENT_PLAN.md v1.0 --- @@ -11,8 +11,11 @@ Todas as 6 fases do plano de desenvolvimento foram implementadas. O projeto compila sem erros, 31 testes unitários passam e a aplicação pode ser executada com `cargo run`. -Após a conclusão das fases, foi implementada a funcionalidade de exportação de faixa de áudio -ou legenda diretamente da lista de faixas existentes. +Após a conclusão das fases, foram implementadas funcionalidades adicionais: +- Exportação de faixa de áudio ou legenda diretamente da lista de faixas existentes +- Exibição das faixas externas adicionadas com opção de remoção individual +- Layout responsivo com painéis fixos (cabeçalho, rodapé, barra lateral) e área central com scroll +- Progresso em tempo real via `run_ffmpeg_async` (Opção A: `std::sync::mpsc` em toda a cadeia) --- @@ -65,6 +68,7 @@ src/ │ ├── adjust_sync.rs — altera SyncOffset de faixa externa │ ├── edit_existing_track_sync.rs — altera SyncOffset de faixa existente │ ├── export_track.rs — exporta faixa de áudio ou legenda para arquivo +│ ├── remove_track.rs — remove faixa externa do projeto pelo TrackId │ ├── set_track_language.rs — altera idioma de faixa externa │ └── generate_output.rs — constrói comando e delega ao MediaProcessorPort │ @@ -91,8 +95,7 @@ src/ ├── mod.rs ├── video_selector.rs — seleção do vídeo base ├── output_selector.rs — seleção do arquivo de saída (.mkv) - ├── existing_track_list.rs — lista faixas detectadas + edição de offset - ├── add_audio_track_form.rs — formulário: áudio externo + ├── existing_track_list.rs — lista faixas detectadas + edição de offset│ ├── added_track_list.rs — lista faixas externas adicionadas + botão remover ├── add_audio_track_form.rs — formulário: áudio externo ├── add_subtitle_form.rs — formulário: legenda externa ├── sync_offset_field.rs — campo de atraso em segundos ├── language_field.rs — campo de idioma ISO 639-2 @@ -101,7 +104,7 @@ src/ --- -## Testes Unitários (31/31 passando) +## Testes Unitários (33/33 passando) ### Domain — Value Objects | Teste | Resultado | @@ -154,6 +157,12 @@ src/ | `export_track::exporta_legenda_output_e_ultimo_argumento` | ✅ | | `export_track::rejeita_faixa_de_video` — retorna erro para `TrackKind::Video` | ✅ | +### Application — RemoveTrack +| Teste | Resultado | +|-------|-----------| +| `remove_track::remove_faixa_existente` — faixa removida do Vec | ✅ | +| `remove_track::erro_se_id_inexistente` — retorna erro se id não existe | ✅ | + --- ## Dependências (Cargo.toml) @@ -184,8 +193,8 @@ rfd = "0.14" ### Funcional - [x] Exportar faixa de áudio ou legenda existente para arquivo separado -- [ ] Exibição de faixas externas adicionadas com opção de remover -- [ ] Progresso em tempo real via `run_ffmpeg_async` (infrastructure já implementada, UI usa thread bloqueante por ora) +- [x] Exibição de faixas externas adicionadas com opção de remover +- [x] Progresso em tempo real via `run_ffmpeg_async` — `run_ffmpeg_async` usa `std::sync::mpsc::Sender`; `start_generation` cria tokio Runtime + thread encaminhadora; cada linha de stderr do FFmpeg aparece no log antes do término - [ ] Exibição do comando gerado em modo debug ### Qualidade @@ -194,7 +203,7 @@ rfd = "0.14" - [ ] Corrigir warnings de `unused` e `dead_code` remanescentes ### UI -- [ ] Layout responsivo mais refinado (scroll nas listas longas) +- [x] Layout responsivo mais refinado - [ ] Persistência do estado do projeto em disco (serde já disponível) - [ ] Mensagens de erro mais amigáveis na UI diff --git a/src/application/use_cases/mod.rs b/src/application/use_cases/mod.rs index c0457d2..3ba9359 100644 --- a/src/application/use_cases/mod.rs +++ b/src/application/use_cases/mod.rs @@ -5,4 +5,5 @@ pub mod edit_existing_track_sync; pub mod export_track; pub mod generate_output; pub mod load_media_info; +pub mod remove_track; pub mod set_track_language; diff --git a/src/application/use_cases/remove_track.rs b/src/application/use_cases/remove_track.rs new file mode 100644 index 0000000..dc96d9d --- /dev/null +++ b/src/application/use_cases/remove_track.rs @@ -0,0 +1,54 @@ +use crate::domain::entities::Project; +use crate::domain::value_objects::TrackId; +use anyhow::{Result, anyhow}; + +/// Remove uma faixa externa do projeto pelo seu TrackId. +pub struct RemoveTrack; + +impl RemoveTrack { + pub fn execute(project: &mut Project, id: TrackId) -> Result<()> { + if project.remove_track(id) { + Ok(()) + } else { + Err(anyhow!("Faixa com id {:?} não encontrada.", id)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::domain::entities::{AudioTrack, MkvOutput, Project, Track, VideoFile}; + use crate::domain::value_objects::{FilePath, SyncOffset, TrackId, TrackLanguage}; + + fn make_project() -> Project { + Project::new( + VideoFile::new(FilePath::from("input.mkv")), + MkvOutput::new(FilePath::from("output.mkv")), + ) + .unwrap() + } + + #[test] + fn remove_faixa_existente() { + let mut project = make_project(); + let id = project.next_track_id(); + project.tracks.push(Track::Audio(AudioTrack::new( + id, + FilePath::from("audio.aac"), + SyncOffset::from_ms(0), + TrackLanguage::new("por").unwrap(), + ))); + assert_eq!(project.tracks.len(), 1); + + RemoveTrack::execute(&mut project, id).unwrap(); + assert!(project.tracks.is_empty()); + } + + #[test] + fn erro_se_id_inexistente() { + let mut project = make_project(); + let result = RemoveTrack::execute(&mut project, TrackId::new(99)); + assert!(result.is_err()); + } +} diff --git a/src/domain/entities/project.rs b/src/domain/entities/project.rs index 3a3f961..713747b 100644 --- a/src/domain/entities/project.rs +++ b/src/domain/entities/project.rs @@ -47,6 +47,13 @@ impl Project { pub fn find_existing_track_mut(&mut self, id: TrackId) -> Option<&mut MediaTrackInfo> { self.existing_tracks.iter_mut().find(|t| t.id == id) } + + /// Remove uma faixa externa pelo TrackId. Retorna true se a faixa foi encontrada e removida. + pub fn remove_track(&mut self, id: TrackId) -> bool { + let before = self.tracks.len(); + self.tracks.retain(|t| t.id() != id); + self.tracks.len() < before + } } #[cfg(test)] diff --git a/src/infrastructure/process/mod.rs b/src/infrastructure/process/mod.rs index 5559de6..266e388 100644 --- a/src/infrastructure/process/mod.rs +++ b/src/infrastructure/process/mod.rs @@ -1,14 +1,11 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result, anyhow}; +use std::sync::mpsc::Sender; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; -use tokio::sync::mpsc; /// Executa o FFmpeg de forma assíncrona, capturando stderr em tempo real. -/// As linhas de saída são enviadas pelo canal `progress_tx`. -pub async fn run_ffmpeg_async( - args: Vec, - progress_tx: mpsc::UnboundedSender, -) -> Result<()> { +/// As linhas de saída são enviadas pelo canal `progress_tx` (std::sync::mpsc). +pub async fn run_ffmpeg_async(args: Vec, progress_tx: Sender) -> Result<()> { let mut child = Command::new("ffmpeg") .args(&args) .stderr(std::process::Stdio::piped()) @@ -47,7 +44,12 @@ pub fn check_binary_available(binary: &str) -> Result<()> { .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .status() - .map_err(|_| anyhow!("'{}' não encontrado no PATH. Por favor, instale o FFmpeg.", binary))?; + .map_err(|_| { + anyhow!( + "'{}' não encontrado no PATH. Por favor, instale o FFmpeg.", + binary + ) + })?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 04be7e9..fa6a157 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,8 +9,8 @@ fn main() -> eframe::Result<()> { let native_options = eframe::NativeOptions { viewport: eframe::egui::ViewportBuilder::default() .with_title("Editor de Faixas de Mídia") - .with_inner_size([900.0, 700.0]) - .with_min_inner_size([600.0, 400.0]), + .with_inner_size([1100.0, 720.0]) + .with_min_inner_size([700.0, 500.0]), ..Default::default() }; diff --git a/src/ui/app.rs b/src/ui/app.rs index 332f487..dd4cad4 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -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::(); + + // 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); - } + }); }); } } diff --git a/src/ui/components/added_track_list.rs b/src/ui/components/added_track_list.rs new file mode 100644 index 0000000..a423cba --- /dev/null +++ b/src/ui/components/added_track_list.rs @@ -0,0 +1,86 @@ +use crate::domain::entities::Track; +use crate::domain::value_objects::TrackId; +use eframe::egui; + +/// Evento emitido por interações com a lista de faixas externas adicionadas. +pub enum AddedTrackEvent { + RemoveRequested(TrackId), +} + +/// Lista as faixas externas adicionadas pelo usuário e permite removê-las. +pub struct AddedTrackList; + +impl AddedTrackList { + pub fn new() -> Self { + AddedTrackList + } + + /// Renderiza a lista. Retorna eventos de remoção. + pub fn ui(&mut self, ui: &mut egui::Ui, tracks: &[Track]) -> Vec { + let mut events = Vec::new(); + + ui.group(|ui| { + ui.heading("Faixas adicionadas"); + + if tracks.is_empty() { + ui.label("Nenhuma faixa adicionada."); + return; + } + + egui::Grid::new("added_tracks_grid") + .num_columns(4) + .striped(true) + .show(ui, |ui| { + ui.strong("Tipo"); + ui.strong("Arquivo"); + ui.strong("Idioma"); + ui.strong(""); + ui.end_row(); + + for track in tracks { + let (kind_label, file_name, language) = match track { + Track::Audio(t) => ( + "Áudio", + t.path + .as_path() + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("—") + .to_string(), + t.language.as_str().to_string(), + ), + Track::Subtitle(t) => ( + "Legenda", + t.path + .as_path() + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("—") + .to_string(), + t.language.as_str().to_string(), + ), + }; + + ui.label(kind_label); + ui.label(&file_name).on_hover_text(track_full_path(track)); + ui.label(language); + + if ui.small_button("🗑 Remover").clicked() { + events.push(AddedTrackEvent::RemoveRequested(track.id())); + } + + ui.end_row(); + } + }); + }); + + events + } +} + +fn track_full_path(track: &Track) -> String { + match track { + Track::Audio(t) => t.path.to_string_lossy().into_owned(), + Track::Subtitle(t) => t.path.to_string_lossy().into_owned(), + } +} diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index c3ebf92..7bafafe 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -1,5 +1,6 @@ pub mod add_audio_track_form; pub mod add_subtitle_form; +pub mod added_track_list; pub mod execution_panel; pub mod existing_track_list; pub mod language_field;