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;
|
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
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user