use egui; use super::super::{theme, widgets}; use crate::import::ImportStrategy; use crate::state::{BrowserState, ImportMode}; /// Draw the import configuration screen with strategy radio buttons. pub fn draw_configure_import(ui: &mut egui::Ui, state: &mut BrowserState) { let (source_display, file_count) = match &state.import_mode { ImportMode::ConfigureImport { source, audio_file_count, .. } => { (source.display().to_string(), *audio_file_count) } _ => return, }; egui::CentralPanel::default().show_inside(ui, |ui| { // Scroll the body so the Cancel/Import row stays reachable on a short // window or when the merge combo + vault field + warnings all expand (P5). egui::ScrollArea::vertical().show(ui, |ui| { widgets::wizard_steps(ui, super::WIZARD_STEPS, 0); ui.heading("Import Folder"); ui.add_space(theme::space::MD); // Source: path + Change button (M-5). Picking the wrong folder no // longer requires Cancel-and-restart from the toolbar. ui.horizontal(|ui| { ui.label(format!("Source: {source_display}")); if ui .small_button("Change...") .on_hover_text("Pick a different source folder") .clicked() && let Some(new_source) = rfd::FileDialog::new() .set_title("Choose source folder") .pick_folder() { state.change_import_source(new_source); } }); ui.add_space(theme::space::SM); // Dry-run preview ui.label( egui::RichText::new(format!( "{file_count} audio file{} found", if file_count == 1 { "" } else { "s" }, )) .strong(), ); ui.label( egui::RichText::new(format!( "Supported: {}. Duplicates will be skipped automatically.", audiofiles_core::util::AUDIO_EXTENSIONS.join(", "), )) .small() .weak(), ); ui.add_space(theme::space::LG); ui.label("Import strategy:"); ui.add_space(theme::space::SM); let is_flat = matches!( &state.import_mode, ImportMode::ConfigureImport { strategy: ImportStrategy::Flat { .. }, .. } ); let is_new_vfs = matches!( &state.import_mode, ImportMode::ConfigureImport { strategy: ImportStrategy::NewVfs { .. }, .. } ); let is_merge = matches!( &state.import_mode, ImportMode::ConfigureImport { strategy: ImportStrategy::MergeIntoVfs { .. }, .. } ); let current_vfs_id = state.current_vfs_id(); let current_dir = state.current_dir; if ui.radio(is_flat, "Flat (all files in current directory)").clicked() && !is_flat && let (ImportMode::ConfigureImport { strategy, .. }, Some(vfs_id)) = (&mut state.import_mode, current_vfs_id) { *strategy = ImportStrategy::Flat { vfs_id, parent_id: current_dir, }; } if ui.radio(is_new_vfs, "New vault (preserve directory structure)").clicked() && !is_new_vfs && let ImportMode::ConfigureImport { ref mut strategy, ref new_vfs_name, .. } = state.import_mode { *strategy = ImportStrategy::NewVfs { vfs_name: new_vfs_name.clone(), }; } if is_new_vfs { ui.indent("new_vfs_indent", |ui| { ui.horizontal(|ui| { ui.label("Vault name:"); if let ImportMode::ConfigureImport { ref mut new_vfs_name, ref mut strategy, .. } = state.import_mode && ui.text_edit_singleline(new_vfs_name).changed() { *strategy = ImportStrategy::NewVfs { vfs_name: new_vfs_name.clone(), }; } }); }); } if ui.radio(is_merge, "Merge into existing vault").clicked() && !is_merge && let ImportMode::ConfigureImport { ref mut strategy, ref available_vfs, selected_merge_vfs_idx, .. } = state.import_mode { let vfs = &available_vfs[selected_merge_vfs_idx]; *strategy = ImportStrategy::MergeIntoVfs { vfs_id: vfs.id, parent_id: None, }; } if is_merge { ui.indent("merge_vfs_indent", |ui| { if let ImportMode::ConfigureImport { ref mut strategy, ref available_vfs, ref mut selected_merge_vfs_idx, .. } = state.import_mode { let current_name = available_vfs .get(*selected_merge_vfs_idx) .map(|v| v.name.as_str()) .unwrap_or("(none)"); egui::ComboBox::from_id_salt("merge_vfs_select") .selected_text(current_name) .show_ui(ui, |ui| { for (i, vfs) in available_vfs.iter().enumerate() { if ui .selectable_label(i == *selected_merge_vfs_idx, &vfs.name) .clicked() { *selected_merge_vfs_idx = i; *strategy = ImportStrategy::MergeIntoVfs { vfs_id: vfs.id, parent_id: None, }; } } }); } }); } ui.add_space(theme::space::SECTION); // One-way edge warning (C-1). Configure → Importing is the only // non-recoverable transition in the wizard: once files start landing // in the content store, cancelling preserves the partial work rather // than rolling it back. Make that explicit so the user reads "Import" // as a commit, not a preview. ui.label( egui::RichText::new( "Once started, you can cancel mid-import but copies already made will stay in the library." ) .small() .color(theme::text_muted()), ); ui.add_space(theme::space::SM); // m-11: gate Import on required fields per strategy variant. NewVfs // needs a non-empty vault name; MergeIntoVfs needs at least one vault // available (the indexed access on available_vfs would otherwise panic // if the user reached this screen with no vaults). let (can_import, disabled_reason) = match &state.import_mode { ImportMode::ConfigureImport { strategy, new_vfs_name, available_vfs, .. } => { match strategy { ImportStrategy::Flat { .. } => (true, ""), ImportStrategy::NewVfs { .. } => { if new_vfs_name.trim().is_empty() { (false, "Enter a name for the new vault.") } else { (true, "") } } ImportStrategy::MergeIntoVfs { .. } => { if available_vfs.is_empty() { (false, "No existing vaults to merge into.") } else { (true, "") } } } } _ => (false, ""), }; ui.horizontal(|ui| { if ui.button("Cancel").clicked() { state.import_mode = ImportMode::None; } let import_btn = ui.add_enabled(can_import, egui::Button::new("Import")); let import_btn = if !can_import && !disabled_reason.is_empty() { import_btn.on_disabled_hover_text(disabled_reason) } else { import_btn }; if import_btn.clicked() && let ImportMode::ConfigureImport { ref source, strategy: ref strat, ref new_vfs_name, ref available_vfs, selected_merge_vfs_idx, .. } = state.import_mode { let source = source.clone(); let strategy = match strat { ImportStrategy::Flat { vfs_id, parent_id } => ImportStrategy::Flat { vfs_id: *vfs_id, parent_id: *parent_id, }, ImportStrategy::NewVfs { .. } => ImportStrategy::NewVfs { vfs_name: new_vfs_name.clone(), }, ImportStrategy::MergeIntoVfs { .. } => { let vfs = &available_vfs[selected_merge_vfs_idx]; ImportStrategy::MergeIntoVfs { vfs_id: vfs.id, parent_id: None, } } }; state.start_folder_import(source, strategy); } }); }); }); } /// Draw the analysis configuration screen. pub fn draw_configure_analysis(ui: &mut egui::Ui, state: &mut BrowserState) { let (sample_count, mut config) = match &state.import_mode { ImportMode::ConfigureAnalysis { sample_hashes, config, } => (sample_hashes.len(), config.clone()), _ => return, }; egui::CentralPanel::default().show_inside(ui, |ui| { widgets::wizard_steps(ui, super::WIZARD_STEPS, 2); ui.heading("Configure Analysis"); ui.add_space(theme::space::MD); ui.label(format!("{sample_count} samples to analyze")); ui.add_space(theme::space::LG); ui.checkbox(&mut config.loudness, "Loudness (Peak, RMS, LUFS)"); ui.checkbox(&mut config.bpm, "BPM Detection"); ui.checkbox(&mut config.key, "Key Detection"); ui.checkbox(&mut config.spectral, "Spectral Features"); ui.checkbox(&mut config.classify, "Auto-classify (requires Spectral)"); ui.checkbox(&mut config.loop_detect, "Loop Detection"); ui.checkbox(&mut config.auto_suggest_tags, "Auto-suggest Tags"); ui.checkbox(&mut config.fingerprint, "Fingerprint (duplicate detection)"); if config.classify { ui.indent("smart_skip_indent", |ui| { ui.checkbox( &mut config.smart_skip, "Smart skip (skip BPM/key for drums, noise, etc.)", ); }); } // Classification depends on spectral features (centroid, flatness, ZCR, etc.), // so force spectral on when classify is checked. if config.classify && !config.spectral { config.spectral = true; } // Smart skip requires classification if !config.classify { config.smart_skip = false; } ui.add_space(theme::space::SECTION); ui.horizontal(|ui| { // Back (C-1): return to the TagFolders screen with previously // entered tag inputs restored. add_tag is INSERT OR IGNORE, so any // tags applied on the first pass don't double up if the user // commits again. Disabled when nothing's stashed (e.g. reached // here outside the folder-import flow). let can_back = state.last_folder_tags.is_some(); if ui .add_enabled(can_back, egui::Button::new("Back")) .on_hover_text("Return to the folder tagging step") .clicked() { state.back_to_tag_folders(); return; } if ui.button("Run Analysis").clicked() { let hashes = match &state.import_mode { ImportMode::ConfigureAnalysis { sample_hashes, .. } => { sample_hashes.clone() } _ => Vec::new(), }; state.run_analysis(hashes, config.clone()); return; } if ui.button("Skip analysis").clicked() { state.import_mode = ImportMode::None; state.status = "Imported. Run analysis from the sidebar when ready.".to_string(); } }); }); if let ImportMode::ConfigureAnalysis { config: ref mut cfg, .. } = state.import_mode { *cfg = config; } }