Split export dialog into simple/advanced

This commit is contained in:
Skyler Lehmkuhl 2026-02-16 08:15:13 -05:00
parent e03d12009f
commit 93a29192fd
2 changed files with 175 additions and 121 deletions

View File

@ -41,35 +41,61 @@ pub struct ExportDialog {
/// Output file path /// Output file path
pub output_path: Option<PathBuf>, pub output_path: Option<PathBuf>,
/// Selected audio preset index (for UI)
pub selected_audio_preset: usize,
/// Error message (if any) /// Error message (if any)
pub error_message: Option<String>, pub error_message: Option<String>,
/// Whether advanced settings are shown
pub show_advanced: bool,
/// Selected video preset index
pub selected_video_preset: usize,
/// Output filename (editable text, without directory)
pub output_filename: String,
/// Output directory
pub output_dir: PathBuf,
} }
impl Default for ExportDialog { impl Default for ExportDialog {
fn default() -> Self { fn default() -> Self {
let home = std::env::var("HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("."));
let music_dir = {
let m = home.join("Music");
if m.is_dir() { m } else { home }
};
Self { Self {
open: false, open: false,
export_type: ExportType::Audio, export_type: ExportType::Audio,
audio_settings: AudioExportSettings::default(), audio_settings: AudioExportSettings::standard_mp3(),
video_settings: VideoExportSettings::default(), video_settings: VideoExportSettings::default(),
include_audio: true, include_audio: true,
output_path: None, output_path: None,
selected_audio_preset: 0,
error_message: None, error_message: None,
show_advanced: false,
selected_video_preset: 0,
output_filename: String::new(),
output_dir: music_dir,
} }
} }
} }
impl ExportDialog { impl ExportDialog {
/// Open the dialog with default settings /// Open the dialog with default settings
pub fn open(&mut self, timeline_duration: f64) { pub fn open(&mut self, timeline_duration: f64, project_name: &str) {
self.open = true; self.open = true;
self.audio_settings.end_time = timeline_duration; self.audio_settings.end_time = timeline_duration;
self.video_settings.end_time = timeline_duration; self.video_settings.end_time = timeline_duration;
self.error_message = None; self.error_message = None;
// Pre-populate filename from project name if not already set
if self.output_filename.is_empty() || !self.output_filename.contains(project_name) {
let ext = self.audio_settings.format.extension();
self.output_filename = format!("{}.{}", project_name, ext);
}
} }
/// Close the dialog /// Close the dialog
@ -78,6 +104,27 @@ impl ExportDialog {
self.error_message = None; self.error_message = None;
} }
/// Update the filename extension to match the current format
fn update_filename_extension(&mut self) {
let ext = match self.export_type {
ExportType::Audio => self.audio_settings.format.extension(),
ExportType::Video => self.video_settings.codec.container_format(),
};
// Replace extension in filename
if let Some(dot_pos) = self.output_filename.rfind('.') {
self.output_filename.truncate(dot_pos + 1);
self.output_filename.push_str(ext);
} else if !self.output_filename.is_empty() {
self.output_filename.push('.');
self.output_filename.push_str(ext);
}
}
/// Build the full output path from directory + filename
fn build_output_path(&self) -> PathBuf {
self.output_dir.join(&self.output_filename)
}
/// Render the export dialog /// Render the export dialog
/// ///
/// Returns Some(ExportResult) if the user clicked Export, None otherwise. /// Returns Some(ExportResult) if the user clicked Export, None otherwise.
@ -109,30 +156,42 @@ impl ExportDialog {
// Export type selection (tabs) // Export type selection (tabs)
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.selectable_value(&mut self.export_type, ExportType::Audio, "🎵 Audio"); if ui.selectable_value(&mut self.export_type, ExportType::Audio, "Audio").clicked() {
ui.selectable_value(&mut self.export_type, ExportType::Video, "🎬 Video"); self.update_filename_extension();
}
if ui.selectable_value(&mut self.export_type, ExportType::Video, "Video").clicked() {
self.update_filename_extension();
}
}); });
ui.add_space(12.0); ui.add_space(12.0);
ui.separator(); ui.separator();
ui.add_space(12.0); ui.add_space(12.0);
// Render either audio or video settings // Basic settings
match self.export_type { match self.export_type {
ExportType::Audio => self.render_audio_settings(ui), ExportType::Audio => self.render_audio_basic(ui),
ExportType::Video => self.render_video_settings(ui), ExportType::Video => self.render_video_basic(ui),
} }
ui.add_space(12.0); ui.add_space(12.0);
// Time range (common to both) // Output file
self.render_time_range(ui);
ui.add_space(12.0);
// Output file path (common to both)
self.render_output_selection(ui); self.render_output_selection(ui);
ui.add_space(4.0);
// Advanced toggle
ui.toggle_value(&mut self.show_advanced, "Advanced settings");
if self.show_advanced {
ui.add_space(8.0);
match self.export_type {
ExportType::Audio => self.render_audio_advanced(ui),
ExportType::Video => self.render_video_advanced(ui),
}
}
ui.add_space(16.0); ui.add_space(16.0);
// Buttons // Buttons
@ -160,66 +219,49 @@ impl ExportDialog {
} }
if should_export { if should_export {
self.output_path = Some(self.build_output_path());
return self.handle_export(); return self.handle_export();
} }
None None
} }
/// Render audio export settings UI /// Render basic audio settings (format + filename)
fn render_audio_settings(&mut self, ui: &mut egui::Ui) { fn render_audio_basic(&mut self, ui: &mut egui::Ui) {
// Preset selection
ui.heading("Preset");
ui.horizontal(|ui| {
let presets = [
("High Quality WAV", AudioExportSettings::high_quality_wav()),
("High Quality FLAC", AudioExportSettings::high_quality_flac()),
("Standard MP3", AudioExportSettings::standard_mp3()),
("Standard AAC", AudioExportSettings::standard_aac()),
("High Quality MP3", AudioExportSettings::high_quality_mp3()),
("High Quality AAC", AudioExportSettings::high_quality_aac()),
("Podcast MP3", AudioExportSettings::podcast_mp3()),
("Podcast AAC", AudioExportSettings::podcast_aac()),
];
egui::ComboBox::from_id_salt("export_preset")
.selected_text(presets[self.selected_audio_preset].0)
.show_ui(ui, |ui| {
for (i, (name, _)) in presets.iter().enumerate() {
if ui.selectable_value(&mut self.selected_audio_preset, i, *name).clicked() {
// Save current time range before applying preset
let saved_start = self.audio_settings.start_time;
let saved_end = self.audio_settings.end_time;
self.audio_settings = presets[i].1.clone();
// Restore time range
self.audio_settings.start_time = saved_start;
self.audio_settings.end_time = saved_end;
}
}
});
});
ui.add_space(12.0);
ui.add_space(12.0);
// Format settings
ui.heading("Format");
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Format:"); ui.label("Format:");
let prev_format = self.audio_settings.format;
egui::ComboBox::from_id_salt("audio_format") egui::ComboBox::from_id_salt("audio_format")
.selected_text(self.audio_settings.format.name()) .selected_text(self.audio_settings.format.name())
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
ui.selectable_value(&mut self.audio_settings.format, AudioFormat::Wav, "WAV (Uncompressed)");
ui.selectable_value(&mut self.audio_settings.format, AudioFormat::Flac, "FLAC (Lossless)");
ui.selectable_value(&mut self.audio_settings.format, AudioFormat::Mp3, "MP3"); ui.selectable_value(&mut self.audio_settings.format, AudioFormat::Mp3, "MP3");
ui.selectable_value(&mut self.audio_settings.format, AudioFormat::Aac, "AAC"); ui.selectable_value(&mut self.audio_settings.format, AudioFormat::Aac, "AAC");
ui.selectable_value(&mut self.audio_settings.format, AudioFormat::Flac, "FLAC (Lossless)");
ui.selectable_value(&mut self.audio_settings.format, AudioFormat::Wav, "WAV (Uncompressed)");
}); });
if self.audio_settings.format != prev_format {
self.update_filename_extension();
// Apply sensible defaults when switching formats
match self.audio_settings.format {
AudioFormat::Mp3 => {
self.audio_settings.sample_rate = 44100;
self.audio_settings.bitrate_kbps = 192;
}
AudioFormat::Aac => {
self.audio_settings.sample_rate = 44100;
self.audio_settings.bitrate_kbps = 256;
}
AudioFormat::Flac | AudioFormat::Wav => {
self.audio_settings.sample_rate = 48000;
self.audio_settings.bit_depth = 24;
}
}
}
}); });
}
ui.add_space(8.0); /// Render advanced audio settings (sample rate, channels, bit depth, bitrate, time range)
fn render_audio_advanced(&mut self, ui: &mut egui::Ui) {
// Audio settings
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Sample Rate:"); ui.label("Sample Rate:");
egui::ComboBox::from_id_salt("sample_rate") egui::ComboBox::from_id_salt("sample_rate")
@ -237,8 +279,6 @@ impl ExportDialog {
ui.radio_value(&mut self.audio_settings.channels, 2, "Stereo"); ui.radio_value(&mut self.audio_settings.channels, 2, "Stereo");
}); });
ui.add_space(8.0);
// Format-specific settings // Format-specific settings
if self.audio_settings.format.supports_bit_depth() { if self.audio_settings.format.supports_bit_depth() {
ui.horizontal(|ui| { ui.horizontal(|ui| {
@ -261,12 +301,48 @@ impl ExportDialog {
}); });
}); });
} }
ui.add_space(8.0);
// Time range
self.render_time_range(ui);
} }
/// Render video export settings UI /// Video presets: (name, codec, quality, width, height, fps)
fn render_video_settings(&mut self, ui: &mut egui::Ui) { const VIDEO_PRESETS: &'static [(&'static str, VideoCodec, VideoQuality, u32, u32, f64)] = &[
// Codec selection ("1080p H.264 (Standard)", VideoCodec::H264, VideoQuality::High, 1920, 1080, 30.0),
ui.heading("Codec"); ("1080p H.264 60fps", VideoCodec::H264, VideoQuality::High, 1920, 1080, 60.0),
("4K H.264", VideoCodec::H264, VideoQuality::VeryHigh, 3840, 2160, 30.0),
("720p H.264 (Small)", VideoCodec::H264, VideoQuality::Medium, 1280, 720, 30.0),
("1080p H.265 (Smaller)", VideoCodec::H265, VideoQuality::High, 1920, 1080, 30.0),
("1080p VP9 (WebM)", VideoCodec::VP9, VideoQuality::High, 1920, 1080, 30.0),
("1080p ProRes 422", VideoCodec::ProRes422, VideoQuality::VeryHigh, 1920, 1080, 30.0),
];
/// Render basic video settings (preset dropdown)
fn render_video_basic(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.label("Preset:");
egui::ComboBox::from_id_salt("video_preset")
.selected_text(Self::VIDEO_PRESETS[self.selected_video_preset].0)
.show_ui(ui, |ui| {
for (i, preset) in Self::VIDEO_PRESETS.iter().enumerate() {
if ui.selectable_value(&mut self.selected_video_preset, i, preset.0).clicked() {
let (_, codec, quality, w, h, fps) = *preset;
self.video_settings.codec = codec;
self.video_settings.quality = quality;
self.video_settings.width = Some(w);
self.video_settings.height = Some(h);
self.video_settings.framerate = fps;
self.update_filename_extension();
}
}
});
});
}
/// Render advanced video settings (codec, resolution, framerate, quality, time range)
fn render_video_advanced(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Codec:"); ui.label("Codec:");
egui::ComboBox::from_id_salt("video_codec") egui::ComboBox::from_id_salt("video_codec")
@ -280,44 +356,34 @@ impl ExportDialog {
}); });
}); });
ui.add_space(12.0);
// Resolution
ui.heading("Resolution");
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Width:"); ui.label("Resolution:");
let mut custom_width = self.video_settings.width.unwrap_or(1920); let mut custom_width = self.video_settings.width.unwrap_or(1920);
if ui.add(egui::DragValue::new(&mut custom_width).range(1..=7680)).changed() { if ui.add(egui::DragValue::new(&mut custom_width).range(1..=7680)).changed() {
self.video_settings.width = Some(custom_width); self.video_settings.width = Some(custom_width);
} }
ui.label("x");
ui.label("Height:");
let mut custom_height = self.video_settings.height.unwrap_or(1080); let mut custom_height = self.video_settings.height.unwrap_or(1080);
if ui.add(egui::DragValue::new(&mut custom_height).range(1..=4320)).changed() { if ui.add(egui::DragValue::new(&mut custom_height).range(1..=4320)).changed() {
self.video_settings.height = Some(custom_height); self.video_settings.height = Some(custom_height);
} }
}); });
// Resolution presets
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("1080p").clicked() { if ui.small_button("1080p").clicked() {
self.video_settings.width = Some(1920); self.video_settings.width = Some(1920);
self.video_settings.height = Some(1080); self.video_settings.height = Some(1080);
} }
if ui.button("4K").clicked() { if ui.small_button("4K").clicked() {
self.video_settings.width = Some(3840); self.video_settings.width = Some(3840);
self.video_settings.height = Some(2160); self.video_settings.height = Some(2160);
} }
if ui.button("720p").clicked() { if ui.small_button("720p").clicked() {
self.video_settings.width = Some(1280); self.video_settings.width = Some(1280);
self.video_settings.height = Some(720); self.video_settings.height = Some(720);
} }
}); });
ui.add_space(12.0);
// Framerate
ui.heading("Framerate");
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("FPS:"); ui.label("FPS:");
egui::ComboBox::from_id_salt("framerate") egui::ComboBox::from_id_salt("framerate")
@ -329,10 +395,6 @@ impl ExportDialog {
}); });
}); });
ui.add_space(12.0);
// Quality
ui.heading("Quality");
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Quality:"); ui.label("Quality:");
egui::ComboBox::from_id_salt("video_quality") egui::ComboBox::from_id_salt("video_quality")
@ -345,10 +407,12 @@ impl ExportDialog {
}); });
}); });
ui.add_space(12.0);
// Include audio checkbox
ui.checkbox(&mut self.include_audio, "Include Audio"); ui.checkbox(&mut self.include_audio, "Include Audio");
ui.add_space(8.0);
// Time range
self.render_time_range(ui);
} }
/// Render time range UI (common to both audio and video) /// Render time range UI (common to both audio and video)
@ -358,7 +422,6 @@ impl ExportDialog {
ExportType::Video => (&mut self.video_settings.start_time, &mut self.video_settings.end_time), ExportType::Video => (&mut self.video_settings.start_time, &mut self.video_settings.end_time),
}; };
ui.heading("Time Range");
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Start:"); ui.label("Start:");
ui.add(egui::DragValue::new(start_time) ui.add(egui::DragValue::new(start_time)
@ -377,46 +440,32 @@ impl ExportDialog {
ui.label(format!("Duration: {:.2} seconds", duration)); ui.label(format!("Duration: {:.2} seconds", duration));
} }
/// Render output file selection UI (common to both audio and video) /// Render output file selection UI
fn render_output_selection(&mut self, ui: &mut egui::Ui) { fn render_output_selection(&mut self, ui: &mut egui::Ui) {
ui.heading("Output");
ui.horizontal(|ui| { ui.horizontal(|ui| {
let path_text = self.output_path.as_ref() ui.label("Save to:");
.map(|p| p.display().to_string()) let dir_text = self.output_dir.display().to_string();
.unwrap_or_else(|| "No file selected".to_string()); ui.label(&dir_text);
if ui.button("Change...").clicked() {
ui.label("File:"); if let Some(dir) = rfd::FileDialog::new()
ui.text_edit_singleline(&mut path_text.clone()); .set_directory(&self.output_dir)
.pick_folder()
if ui.button("Browse...").clicked() {
// Determine file extension and filter based on export type
let (default_name, filter_name, extensions) = match self.export_type {
ExportType::Audio => {
let ext = self.audio_settings.format.extension();
(format!("audio.{}", ext), "Audio", vec![ext])
}
ExportType::Video => {
let ext = self.video_settings.codec.container_format();
(format!("video.{}", ext), "Video", vec![ext])
}
};
if let Some(path) = rfd::FileDialog::new()
.set_file_name(&default_name)
.add_filter(filter_name, &extensions)
.save_file()
{ {
self.output_path = Some(path); self.output_dir = dir;
} }
} }
}); });
ui.horizontal(|ui| {
ui.label("Filename:");
ui.text_edit_singleline(&mut self.output_filename);
});
} }
/// Handle export button click /// Handle export button click
fn handle_export(&mut self) -> Option<ExportResult> { fn handle_export(&mut self) -> Option<ExportResult> {
// Check if output path is set if self.output_filename.trim().is_empty() {
if self.output_path.is_none() { self.error_message = Some("Please enter a filename".to_string());
self.error_message = Some("Please select an output file".to_string());
return None; return None;
} }

View File

@ -2157,7 +2157,12 @@ impl EditorApp {
println!("Menu: Export"); println!("Menu: Export");
// Open export dialog with calculated timeline endpoint // Open export dialog with calculated timeline endpoint
let timeline_endpoint = self.action_executor.document().calculate_timeline_endpoint(); let timeline_endpoint = self.action_executor.document().calculate_timeline_endpoint();
self.export_dialog.open(timeline_endpoint); // Derive project name from the .beam file path, falling back to document name
let project_name = self.current_file_path.as_ref()
.and_then(|p| p.file_stem())
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| self.action_executor.document().name.clone());
self.export_dialog.open(timeline_endpoint, &project_name);
} }
MenuAction::Quit => { MenuAction::Quit => {
println!("Menu: Quit"); println!("Menu: Quit");