feat: adiciona pré-preenchimento de nomes de arquivos em diálogos de salvamento com base no arquivo de vídeo selecionado

This commit is contained in:
2026-02-28 16:07:49 -03:00
parent 74840e70f7
commit edd2786e06
3 changed files with 96 additions and 39 deletions
+68 -29
View File
@@ -5,6 +5,23 @@ use std::path::PathBuf;
pub struct FilePickerAdapter; pub struct FilePickerAdapter;
impl 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. /// Abre diálogo de seleção de arquivo de vídeo.
pub fn pick_video() -> Option<FilePath> { pub fn pick_video() -> Option<FilePath> {
rfd::FileDialog::new() rfd::FileDialog::new()
@@ -32,14 +49,19 @@ impl FilePickerAdapter {
.map(|p: PathBuf| FilePath::from(p)) .map(|p: PathBuf| FilePath::from(p))
} }
// ─── Diálogos de salvamento ──────────────────────────────────────────────
/// Abre diálogo de salvar arquivo MKV de saída. /// Abre diálogo de salvar arquivo MKV de saída.
pub fn pick_output() -> Option<FilePath> { ///
/// `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<FilePath> {
rfd::FileDialog::new() rfd::FileDialog::new()
.add_filter("MKV", &["mkv"]) .add_filter("MKV", &["mkv"])
.set_file_name("output.mkv") .set_file_name(Self::suggested_name(base_stem, "output", "mkv"))
.save_file() .save_file()
.map(|mut p: PathBuf| { .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"); p.set_extension("mkv");
FilePath::from(p) FilePath::from(p)
}) })
@@ -47,14 +69,44 @@ impl FilePickerAdapter {
/// Abre diálogo de salvar faixa de áudio exportada. /// Abre diálogo de salvar faixa de áudio 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.
/// (ex: `aac` → `.aac`, `vorbis` → `.ogg`). Cai em `.mka` para codecs desconhecidos. /// A extensão é inferida a partir do `codec` detectado pelo ffprobe
pub fn save_audio(codec: &str) -> Option<FilePath> { /// (ex: `aac` → `.m4a`, `vorbis` → `.ogg`). Cai em `.mka` para codecs desconhecidos.
let (ext, label) = match codec { /// O nome sugerido será `{base_stem}_audio.{ext}`.
// AAC exportado como .m4a (container MPEG-4) em vez de .aac (ADTS raw). pub fn save_audio(base_stem: &str, codec: &str) -> Option<FilePath> {
// O formato ADTS não armazena duração total — o ffprobe estima pelo bitrate let (ext, label) = Self::audio_ext_and_label(codec);
// e pode reportar até dezenas de segundos a menos. O container M4A preserva rfd::FileDialog::new()
// a duração exata dos atoms do container original. .add_filter(label, &[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.
///
/// `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.
/// O nome sugerido será `{base_stem}_legenda.{ext}`.
pub fn save_subtitle(base_stem: &str, codec: &str) -> Option<FilePath> {
let (ext, label) = Self::subtitle_ext_and_label(codec);
rfd::FileDialog::new()
.add_filter(label, &[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)"), "aac" => ("m4a", "M4A (AAC)"),
"ac3" => ("ac3", "AC3"), "ac3" => ("ac3", "AC3"),
"eac3" => ("eac3", "E-AC3"), "eac3" => ("eac3", "E-AC3"),
@@ -66,30 +118,17 @@ impl FilePickerAdapter {
"truehd" => ("thd", "TrueHD"), "truehd" => ("thd", "TrueHD"),
"pcm_s16le" | "pcm_s24le" | "pcm_s32le" => ("wav", "WAV"), "pcm_s16le" | "pcm_s24le" | "pcm_s32le" => ("wav", "WAV"),
_ => ("mka", "Matroska Audio"), _ => ("mka", "Matroska Audio"),
}; }
rfd::FileDialog::new()
.add_filter(label, &[ext])
.set_file_name(format!("track.{}", ext))
.save_file()
.map(FilePath::from)
} }
/// Abre diálogo de salvar faixa de legenda exportada. /// Retorna `(extensão, rótulo_filtro)` para um codec de legenda.
/// fn subtitle_ext_and_label(codec: &str) -> (&'static str, &'static str) {
/// A extensão padrão é inferida a partir do codec detectado pelo ffprobe match codec {
/// (ex: `subrip` → `.srt`, `ass` → `.ass`). Cai em `.srt` para codecs desconhecidos.
pub fn save_subtitle(codec: &str) -> Option<FilePath> {
let (ext, label) = match codec {
"ass" | "ssa" => ("ass", "SubStation Alpha"), "ass" | "ssa" => ("ass", "SubStation Alpha"),
"webvtt" => ("vtt", "WebVTT"), "webvtt" => ("vtt", "WebVTT"),
"dvd_subtitle" => ("sub", "DVD Subtitle"), "dvd_subtitle" => ("sub", "DVD Subtitle"),
"hdmv_pgs_subtitle" => ("sup", "PGS Subtitle"), "hdmv_pgs_subtitle" => ("sup", "PGS Subtitle"),
_ => ("srt", "SubRip"), _ => ("srt", "SubRip"),
}; }
rfd::FileDialog::new()
.add_filter(label, &[ext])
.set_file_name(format!("subtitle.{}", ext))
.save_file()
.map(FilePath::from)
} }
} }
+17 -3
View File
@@ -264,7 +264,13 @@ impl eframe::App for App {
ui.add_space(8.0); ui.add_space(8.0);
// ── Seleção de saída ────────────────────────────────── // ── 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); self.pending_output = Some(path);
if self.pending_source.is_some() { if self.pending_source.is_some() {
self.try_build_project(); self.try_build_project();
@@ -308,12 +314,20 @@ impl eframe::App for App {
if let Some(track) = if let Some(track) =
project.existing_tracks.iter().find(|t| t.id == id).cloned() 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 { let maybe_output = match track.kind {
TrackKind::Audio => { TrackKind::Audio => {
FilePickerAdapter::save_audio(&track.codec) FilePickerAdapter::save_audio(base_stem, &track.codec)
} }
TrackKind::Subtitle => { TrackKind::Subtitle => {
FilePickerAdapter::save_subtitle(&track.codec) FilePickerAdapter::save_subtitle(base_stem, &track.codec)
} }
_ => None, _ => None,
}; };
+6 -2
View File
@@ -13,7 +13,10 @@ impl OutputSelector {
} }
/// Renderiza o seletor. Retorna `Some(FilePath)` se um novo caminho foi selecionado. /// Renderiza o seletor. Retorna `Some(FilePath)` se um novo caminho foi selecionado.
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<FilePath> { ///
/// `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<FilePath> {
let mut selected = None; let mut selected = None;
ui.group(|ui| { ui.group(|ui| {
@@ -28,7 +31,8 @@ impl OutputSelector {
ui.label(&label); ui.label(&label);
if ui.button("Salvar como…").clicked() { 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()); self.selected_path = Some(path.clone());
selected = Some(path); selected = Some(path);
} }