diff --git a/src/adapters/filesystem/file_picker.rs b/src/adapters/filesystem/file_picker.rs index 99d305a..573dc7c 100644 --- a/src/adapters/filesystem/file_picker.rs +++ b/src/adapters/filesystem/file_picker.rs @@ -5,6 +5,23 @@ use std::path::PathBuf; pub struct FilePickerAdapter; impl FilePickerAdapter { + // ─── Helper interno ────────────────────────────────────────────────────── + + /// Gera um nome de arquivo sugerido no formato `{base}_{tag}.{ext}`. + /// + /// Usado por todos os diálogos de salvamento para pré-preencher o nome com + /// base no arquivo de vídeo selecionado, evitando sobrescrever o original. + /// + /// Exemplos: + /// - `("filme", "output", "mkv")` → `"filme_output.mkv"` + /// - `("filme", "audio", "m4a")` → `"filme_audio.m4a"` + /// - `("filme", "legenda","srt")` → `"filme_legenda.srt"` + fn suggested_name(base: &str, tag: &str, ext: &str) -> String { + format!("{}_{}.{}", base, tag, ext) + } + + // ─── Diálogos de abertura ──────────────────────────────────────────────── + /// Abre diálogo de seleção de arquivo de vídeo. pub fn pick_video() -> Option { rfd::FileDialog::new() @@ -32,14 +49,19 @@ impl FilePickerAdapter { .map(|p: PathBuf| FilePath::from(p)) } + // ─── Diálogos de salvamento ────────────────────────────────────────────── + /// Abre diálogo de salvar arquivo MKV de saída. - pub fn pick_output() -> Option { + /// + /// `base_stem` é o nome do arquivo de vídeo base sem extensão (ex: `"filme"`). + /// O nome sugerido será `{base_stem}_output.mkv`. + pub fn pick_output(base_stem: &str) -> Option { rfd::FileDialog::new() .add_filter("MKV", &["mkv"]) - .set_file_name("output.mkv") + .set_file_name(Self::suggested_name(base_stem, "output", "mkv")) .save_file() .map(|mut p: PathBuf| { - // Força extensão .mkv + // Força extensão .mkv independente do que o usuário digitar p.set_extension("mkv"); FilePath::from(p) }) @@ -47,49 +69,66 @@ impl FilePickerAdapter { /// Abre diálogo de salvar faixa de áudio exportada. /// - /// A extensão padrão é inferida a partir do codec detectado pelo ffprobe - /// (ex: `aac` → `.aac`, `vorbis` → `.ogg`). Cai em `.mka` para codecs desconhecidos. - pub fn save_audio(codec: &str) -> Option { - let (ext, label) = match codec { - // AAC exportado como .m4a (container MPEG-4) em vez de .aac (ADTS raw). - // O formato ADTS não armazena duração total — o ffprobe estima pelo bitrate - // e pode reportar até dezenas de segundos a menos. O container M4A preserva - // a duração exata dos atoms do container original. - "aac" => ("m4a", "M4A (AAC)"), - "ac3" => ("ac3", "AC3"), - "eac3" => ("eac3", "E-AC3"), - "dts" => ("dts", "DTS"), - "mp3" => ("mp3", "MP3"), - "vorbis" => ("ogg", "Ogg Vorbis"), - "opus" => ("opus", "Opus"), - "flac" => ("flac", "FLAC"), - "truehd" => ("thd", "TrueHD"), - "pcm_s16le" | "pcm_s24le" | "pcm_s32le" => ("wav", "WAV"), - _ => ("mka", "Matroska Audio"), - }; + /// `base_stem` é o nome do arquivo de vídeo base sem extensão. + /// A extensão é inferida a partir do `codec` detectado pelo ffprobe + /// (ex: `aac` → `.m4a`, `vorbis` → `.ogg`). Cai em `.mka` para codecs desconhecidos. + /// O nome sugerido será `{base_stem}_audio.{ext}`. + pub fn save_audio(base_stem: &str, codec: &str) -> Option { + let (ext, label) = Self::audio_ext_and_label(codec); rfd::FileDialog::new() .add_filter(label, &[ext]) - .set_file_name(format!("track.{}", ext)) + .set_file_name(Self::suggested_name(base_stem, "audio", ext)) .save_file() .map(FilePath::from) } /// Abre diálogo de salvar faixa de legenda exportada. /// - /// A extensão padrão é inferida a partir do codec detectado pelo ffprobe + /// `base_stem` é o nome do arquivo de vídeo base sem extensão. + /// A extensão é inferida a partir do `codec` detectado pelo ffprobe /// (ex: `subrip` → `.srt`, `ass` → `.ass`). Cai em `.srt` para codecs desconhecidos. - pub fn save_subtitle(codec: &str) -> Option { - let (ext, label) = match codec { - "ass" | "ssa" => ("ass", "SubStation Alpha"), - "webvtt" => ("vtt", "WebVTT"), - "dvd_subtitle" => ("sub", "DVD Subtitle"), - "hdmv_pgs_subtitle" => ("sup", "PGS Subtitle"), - _ => ("srt", "SubRip"), - }; + /// O nome sugerido será `{base_stem}_legenda.{ext}`. + pub fn save_subtitle(base_stem: &str, codec: &str) -> Option { + let (ext, label) = Self::subtitle_ext_and_label(codec); rfd::FileDialog::new() .add_filter(label, &[ext]) - .set_file_name(format!("subtitle.{}", ext)) + .set_file_name(Self::suggested_name(base_stem, "legenda", ext)) .save_file() .map(FilePath::from) } + + // ─── Mapeamentos de codec → extensão ──────────────────────────────────── + + /// Retorna `(extensão, rótulo_filtro)` para um codec de áudio. + /// + /// AAC é exportado como `.m4a` (container MPEG-4) em vez de `.aac` (ADTS raw) + /// porque o formato ADTS não armazena duração total — o ffprobe estima pelo bitrate + /// e pode reportar até dezenas de segundos a menos. O container M4A preserva a + /// duração exata dos atoms do container original. + fn audio_ext_and_label(codec: &str) -> (&'static str, &'static str) { + match codec { + "aac" => ("m4a", "M4A (AAC)"), + "ac3" => ("ac3", "AC3"), + "eac3" => ("eac3", "E-AC3"), + "dts" => ("dts", "DTS"), + "mp3" => ("mp3", "MP3"), + "vorbis" => ("ogg", "Ogg Vorbis"), + "opus" => ("opus", "Opus"), + "flac" => ("flac", "FLAC"), + "truehd" => ("thd", "TrueHD"), + "pcm_s16le" | "pcm_s24le" | "pcm_s32le" => ("wav", "WAV"), + _ => ("mka", "Matroska Audio"), + } + } + + /// Retorna `(extensão, rótulo_filtro)` para um codec de legenda. + fn subtitle_ext_and_label(codec: &str) -> (&'static str, &'static str) { + match codec { + "ass" | "ssa" => ("ass", "SubStation Alpha"), + "webvtt" => ("vtt", "WebVTT"), + "dvd_subtitle" => ("sub", "DVD Subtitle"), + "hdmv_pgs_subtitle" => ("sup", "PGS Subtitle"), + _ => ("srt", "SubRip"), + } + } } diff --git a/src/ui/app.rs b/src/ui/app.rs index 5b1f5bb..f914fde 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -264,7 +264,13 @@ impl eframe::App for App { ui.add_space(8.0); // ── Seleção de saída ────────────────────────────────── - if let Some(path) = self.output_selector.ui(ui) { + // Passa o stem do vídeo para pré-preencher o nome sugerido + let source_stem: Option<&str> = self + .pending_source + .as_ref() + .and_then(|p| p.as_path().file_stem()) + .and_then(|s| s.to_str()); + if let Some(path) = self.output_selector.ui(ui, source_stem) { self.pending_output = Some(path); if self.pending_source.is_some() { self.try_build_project(); @@ -308,12 +314,20 @@ impl eframe::App for App { if let Some(track) = project.existing_tracks.iter().find(|t| t.id == id).cloned() { + // Stem do arquivo de vídeo base para nomear o arquivo exportado + let base_stem = project + .source + .path + .as_path() + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("track"); let maybe_output = match track.kind { TrackKind::Audio => { - FilePickerAdapter::save_audio(&track.codec) + FilePickerAdapter::save_audio(base_stem, &track.codec) } TrackKind::Subtitle => { - FilePickerAdapter::save_subtitle(&track.codec) + FilePickerAdapter::save_subtitle(base_stem, &track.codec) } _ => None, }; diff --git a/src/ui/components/output_selector.rs b/src/ui/components/output_selector.rs index 3d45789..f075b6e 100644 --- a/src/ui/components/output_selector.rs +++ b/src/ui/components/output_selector.rs @@ -13,7 +13,10 @@ impl OutputSelector { } /// Renderiza o seletor. Retorna `Some(FilePath)` se um novo caminho foi selecionado. - pub fn ui(&mut self, ui: &mut egui::Ui) -> Option { + /// + /// `source_stem` é o nome do arquivo de vídeo base sem extensão (ex: `"filme"`). + /// Quando fornecido, o diálogo pré-preenche o nome como `{source_stem}_output.mkv`. + pub fn ui(&mut self, ui: &mut egui::Ui, source_stem: Option<&str>) -> Option { let mut selected = None; ui.group(|ui| { @@ -28,7 +31,8 @@ impl OutputSelector { ui.label(&label); if ui.button("Salvar como…").clicked() { - if let Some(path) = FilePickerAdapter::pick_output() { + let stem = source_stem.unwrap_or("output"); + if let Some(path) = FilePickerAdapter::pick_output(stem) { self.selected_path = Some(path.clone()); selected = Some(path); }