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:
@@ -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<FilePath> {
|
||||
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<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()
|
||||
.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<FilePath> {
|
||||
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<FilePath> {
|
||||
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<FilePath> {
|
||||
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<FilePath> {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-3
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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<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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user