Skip to main content

max / audiofiles

Forge audit fixes: resampler tail, slice count, clippy gate Pre-launch deep-audit fixes (Lane F): - Resampler dropped its tail: the chunked SincFixedIn loop never flushed the resampler's internal buffer, so every rate-converted conform/export lost its final frames. Now flush with empty input until the full output length is produced, then clamp to round(num_frames * ratio). The sinc resampler already time-aligns output to input, so no leading-delay trim is applied. Added an impulse regression test asserting exact length and impulse position. - Forge chop reported slice_count as slices.len() even when a slice rendered empty and was skipped, overstating the count and leaving a gap in the file numbering. Count and number only slices actually written. - Fixed a clippy erasing_op deny (0 * SIZE) that was breaking the clippy gate, and cleared the remaining clippy warnings (needless return, sort_by_key, plus scoped allows for two intentional design lints). cargo clippy --all-targets is now clean; 840 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-08 01:03 UTC
Commit: 04e06abe9006e3868a16a1a8de9ebc26a4b26ef7
Parent: a52d632
46 files changed, +293 insertions, -371 deletions
@@ -396,8 +396,8 @@ mod tests {
396 396 #[test]
397 397 fn clamp_prevents_overflow() {
398 398 // Simulate very loud mixed audio
399 - let buf = vec![1.5f32, -1.5, 0.5, -0.5];
400 - let mut data = vec![0.0f32; 4];
399 + let buf = [1.5f32, -1.5, 0.5, -0.5];
400 + let mut data = [0.0f32; 4];
401 401 for (out, &mix) in data.iter_mut().zip(buf.iter()) {
402 402 *out = mix.clamp(-1.0, 1.0);
403 403 }
@@ -292,14 +292,12 @@ pub fn trial_days_remaining(trial: &TrialState) -> i64 {
292 292 let now = chrono::Utc::now();
293 293
294 294 // Clock rollback detection: if now is before last_seen_date, expire immediately
295 - if let Some(ref last) = trial.last_seen_date {
296 - if let Ok(last_seen) = chrono::DateTime::parse_from_rfc3339(last) {
297 - if now.signed_duration_since(last_seen).num_hours() < -1 {
295 + if let Some(ref last) = trial.last_seen_date
296 + && let Ok(last_seen) = chrono::DateTime::parse_from_rfc3339(last)
297 + && now.signed_duration_since(last_seen).num_hours() < -1 {
298 298 // Allow up to 1 hour of drift (DST, NTP correction)
299 299 return 0;
300 300 }
301 - }
302 - }
303 301
304 302 let elapsed = now.signed_duration_since(first);
305 303 30 - elapsed.num_days()
@@ -164,8 +164,7 @@ fn load_api_key(data_dir: &Path) -> Option<String> {
164 164 return Some(key);
165 165 }
166 166 // Fall back to bundled synckit.toml
167 - parse_synckit_toml_key().map(String::from)
168 - }
167 + parse_synckit_toml_key()}
169 168
170 169 /// Save an API key to the data directory for future launches.
171 170 #[cfg(test)]
@@ -526,8 +525,8 @@ impl AudioFilesApp {
526 525 let ctx = ui.ctx().clone();
527 526 let ctx = &ctx;
528 527 // Poll tray menu events
529 - if let Some(ref tray) = self.tray {
530 - if let Some(action) = tray.poll() {
528 + if let Some(ref tray) = self.tray
529 + && let Some(action) = tray.poll() {
531 530 match action {
532 531 tray::TrayAction::ShowWindow => {
533 532 ctx.send_viewport_cmd(ViewportCommand::Focus);
@@ -542,11 +541,10 @@ impl AudioFilesApp {
542 541 }
543 542 }
544 543 }
545 - }
546 544
547 545 // Update tray tooltip based on playback state
548 - if let Some(ref tray) = self.tray {
549 - if let Some(ref browser) = self.browser {
546 + if let Some(ref tray) = self.tray
547 + && let Some(ref browser) = self.browser {
550 548 let playing = browser.shared.preview.lock().playing;
551 549 if playing {
552 550 tray.set_tooltip(&browser.status);
@@ -554,23 +552,21 @@ impl AudioFilesApp {
554 552 tray.set_tooltip("audiofiles");
555 553 }
556 554 }
557 - }
558 555
559 556 // Check if sync pulled remote changes → refresh browser contents
560 - if let Some(ref sync) = self.sync_manager {
561 - if sync.status().needs_refresh {
557 + if let Some(ref sync) = self.sync_manager
558 + && sync.status().needs_refresh {
562 559 if let Some(ref mut browser) = self.browser {
563 560 browser.refresh_vfs_list();
564 561 browser.refresh_contents();
565 562 }
566 563 sync.clear_needs_refresh();
567 564 }
568 - }
569 565
570 566 // ── Sync setup actions (before draw, so UI sees results this frame) ──
571 567 // ── Vault actions ──
572 - if let Some(ref mut browser) = self.browser {
573 - if let Some(action) = browser.settings.pending_action.take() {
568 + if let Some(ref mut browser) = self.browser
569 + && let Some(action) = browser.settings.pending_action.take() {
574 570 use audiofiles_browser::state::VaultAction;
575 571 match action {
576 572 VaultAction::SwitchVault(path) => {
@@ -581,12 +577,11 @@ impl AudioFilesApp {
581 577 let switch_path = path.clone();
582 578 if self.with_vault_registry(|reg| vault::create_vault(reg, &name, &path)) {
583 579 self.switch_vault(switch_path);
584 - if loose_files {
585 - if let Some(ref mut browser) = self.browser {
580 + if loose_files
581 + && let Some(ref mut browser) = self.browser {
586 582 let _ = browser.backend.set_config("loose_files", "1");
587 583 browser.settings.is_loose_files = true;
588 584 }
589 - }
590 585 return;
591 586 }
592 587 }
@@ -637,7 +632,6 @@ impl AudioFilesApp {
637 632 }
638 633 }
639 634 }
640 - }
641 635
642 636 // ── VFS Mirror: sync if dirty ──
643 637 if let Some(ref mut browser) = self.browser {
@@ -960,7 +954,7 @@ mod tests {
960 954 // Empty file → falls through to bundled synckit.toml key
961 955 if std::env::var("AF_SYNC_API_KEY").is_err() {
962 956 let key = load_api_key(dir.path());
963 - assert_eq!(key, parse_synckit_toml_key().map(String::from));
957 + assert_eq!(key, parse_synckit_toml_key());
964 958 }
965 959 }
966 960
@@ -970,7 +964,7 @@ mod tests {
970 964 std::fs::write(dir.path().join("sync_api_key"), " \n ").unwrap();
971 965 if std::env::var("AF_SYNC_API_KEY").is_err() {
972 966 let key = load_api_key(dir.path());
973 - assert_eq!(key, parse_synckit_toml_key().map(String::from));
967 + assert_eq!(key, parse_synckit_toml_key());
974 968 }
975 969 }
976 970
@@ -979,7 +973,7 @@ mod tests {
979 973 let dir = tempfile::tempdir().unwrap();
980 974 if std::env::var("AF_SYNC_API_KEY").is_err() {
981 975 let key = load_api_key(dir.path());
982 - assert_eq!(key, parse_synckit_toml_key().map(String::from));
976 + assert_eq!(key, parse_synckit_toml_key());
983 977 }
984 978 }
985 979
@@ -137,7 +137,8 @@ mod tests {
137 137
138 138 assert_eq!(rgba.len(), SIZE * SIZE * 4);
139 139 // Middle bar (index 2) is full height — pixel at (9, 0) should be coloured
140 - let mid_offset = (0 * SIZE + 9) * 4;
140 + // (row 0, col 9) -> offset (0 * SIZE + 9) * 4
141 + let mid_offset = 9 * 4;
141 142 assert_eq!(&rgba[mid_offset..mid_offset + 4], &bar_colour);
142 143 // Corner (0,0) should be transparent
143 144 assert_eq!(&rgba[0..4], &[0, 0, 0, 0]);
@@ -187,8 +187,8 @@ async fn check_once(status: &Arc<Mutex<UpdateStatus>>) {
187 187 Ok(resp) if resp.status().is_success() => {
188 188 match resp.json::<UpdateResponse>().await {
189 189 Ok(update) => {
190 - if let Ok(remote) = Version::parse(&update.version) {
191 - if remote > current && is_trusted_download_url(&update.url) {
190 + if let Ok(remote) = Version::parse(&update.version)
191 + && remote > current && is_trusted_download_url(&update.url) {
192 192 tracing::info!("Update available: v{}", update.version);
193 193 let mut s = status.lock();
194 194 s.available = true;
@@ -196,7 +196,6 @@ async fn check_once(status: &Arc<Mutex<UpdateStatus>>) {
196 196 s.notes = update.notes;
197 197 s.download_url = update.url;
198 198 }
199 - }
200 199 }
201 200 Err(e) => {
202 201 tracing::warn!("Failed to parse update response: {e}");
@@ -74,11 +74,10 @@ impl AudioFilesApp {
74 74 });
75 75 ui.add_space(theme::space::SM);
76 76 ui.horizontal(|ui| {
77 - if ui.button("Choose different location...").clicked() {
78 - if let Some(path) = rfd::FileDialog::new().pick_folder() {
77 + if ui.button("Choose different location...").clicked()
78 + && let Some(path) = rfd::FileDialog::new().pick_folder() {
79 79 self.vault_setup_path = Some(path);
80 80 }
81 - }
82 81 if is_custom && ui.button("Use default").clicked() {
83 82 self.vault_setup_path = None;
84 83 }
@@ -112,11 +112,10 @@ impl DirectBackend {
112 112 let profile = &plugin.profile;
113 113
114 114 // Format: if Original, set to profile's first supported format
115 - if config.format == ExportFormat::Original {
116 - if let Some(fmt) = profile.audio.formats.first() {
115 + if config.format == ExportFormat::Original
116 + && let Some(fmt) = profile.audio.formats.first() {
117 117 config.format = fmt.clone();
118 118 }
119 - }
120 119
121 120 // Sample rate: if not set, use profile's first rate
122 121 if config.sample_rate.is_none() {
@@ -149,7 +149,8 @@ fn remove_orphans(
149 149 WHERE vn.id IS NULL AND s.deleted_at IS NULL",
150 150 ) {
151 151 Ok(mut stmt) => {
152 - let rows = stmt
152 +
153 + stmt
153 154 .query_map([], |row| {
154 155 Ok((
155 156 row.get::<_, String>(0)?,
@@ -159,8 +160,7 @@ fn remove_orphans(
159 160 })
160 161 .ok()
161 162 .map(|iter| iter.flatten().collect::<Vec<_>>())
162 - .unwrap_or_default();
163 - rows
163 + .unwrap_or_default()
164 164 }
165 165 Err(e) => {
166 166 error!("Cleanup worker failed to query orphans: {e}");
@@ -206,11 +206,10 @@ fn remove_orphans(
206 206 ) {
207 207 Ok(_) => {
208 208 // Delete from disk
209 - if let Ok(path) = store.sample_path(hash, ext) {
210 - if path.exists() {
209 + if let Ok(path) = store.sample_path(hash, ext)
210 + && path.exists() {
211 211 let _ = std::fs::remove_file(&path);
212 212 }
213 - }
214 213 removed += 1;
215 214 }
216 215 Err(e) => {
@@ -403,23 +403,21 @@ fn handle_keyboard(ctx: &egui::Context, state: &mut BrowserState) {
403 403 if input.key_pressed(egui::Key::E) {
404 404 if state.edit.show_window {
405 405 state.close_edit_window();
406 - } else if let Some(node) = state.selected_node() {
407 - if let Some(hash) = &node.node.sample_hash {
406 + } else if let Some(node) = state.selected_node()
407 + && let Some(hash) = &node.node.sample_hash {
408 408 let hash = hash.clone();
409 409 state.open_edit_window(&hash);
410 410 }
411 - }
412 411 }
413 412 // "F" toggles the floating Sample Forge window for the selected sample
414 413 if input.key_pressed(egui::Key::F) && !shift {
415 414 if state.forge.show_window {
416 415 state.close_forge_window();
417 - } else if let Some(node) = state.selected_node() {
418 - if let Some(hash) = &node.node.sample_hash {
416 + } else if let Some(node) = state.selected_node()
417 + && let Some(hash) = &node.node.sample_hash {
419 418 let hash = hash.clone();
420 419 state.open_forge_window(&hash);
421 420 }
422 - }
423 421 }
424 422 // "L" toggles loop
425 423 if input.key_pressed(egui::Key::L) {
@@ -434,28 +432,23 @@ fn handle_keyboard(ctx: &egui::Context, state: &mut BrowserState) {
434 432 state.toggle_detail();
435 433 }
436 434 // Shift+F: find similar
437 - if shift && input.key_pressed(egui::Key::F) {
438 - if let Some(node) = state.selected_node() {
439 - if let Some(hash) = &node.node.sample_hash {
435 + if shift && input.key_pressed(egui::Key::F)
436 + && let Some(node) = state.selected_node()
437 + && let Some(hash) = &node.node.sample_hash {
440 438 let hash = hash.clone();
441 439 state.find_similar(&hash);
442 440 }
443 - }
444 - }
445 441 // Shift+D: find duplicates
446 - if shift && input.key_pressed(egui::Key::D) {
447 - if let Some(node) = state.selected_node() {
448 - if let Some(hash) = &node.node.sample_hash {
442 + if shift && input.key_pressed(egui::Key::D)
443 + && let Some(node) = state.selected_node()
444 + && let Some(hash) = &node.node.sample_hash {
449 445 let hash = hash.clone();
450 446 state.find_near_duplicates(&hash);
451 447 }
452 - }
453 - }
454 448 // Cmd+Shift+M: bulk move (Cmd+M alone conflicts with macOS minimize)
455 - if input.modifiers.command && input.modifiers.shift && input.key_pressed(egui::Key::M) {
456 - if state.selection.count() > 1 {
449 + if input.modifiers.command && input.modifiers.shift && input.key_pressed(egui::Key::M)
450 + && state.selection.count() > 1 {
457 451 state.open_bulk_move_modal();
458 452 }
459 - }
460 453 });
461 454 }
@@ -216,11 +216,10 @@ fn import_single_file(
216 216 Ok(_) => Ok(ImportFileResult::Imported(hash, ext)),
217 217 Err(CoreError::NameConflict(_)) => Ok(ImportFileResult::Duplicate),
218 218 Err(e) => {
219 - if let CoreError::Db(ref sqlite_err) = e {
220 - if sqlite_err.to_string().contains("UNIQUE") {
219 + if let CoreError::Db(ref sqlite_err) = e
220 + && sqlite_err.to_string().contains("UNIQUE") {
221 221 return Ok(ImportFileResult::Duplicate);
222 222 }
223 - }
224 223 Err(e)
225 224 }
226 225 }
@@ -18,11 +18,10 @@ impl BrowserState {
18 18 let analysis = self.backend.get_analysis(hash).ok().flatten();
19 19 let source_rate = analysis.as_ref().map(|a| a.sample_rate).unwrap_or(44100);
20 20 // Seed BPM from analysis when present so BPM-grid chop is ready to go.
21 - if let Some(bpm) = analysis.as_ref().and_then(|a| a.bpm) {
22 - if bpm > 0.0 {
21 + if let Some(bpm) = analysis.as_ref().and_then(|a| a.bpm)
22 + && bpm > 0.0 {
23 23 self.forge.bpm = bpm;
24 24 }
25 - }
26 25 let ext = self.backend.sample_extension(hash).unwrap_or_else(|_| "wav".to_string());
27 26 let name = self
28 27 .selected_node()
@@ -704,11 +704,10 @@ impl BrowserState {
704 704 pub fn retry_analysis(&mut self) {
705 705 self.cancel_analysis();
706 706 let hashes = std::mem::take(&mut self.last_analysis_hashes);
707 - if let Some(config) = self.last_analysis_config.take() {
708 - if !hashes.is_empty() {
707 + if let Some(config) = self.last_analysis_config.take()
708 + && !hashes.is_empty() {
709 709 self.run_analysis(hashes, config);
710 710 }
711 - }
712 711 }
713 712
714 713 /// Apply user-entered tags to each imported folder's samples, then start analysis.
@@ -302,11 +302,10 @@ impl BrowserState {
302 302
303 303 /// Load column config from the user_config table.
304 304 pub fn load_column_config(&mut self) {
305 - if let Ok(Some(json)) = self.backend.get_config("column_config") {
306 - if let Ok(parsed) = serde_json::from_str::<ColumnConfig>(&json) {
305 + if let Ok(Some(json)) = self.backend.get_config("column_config")
306 + && let Ok(parsed) = serde_json::from_str::<ColumnConfig>(&json) {
307 307 self.column_config = parsed;
308 308 }
309 - }
310 309 }
311 310
312 311 /// Save the current theme ID to the user_config table.
@@ -224,6 +224,7 @@ pub struct BrowserState {
224 224 /// Back button on the ConfigureAnalysis screen can rehydrate the previous
225 225 /// state (C-1). Tags themselves are `INSERT OR IGNORE` so re-applying after
226 226 /// a Back is a no-op for the backend.
227 + #[allow(clippy::type_complexity)] // a snapshot tuple for Back-button rehydration; a named alias would not earn its keep
227 228 pub last_folder_tags: Option<(Vec<crate::state::FolderTagEntry>, Vec<(String, String)>)>,
228 229 /// Rolling progress samples for the current long-running operation
229 230 /// (import / analysis / export). Drives the rate + ETA readout (M-11).
@@ -118,14 +118,13 @@ impl BrowserState {
118 118 /// Reload the tag list for the currently focused sample (shown in the detail panel).
119 119 pub fn refresh_selected_tags(&mut self) {
120 120 self.selected_tags = Arc::new(Vec::new());
121 - if let Some(node) = self.selected_node() {
122 - if let Some(hash) = &node.node.sample_hash {
121 + if let Some(node) = self.selected_node()
122 + && let Some(hash) = &node.node.sample_hash {
123 123 self.selected_tags = Arc::new(self.backend.get_sample_tags(hash).unwrap_or_else(|e| {
124 124 warn!("Failed to load tags: {e}");
125 125 Vec::new()
126 126 }));
127 127 }
128 - }
129 128 }
130 129
131 130 /// Refresh the detail panel (analysis + waveform) for the currently selected sample.
@@ -133,14 +132,13 @@ impl BrowserState {
133 132 self.selected_analysis = None;
134 133 self.selected_waveform = None;
135 134
136 - if let Some(node) = self.selected_node() {
137 - if let Some(hash) = &node.node.sample_hash {
135 + if let Some(node) = self.selected_node()
136 + && let Some(hash) = &node.node.sample_hash {
138 137 self.selected_analysis = self.backend.get_analysis(hash)
139 138 .unwrap_or(None);
140 139 self.selected_waveform = self.backend.get_waveform(hash)
141 140 .unwrap_or(None);
142 141 }
143 - }
144 142 }
145 143
146 144 /// Whether the file list currently shows a ".." parent-directory entry.
@@ -199,8 +197,8 @@ impl BrowserState {
199 197 return;
200 198 }
201 199
202 - if let Some(node) = self.selected_node() {
203 - if node.node.node_type == NodeType::Directory {
200 + if let Some(node) = self.selected_node()
201 + && node.node.node_type == NodeType::Directory {
204 202 self.current_dir = Some(node.node.id);
205 203 self.breadcrumb = self.backend.get_breadcrumb(node.node.id).unwrap_or_else(|e| {
206 204 warn!("Breadcrumb failed: {e}");
@@ -209,7 +207,6 @@ impl BrowserState {
209 207 self.selection.clear();
210 208 self.refresh_contents();
211 209 }
212 - }
213 210 }
214 211
215 212 /// Navigate to the parent directory, or do nothing if already at root.
@@ -99,12 +99,11 @@ impl BrowserState {
99 99 pub fn toggle_preview(&mut self) {
100 100 if self.shared.preview.lock().playing {
101 101 self.stop_preview();
102 - } else if let Some(node) = self.selected_node() {
103 - if let Some(hash) = &node.node.sample_hash {
102 + } else if let Some(node) = self.selected_node()
103 + && let Some(hash) = &node.node.sample_hash {
104 104 let hash = hash.clone();
105 105 self.trigger_preview(&hash);
106 106 }
107 - }
108 107 }
109 108
110 109 /// If autoplay is enabled and the focused node is a sample, preview it.
@@ -112,12 +111,11 @@ impl BrowserState {
112 111 if !self.autoplay {
113 112 return;
114 113 }
115 - if let Some(node) = self.selected_node() {
116 - if let Some(hash) = &node.node.sample_hash {
114 + if let Some(node) = self.selected_node()
115 + && let Some(hash) = &node.node.sample_hash {
117 116 let hash = hash.clone();
118 117 self.trigger_preview(&hash);
119 118 }
120 - }
121 119 }
122 120
123 121 /// Toggle loop mode and persist the setting.
@@ -56,8 +56,8 @@ pub fn draw_detail(ui: &mut egui::Ui, state: &mut BrowserState) {
56 56 // Hover indicator: paint a vertical accent_blue line at the cursor X
57 57 // and a time label above it so the user can see where a click-to-seek
58 58 // would land before committing.
59 - if resp.hovered() {
60 - if let Some(pos) = resp.hover_pos() {
59 + if resp.hovered()
60 + && let Some(pos) = resp.hover_pos() {
61 61 let rect = resp.rect;
62 62 let normalized = ((pos.x - rect.left()) / rect.width()).clamp(0.0, 1.0);
63 63 let total_secs = waveform_data.duration as f32;
@@ -82,15 +82,14 @@ pub fn draw_detail(ui: &mut egui::Ui, state: &mut BrowserState) {
82 82 theme::text_secondary(),
83 83 );
84 84 }
85 - }
86 85 // Click-to-seek: map the click's X position to a 0.0–1.0 fraction
87 86 // within the waveform rect, then set the playback cursor to that frame.
88 - if resp.clicked() {
89 - if let Some(pos) = resp.interact_pointer_pos() {
87 + if resp.clicked()
88 + && let Some(pos) = resp.interact_pointer_pos() {
90 89 let rect = resp.rect;
91 90 let normalized = ((pos.x - rect.left()) / rect.width()).clamp(0.0, 1.0);
92 - if let Some(hash) = &node.node.sample_hash {
93 - if state.previewing_hash.as_deref() == Some(hash) {
91 + if let Some(hash) = &node.node.sample_hash
92 + && state.previewing_hash.as_deref() == Some(hash) {
94 93 let mut playback = state.shared.preview.lock();
95 94 if let Some(ref buf) = playback.buffer {
96 95 let total_frames = if playback.streaming {
@@ -102,9 +101,7 @@ pub fn draw_detail(ui: &mut egui::Ui, state: &mut BrowserState) {
102 101 .min((playback.decoded_frames.max(1) - 1) as f64);
103 102 }
104 103 }
105 - }
106 104 }
107 - }
108 105
109 106 ui.add_space(theme::section_spacing());
110 107 }
@@ -226,8 +223,8 @@ pub fn draw_detail(ui: &mut egui::Ui, state: &mut BrowserState) {
226 223 || ui.small_button("+").on_hover_text("Add tag").clicked()
227 224 {
228 225 let tag = state.tag_input.trim().to_string();
229 - if !tag.is_empty() {
230 - if let Some(ref hash) = node.node.sample_hash {
226 + if !tag.is_empty()
227 + && let Some(ref hash) = node.node.sample_hash {
231 228 if audiofiles_core::tags::validate_tag(&tag).is_ok() {
232 229 let _ = state.backend.add_tag(hash, &tag);
233 230 state.tag_input.clear();
@@ -236,15 +233,14 @@ pub fn draw_detail(ui: &mut egui::Ui, state: &mut BrowserState) {
236 233 state.status = format!("Invalid tag: {tag}");
237 234 }
238 235 }
239 - }
240 236 }
241 237 });
242 238
243 239 // Tag suggestions based on classification. Per-classification dismissals
244 240 // let the user say "I never tag kicks with `percussion`" once and have
245 241 // the suggestion stop appearing on every future kick.
246 - if let Some(ref analysis) = state.selected_analysis {
247 - if let Some(ref class) = analysis.classification {
242 + if let Some(ref analysis) = state.selected_analysis
243 + && let Some(ref class) = analysis.classification {
248 244 let class_str = class.to_string();
249 245 let dismissed_for_class = state
250 246 .dismissed_suggestions
@@ -273,12 +269,10 @@ pub fn draw_detail(ui: &mut egui::Ui, state: &mut BrowserState) {
273 269 )
274 270 .on_hover_text(format!("Add tag: {sug}"))
275 271 .clicked()
276 - {
277 - if let Some(ref hash) = node.node.sample_hash {
272 + && let Some(ref hash) = node.node.sample_hash {
278 273 let _ = state.backend.add_tag(hash, sug);
279 274 state.refresh_selected_tags();
280 275 }
281 - }
282 276 // Painted X (two crossed line_segments) — matches the
283 277 // Phase 4 M-8 X-icon precedent in instrument_panel.rs
284 278 // rather than a literal "x" glyph. Muted stroke since
@@ -350,7 +344,6 @@ pub fn draw_detail(ui: &mut egui::Ui, state: &mut BrowserState) {
350 344 ui.ctx().request_repaint();
351 345 }
352 346 }
353 - }
354 347
355 348 }); // end of Tags CollapsingHeader
356 349
@@ -359,12 +352,11 @@ pub fn draw_detail(ui: &mut egui::Ui, state: &mut BrowserState) {
359 352 .default_open(true)
360 353 .show(ui, |ui| {
361 354 ui.horizontal(|ui| {
362 - if ui.button("Copy Path").on_hover_text("Copy file path to clipboard").clicked() {
363 - if let Some(path) = state.selected_sample_path() {
355 + if ui.button("Copy Path").on_hover_text("Copy file path to clipboard").clicked()
356 + && let Some(path) = state.selected_sample_path() {
364 357 state.status = format!("Copied: {path}");
365 358 ui.ctx().copy_text(path);
366 359 }
367 - }
368 360 if let Some(hash) = &node.node.sample_hash {
369 361 let hash = hash.clone();
370 362 if ui.button("Edit").on_hover_text("Open sample editor (E)").clicked() {
@@ -148,8 +148,8 @@ fn draw_waveform_section(ui: &mut egui::Ui, state: &mut BrowserState, hash: &str
148 148 // first so a click in the editor auditions from the clicked point —
149 149 // previously the click was a silent no-op until preview was started
150 150 // elsewhere.
151 - if resp.clicked() {
152 - if let Some(pos) = resp.interact_pointer_pos() {
151 + if resp.clicked()
152 + && let Some(pos) = resp.interact_pointer_pos() {
153 153 let rect = resp.rect;
154 154 let normalized = ((pos.x - rect.left()) / rect.width()).clamp(0.0, 1.0);
155 155 if state.previewing_hash.as_deref() != Some(hash) {
@@ -166,7 +166,6 @@ fn draw_waveform_section(ui: &mut egui::Ui, state: &mut BrowserState, hash: &str
166 166 .min((playback.decoded_frames.max(1) - 1) as f64);
167 167 }
168 168 }
169 - }
170 169 }
171 170 }
172 171
@@ -209,11 +208,10 @@ fn trim_handle(ui: &mut egui::Ui, rect: egui::Rect, frac: f32, id_salt: &str) ->
209 208 ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
210 209 }
211 210 let mut new_frac = frac;
212 - if resp.dragged() {
213 - if let Some(p) = resp.interact_pointer_pos() {
211 + if resp.dragged()
212 + && let Some(p) = resp.interact_pointer_pos() {
214 213 new_frac = ((p.x - rect.left()) / rect.width()).clamp(0.0, 1.0);
215 214 }
216 - }
217 215 let width = if active { 2.5 } else { 1.0 };
218 216 ui.painter_at(rect).line_segment(
219 217 [egui::pos2(x, rect.top()), egui::pos2(x, rect.bottom())],
@@ -534,16 +532,14 @@ fn draw_result_section(ui: &mut egui::Ui, state: &mut BrowserState) {
534 532
535 533 let mut mode = state.edit.result_mode;
536 534 ui.horizontal(|ui| {
537 - if ui.radio_value(&mut mode, Some(EditResultMode::Replace), "Replace original").changed() {
538 - if let Some(m) = mode {
535 + if ui.radio_value(&mut mode, Some(EditResultMode::Replace), "Replace original").changed()
536 + && let Some(m) = mode {
539 537 state.set_edit_result_mode(m);
540 538 }
541 - }
542 - if ui.radio_value(&mut mode, Some(EditResultMode::Sibling), "Create sibling").changed() {
543 - if let Some(m) = mode {
539 + if ui.radio_value(&mut mode, Some(EditResultMode::Sibling), "Create sibling").changed()
540 + && let Some(m) = mode {
544 541 state.set_edit_result_mode(m);
545 542 }
546 - }
547 543 });
548 544 // Replace mode advisory. The previous "no in-app undo" copy was retired
549 545 // when the inline Undo affordance landed (C-1 part 2). The hint still
@@ -115,9 +115,9 @@ pub fn draw_configure_export(ui: &mut egui::Ui, state: &mut BrowserState) {
115 115 }
116 116
117 117 // Device file size limit warning
118 - if let Some(ref profile_name) = config.device_profile {
119 - if let Some(profile) = available_profiles.iter().find(|p| &p.name == profile_name) {
120 - if let Some(max_bytes) = profile.max_file_size_bytes {
118 + if let Some(ref profile_name) = config.device_profile
119 + && let Some(profile) = available_profiles.iter().find(|p| &p.name == profile_name)
120 + && let Some(max_bytes) = profile.max_file_size_bytes {
121 121 // Estimate: duration * sample_rate * channels * bytes_per_sample
122 122 // Use worst case: stereo 24-bit at 48kHz = 288000 bytes/sec
123 123 let bytes_per_sec: f64 = 288_000.0;
@@ -148,8 +148,6 @@ pub fn draw_configure_export(ui: &mut egui::Ui, state: &mut BrowserState) {
148 148 );
149 149 }
150 150 }
151 - }
152 - }
153 151
154 152 // Disk space check (M-3): estimate from actual per-item durations
155 153 // and the current encoding config rather than a fixed 10 MB/item
@@ -181,15 +179,14 @@ pub fn draw_configure_export(ui: &mut egui::Ui, state: &mut BrowserState) {
181 179 if ui.button("Cancel").clicked() {
182 180 state.import_mode = ImportMode::None;
183 181 }
184 - if ui.button("Export").clicked() {
185 - if let ImportMode::ConfigureExport { ref items, ref config, .. } =
182 + if ui.button("Export").clicked()
183 + && let ImportMode::ConfigureExport { ref items, ref config, .. } =
186 184 state.import_mode
187 185 {
188 186 let items = items.clone();
189 187 let config = config.clone();
190 188 state.run_export(items, config);
191 189 }
192 - }
193 190 });
194 191 ui.add_space(theme::space::XS);
195 192 });
@@ -258,8 +255,8 @@ pub fn draw_configure_export(ui: &mut egui::Ui, state: &mut BrowserState) {
258 255 // device's supported formats / rates / depths / channels
259 256 // so the user knows what the lock is hiding, not just
260 257 // that something is hidden.
261 - if let Some(ref name) = config.device_profile {
262 - if let Some(profile) =
258 + if let Some(ref name) = config.device_profile
259 + && let Some(profile) =
263 260 available_profiles.iter().find(|p| &p.name == name)
264 261 {
265 262 ui.label(
@@ -289,7 +286,6 @@ pub fn draw_configure_export(ui: &mut egui::Ui, state: &mut BrowserState) {
289 286 );
290 287 }
291 288 }
292 - }
293 289 }
294 290 ui.add_space(theme::space::MD);
295 291 }
@@ -351,8 +347,8 @@ pub fn draw_configure_export(ui: &mut egui::Ui, state: &mut BrowserState) {
351 347 }
352 348 );
353 349
354 - if needs_encoding_options {
355 - if let ImportMode::ConfigureExport { ref mut config, .. } = state.import_mode {
350 + if needs_encoding_options
351 + && let ImportMode::ConfigureExport { ref mut config, .. } = state.import_mode {
356 352 // Sample rate
357 353 ui.label(egui::RichText::new("Sample Rate").strong());
358 354 let rates: [(Option<u32>, &str); 4] = [
@@ -382,7 +378,6 @@ pub fn draw_configure_export(ui: &mut egui::Ui, state: &mut BrowserState) {
382 378 }
383 379 ui.add_space(theme::space::MD);
384 380 }
385 - }
386 381
387 382 // --- Channels ---
388 383 ui.label(egui::RichText::new("Channels").strong());
@@ -431,8 +426,8 @@ pub fn draw_configure_export(ui: &mut egui::Ui, state: &mut BrowserState) {
431 426 ui.add_space(theme::space::MD);
432 427
433 428 // --- Naming pattern (when flattened) ---
434 - if let ImportMode::ConfigureExport { ref mut config, ref items, .. } = state.import_mode {
435 - if config.flatten {
429 + if let ImportMode::ConfigureExport { ref mut config, ref items, .. } = state.import_mode
430 + && config.flatten {
436 431 ui.label(egui::RichText::new("Naming Pattern").strong());
437 432 let mut pattern = config.naming_pattern.clone().unwrap_or_default();
438 433
@@ -509,7 +504,6 @@ pub fn draw_configure_export(ui: &mut egui::Ui, state: &mut BrowserState) {
509 504 }
510 505 ui.add_space(theme::space::MD);
511 506 }
512 - }
513 507
514 508 // --- Destination ---
515 509 ui.label(egui::RichText::new("Destination").strong());
@@ -517,15 +511,14 @@ pub fn draw_configure_export(ui: &mut egui::Ui, state: &mut BrowserState) {
517 511 ui.horizontal(|ui| {
518 512 let dest_display = config.destination.display().to_string();
519 513 ui.label(&dest_display);
520 - if ui.button("Browse...").clicked() {
521 - if let Some(path) = rfd::FileDialog::new()
514 + if ui.button("Browse...").clicked()
515 + && let Some(path) = rfd::FileDialog::new()
522 516 .set_title("Export Destination")
523 517 .set_directory(&config.destination)
524 518 .pick_folder()
525 519 {
526 520 config.destination = path;
527 521 }
528 - }
529 522 });
530 523 }
531 524 });
@@ -631,8 +624,8 @@ pub fn draw_export_complete(ui: &mut egui::Ui, state: &mut BrowserState) {
631 624 // without navigating Finder/Explorer themselves. Suppressed when
632 625 // the destination wasn't stashed (e.g. export driven from outside
633 626 // the wizard's run_export path).
634 - if let Some(dest) = state.last_export_destination.clone() {
635 - if ui.button("Open destination folder").clicked() {
627 + if let Some(dest) = state.last_export_destination.clone()
628 + && ui.button("Open destination folder").clicked() {
636 629 #[cfg(target_os = "macos")]
637 630 let _ = std::process::Command::new("open").arg(&dest).spawn();
638 631 #[cfg(target_os = "linux")]
@@ -642,7 +635,6 @@ pub fn draw_export_complete(ui: &mut egui::Ui, state: &mut BrowserState) {
642 635 .args(["/c", "start", "", &dest.display().to_string()])
643 636 .spawn();
644 637 }
645 - }
646 638 });
647 639 });
648 640 }
@@ -59,14 +59,13 @@ pub fn draw_file_list(
59 59 ui.horizontal(|ui| {
60 60 widgets::step_number(ui, 1);
61 61 ui.label("Drop a folder of samples onto this window, or click ");
62 - if ui.link("Import").clicked() {
63 - if let Some(path) = rfd::FileDialog::new()
62 + if ui.link("Import").clicked()
63 + && let Some(path) = rfd::FileDialog::new()
64 64 .set_title("Quick Import Folder")
65 65 .pick_folder()
66 66 {
67 67 state.quick_import_folder(path);
68 68 }
69 - }
70 69 });
71 70 ui.add_space(theme::space::SM);
72 71
@@ -112,14 +111,13 @@ pub fn draw_file_list(
112 111 tooltip: Some("Choose a folder of samples to import"),
113 112 }),
114 113 );
115 - if clicked {
116 - if let Some(path) = rfd::FileDialog::new()
114 + if clicked
115 + && let Some(path) = rfd::FileDialog::new()
117 116 .set_title("Import folder")
118 117 .pick_folder()
119 118 {
120 119 state.quick_import_folder(path);
121 120 }
122 - }
123 121 // Quiet link to bring the welcome screen back if the user dismissed it.
124 122 ui.vertical_centered(|ui| {
125 123 ui.add_space(theme::space::LG);
@@ -350,8 +348,8 @@ pub fn draw_file_list(
350 348 }
351 349 let Some(hash) = node.node.sample_hash.as_ref() else { return; };
352 350 if node.cloud_only {
353 - if let Some(sync) = sync_manager {
354 - if ui
351 + if let Some(sync) = sync_manager
352 + && ui
355 353 .button("Download")
356 354 .on_hover_text("Fetch this sample from the cloud")
357 355 .clicked()
@@ -367,7 +365,6 @@ pub fn draw_file_list(
367 365 "Sync not ready — open the Sync panel first".to_string();
368 366 }
369 367 }
370 - }
371 368 } else {
372 369 let is_playing = state.previewing_hash.as_deref() == Some(hash)
373 370 && state.shared.preview.lock().playing;
@@ -495,25 +492,23 @@ fn draw_name_column(
495 492 state.enter_directory();
496 493 }
497 494 NodeType::Sample => {
498 - if !node.cloud_only {
499 - if let Some(hash) = &node.node.sample_hash {
495 + if !node.cloud_only
496 + && let Some(hash) = &node.node.sample_hash {
500 497 let hash = hash.clone();
501 498 state.trigger_preview(&hash);
502 499 }
503 - }
504 500 }
505 501 }
506 502 }
507 503
508 504 // Drag source for instrument zone assignment (not for cloud-only)
509 - if state.instrument_visible && node.node.node_type == NodeType::Sample && !node.cloud_only {
510 - if let Some(hash) = &node.node.sample_hash {
505 + if state.instrument_visible && node.node.node_type == NodeType::Sample && !node.cloud_only
506 + && let Some(hash) = &node.node.sample_hash {
511 507 resp.dnd_set_drag_payload(DragPayload {
512 508 hash: hash.to_string(),
513 509 name: node.node.name.clone(),
514 510 });
515 511 }
516 - }
517 512
518 513 // Native OS drag-out to Finder/DAW (only when instrument panel is closed)
519 514 #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
@@ -564,6 +559,7 @@ fn draw_name_column(
564 559
565 560 /// Draw the analysis data columns (duration, classification, BPM, key, peak dB, tags)
566 561 /// for a single file-list row. Each visible column emits a `row.col()` call.
562 + #[allow(clippy::too_many_arguments)] // one column-renderer; the args map 1:1 to visible columns
567 563 fn draw_analysis_columns(
568 564 row: &mut egui_extras::TableRow,
569 565 node: &VfsNodeWithAnalysis,