| 1 |
use egui; |
| 2 |
use super::super::{theme, widgets}; |
| 3 |
|
| 4 |
use crate::import::ImportStrategy; |
| 5 |
use crate::state::{BrowserState, ImportMode}; |
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
pub fn draw_configure_import(ui: &mut egui::Ui, state: &mut BrowserState) { |
| 10 |
let (source_display, file_count) = match &state.import_mode { |
| 11 |
ImportMode::ConfigureImport { source, audio_file_count, .. } => { |
| 12 |
(source.display().to_string(), *audio_file_count) |
| 13 |
} |
| 14 |
_ => return, |
| 15 |
}; |
| 16 |
|
| 17 |
egui::CentralPanel::default().show_inside(ui, |ui| { |
| 18 |
|
| 19 |
|
| 20 |
egui::ScrollArea::vertical().show(ui, |ui| { |
| 21 |
widgets::wizard_steps(ui, super::WIZARD_STEPS, 0); |
| 22 |
ui.heading("Import Folder"); |
| 23 |
ui.add_space(theme::space::MD); |
| 24 |
|
| 25 |
|
| 26 |
ui.horizontal(|ui| { |
| 27 |
ui.label(format!("Source: {source_display}")); |
| 28 |
if ui |
| 29 |
.small_button("Change...") |
| 30 |
.on_hover_text("Pick a different source folder") |
| 31 |
.clicked() |
| 32 |
&& let Some(new_source) = rfd::FileDialog::new() |
| 33 |
.set_title("Choose source folder") |
| 34 |
.pick_folder() |
| 35 |
{ |
| 36 |
state.change_import_source(new_source); |
| 37 |
} |
| 38 |
}); |
| 39 |
ui.add_space(theme::space::SM); |
| 40 |
|
| 41 |
|
| 42 |
ui.label( |
| 43 |
egui::RichText::new(format!( |
| 44 |
"{file_count} audio file{} found", |
| 45 |
if file_count == 1 { "" } else { "s" }, |
| 46 |
)) |
| 47 |
.strong(), |
| 48 |
); |
| 49 |
ui.label( |
| 50 |
egui::RichText::new(format!( |
| 51 |
"Supported: {}. Duplicates will be skipped automatically.", |
| 52 |
audiofiles_core::util::AUDIO_EXTENSIONS.join(", "), |
| 53 |
)) |
| 54 |
.small() |
| 55 |
.weak(), |
| 56 |
); |
| 57 |
ui.add_space(theme::space::LG); |
| 58 |
|
| 59 |
ui.label("Import strategy:"); |
| 60 |
ui.add_space(theme::space::SM); |
| 61 |
|
| 62 |
let is_flat = matches!( |
| 63 |
&state.import_mode, |
| 64 |
ImportMode::ConfigureImport { strategy: ImportStrategy::Flat { .. }, .. } |
| 65 |
); |
| 66 |
let is_new_vfs = matches!( |
| 67 |
&state.import_mode, |
| 68 |
ImportMode::ConfigureImport { strategy: ImportStrategy::NewVfs { .. }, .. } |
| 69 |
); |
| 70 |
let is_merge = matches!( |
| 71 |
&state.import_mode, |
| 72 |
ImportMode::ConfigureImport { strategy: ImportStrategy::MergeIntoVfs { .. }, .. } |
| 73 |
); |
| 74 |
|
| 75 |
let current_vfs_id = state.current_vfs_id(); |
| 76 |
let current_dir = state.current_dir; |
| 77 |
if ui.radio(is_flat, "Flat (all files in current directory)").clicked() && !is_flat |
| 78 |
&& let (ImportMode::ConfigureImport { strategy, .. }, Some(vfs_id)) = |
| 79 |
(&mut state.import_mode, current_vfs_id) |
| 80 |
{ |
| 81 |
*strategy = ImportStrategy::Flat { |
| 82 |
vfs_id, |
| 83 |
parent_id: current_dir, |
| 84 |
}; |
| 85 |
} |
| 86 |
|
| 87 |
if ui.radio(is_new_vfs, "New vault (preserve directory structure)").clicked() && !is_new_vfs |
| 88 |
&& let ImportMode::ConfigureImport { |
| 89 |
ref mut strategy, |
| 90 |
ref new_vfs_name, |
| 91 |
.. |
| 92 |
} = state.import_mode |
| 93 |
{ |
| 94 |
*strategy = ImportStrategy::NewVfs { |
| 95 |
vfs_name: new_vfs_name.clone(), |
| 96 |
}; |
| 97 |
} |
| 98 |
|
| 99 |
if is_new_vfs { |
| 100 |
ui.indent("new_vfs_indent", |ui| { |
| 101 |
ui.horizontal(|ui| { |
| 102 |
ui.label("Vault name:"); |
| 103 |
if let ImportMode::ConfigureImport { |
| 104 |
ref mut new_vfs_name, |
| 105 |
ref mut strategy, |
| 106 |
.. |
| 107 |
} = state.import_mode |
| 108 |
&& ui.text_edit_singleline(new_vfs_name).changed() { |
| 109 |
*strategy = ImportStrategy::NewVfs { |
| 110 |
vfs_name: new_vfs_name.clone(), |
| 111 |
}; |
| 112 |
} |
| 113 |
}); |
| 114 |
}); |
| 115 |
} |
| 116 |
|
| 117 |
if ui.radio(is_merge, "Merge into existing vault").clicked() && !is_merge |
| 118 |
&& let ImportMode::ConfigureImport { |
| 119 |
ref mut strategy, |
| 120 |
ref available_vfs, |
| 121 |
selected_merge_vfs_idx, |
| 122 |
.. |
| 123 |
} = state.import_mode |
| 124 |
{ |
| 125 |
let vfs = &available_vfs[selected_merge_vfs_idx]; |
| 126 |
*strategy = ImportStrategy::MergeIntoVfs { |
| 127 |
vfs_id: vfs.id, |
| 128 |
parent_id: None, |
| 129 |
}; |
| 130 |
} |
| 131 |
|
| 132 |
if is_merge { |
| 133 |
ui.indent("merge_vfs_indent", |ui| { |
| 134 |
if let ImportMode::ConfigureImport { |
| 135 |
ref mut strategy, |
| 136 |
ref available_vfs, |
| 137 |
ref mut selected_merge_vfs_idx, |
| 138 |
.. |
| 139 |
} = state.import_mode |
| 140 |
{ |
| 141 |
let current_name = available_vfs |
| 142 |
.get(*selected_merge_vfs_idx) |
| 143 |
.map(|v| v.name.as_str()) |
| 144 |
.unwrap_or("(none)"); |
| 145 |
|
| 146 |
egui::ComboBox::from_id_salt("merge_vfs_select") |
| 147 |
.selected_text(current_name) |
| 148 |
.show_ui(ui, |ui| { |
| 149 |
for (i, vfs) in available_vfs.iter().enumerate() { |
| 150 |
if ui |
| 151 |
.selectable_label(i == *selected_merge_vfs_idx, &vfs.name) |
| 152 |
.clicked() |
| 153 |
{ |
| 154 |
*selected_merge_vfs_idx = i; |
| 155 |
*strategy = ImportStrategy::MergeIntoVfs { |
| 156 |
vfs_id: vfs.id, |
| 157 |
parent_id: None, |
| 158 |
}; |
| 159 |
} |
| 160 |
} |
| 161 |
}); |
| 162 |
} |
| 163 |
}); |
| 164 |
} |
| 165 |
|
| 166 |
ui.add_space(theme::space::SECTION); |
| 167 |
|
| 168 |
|
| 169 |
|
| 170 |
|
| 171 |
|
| 172 |
|
| 173 |
ui.label( |
| 174 |
egui::RichText::new( |
| 175 |
"Once started, you can cancel mid-import but copies already made will stay in the library." |
| 176 |
) |
| 177 |
.small() |
| 178 |
.color(theme::text_muted()), |
| 179 |
); |
| 180 |
ui.add_space(theme::space::SM); |
| 181 |
|
| 182 |
|
| 183 |
|
| 184 |
|
| 185 |
|
| 186 |
let (can_import, disabled_reason) = match &state.import_mode { |
| 187 |
ImportMode::ConfigureImport { strategy, new_vfs_name, available_vfs, .. } => { |
| 188 |
match strategy { |
| 189 |
ImportStrategy::Flat { .. } => (true, ""), |
| 190 |
ImportStrategy::NewVfs { .. } => { |
| 191 |
if new_vfs_name.trim().is_empty() { |
| 192 |
(false, "Enter a name for the new vault.") |
| 193 |
} else { |
| 194 |
(true, "") |
| 195 |
} |
| 196 |
} |
| 197 |
ImportStrategy::MergeIntoVfs { .. } => { |
| 198 |
if available_vfs.is_empty() { |
| 199 |
(false, "No existing vaults to merge into.") |
| 200 |
} else { |
| 201 |
(true, "") |
| 202 |
} |
| 203 |
} |
| 204 |
} |
| 205 |
} |
| 206 |
_ => (false, ""), |
| 207 |
}; |
| 208 |
|
| 209 |
ui.horizontal(|ui| { |
| 210 |
if ui.button("Cancel").clicked() { |
| 211 |
state.import_mode = ImportMode::None; |
| 212 |
} |
| 213 |
let import_btn = ui.add_enabled(can_import, egui::Button::new("Import")); |
| 214 |
let import_btn = if !can_import && !disabled_reason.is_empty() { |
| 215 |
import_btn.on_disabled_hover_text(disabled_reason) |
| 216 |
} else { |
| 217 |
import_btn |
| 218 |
}; |
| 219 |
if import_btn.clicked() |
| 220 |
&& let ImportMode::ConfigureImport { |
| 221 |
ref source, |
| 222 |
strategy: ref strat, |
| 223 |
ref new_vfs_name, |
| 224 |
ref available_vfs, |
| 225 |
selected_merge_vfs_idx, |
| 226 |
.. |
| 227 |
} = state.import_mode |
| 228 |
{ |
| 229 |
let source = source.clone(); |
| 230 |
let strategy = match strat { |
| 231 |
ImportStrategy::Flat { vfs_id, parent_id } => ImportStrategy::Flat { |
| 232 |
vfs_id: *vfs_id, |
| 233 |
parent_id: *parent_id, |
| 234 |
}, |
| 235 |
ImportStrategy::NewVfs { .. } => ImportStrategy::NewVfs { |
| 236 |
vfs_name: new_vfs_name.clone(), |
| 237 |
}, |
| 238 |
ImportStrategy::MergeIntoVfs { .. } => { |
| 239 |
let vfs = &available_vfs[selected_merge_vfs_idx]; |
| 240 |
ImportStrategy::MergeIntoVfs { |
| 241 |
vfs_id: vfs.id, |
| 242 |
parent_id: None, |
| 243 |
} |
| 244 |
} |
| 245 |
}; |
| 246 |
state.start_folder_import(source, strategy); |
| 247 |
} |
| 248 |
}); |
| 249 |
}); |
| 250 |
}); |
| 251 |
} |
| 252 |
|
| 253 |
|
| 254 |
pub fn draw_configure_analysis(ui: &mut egui::Ui, state: &mut BrowserState) { |
| 255 |
let (sample_count, mut config) = match &state.import_mode { |
| 256 |
ImportMode::ConfigureAnalysis { |
| 257 |
sample_hashes, |
| 258 |
config, |
| 259 |
} => (sample_hashes.len(), config.clone()), |
| 260 |
_ => return, |
| 261 |
}; |
| 262 |
|
| 263 |
egui::CentralPanel::default().show_inside(ui, |ui| { |
| 264 |
widgets::wizard_steps(ui, super::WIZARD_STEPS, 2); |
| 265 |
ui.heading("Configure Analysis"); |
| 266 |
ui.add_space(theme::space::MD); |
| 267 |
ui.label(format!("{sample_count} samples to analyze")); |
| 268 |
ui.add_space(theme::space::LG); |
| 269 |
|
| 270 |
ui.checkbox(&mut config.loudness, "Loudness (Peak, RMS, LUFS)"); |
| 271 |
ui.checkbox(&mut config.bpm, "BPM Detection"); |
| 272 |
ui.checkbox(&mut config.key, "Key Detection"); |
| 273 |
ui.checkbox(&mut config.spectral, "Spectral Features"); |
| 274 |
ui.checkbox(&mut config.classify, "Auto-classify (requires Spectral)"); |
| 275 |
ui.checkbox(&mut config.loop_detect, "Loop Detection"); |
| 276 |
ui.checkbox(&mut config.auto_suggest_tags, "Auto-suggest Tags"); |
| 277 |
ui.checkbox(&mut config.fingerprint, "Fingerprint (duplicate detection)"); |
| 278 |
|
| 279 |
if config.classify { |
| 280 |
ui.indent("smart_skip_indent", |ui| { |
| 281 |
ui.checkbox( |
| 282 |
&mut config.smart_skip, |
| 283 |
"Smart skip (skip BPM/key for drums, noise, etc.)", |
| 284 |
); |
| 285 |
}); |
| 286 |
} |
| 287 |
|
| 288 |
|
| 289 |
|
| 290 |
if config.classify && !config.spectral { |
| 291 |
config.spectral = true; |
| 292 |
} |
| 293 |
|
| 294 |
|
| 295 |
if !config.classify { |
| 296 |
config.smart_skip = false; |
| 297 |
} |
| 298 |
|
| 299 |
ui.add_space(theme::space::SECTION); |
| 300 |
|
| 301 |
ui.horizontal(|ui| { |
| 302 |
|
| 303 |
|
| 304 |
|
| 305 |
|
| 306 |
|
| 307 |
let can_back = state.last_folder_tags.is_some(); |
| 308 |
if ui |
| 309 |
.add_enabled(can_back, egui::Button::new("Back")) |
| 310 |
.on_hover_text("Return to the folder tagging step") |
| 311 |
.clicked() |
| 312 |
{ |
| 313 |
state.back_to_tag_folders(); |
| 314 |
return; |
| 315 |
} |
| 316 |
if ui.button("Run Analysis").clicked() { |
| 317 |
let hashes = match &state.import_mode { |
| 318 |
ImportMode::ConfigureAnalysis { sample_hashes, .. } => { |
| 319 |
sample_hashes.clone() |
| 320 |
} |
| 321 |
_ => Vec::new(), |
| 322 |
}; |
| 323 |
state.run_analysis(hashes, config.clone()); |
| 324 |
return; |
| 325 |
} |
| 326 |
|
| 327 |
if ui.button("Skip analysis").clicked() { |
| 328 |
state.import_mode = ImportMode::None; |
| 329 |
state.status = "Imported. Run analysis from the sidebar when ready.".to_string(); |
| 330 |
} |
| 331 |
}); |
| 332 |
}); |
| 333 |
|
| 334 |
if let ImportMode::ConfigureAnalysis { |
| 335 |
config: ref mut cfg, |
| 336 |
.. |
| 337 |
} = state.import_mode |
| 338 |
{ |
| 339 |
*cfg = config; |
| 340 |
} |
| 341 |
} |
| 342 |
|