Skip to main content

max / audiofiles

audiofiles: home preview/instrument + detail clusters into sub-structs Finish the BrowserState decompose: move the 12 preview/instrument fields into PreviewUiState and the 9 detail-panel fields into DetailUiState, matching the existing sub-struct pattern. Leaves sidebar_visible, status, and all_tags loose (footer/left-panel/global-cache, not detail state).
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-25 14:26 UTC
Signed with PGP, not checked
Commit: 29b1411a472efa85d35a22eb433d719157dcab29
Parent: fc43aa6
8 files changed, +79 insertions, -41 deletions
@@ -510,7 +510,8 @@
510 510 self.detail.selected_tags =
511 511 std::sync::Arc::new(self.backend.get_sample_tags(&hash).unwrap_or_default());
512 512 if let Ok(prov) = self.backend.sample_tag_provenance(&hash) {
513 - self.detail.selected_tag_sources = prov.into_iter().map(|(t, s, r)| (t, (s, r))).collect();
513 + self.detail.selected_tag_sources =
514 + prov.into_iter().map(|(t, s, r)| (t, (s, r))).collect();
514 515 }
515 516 }
516 517 }
@@ -473,7 +473,8 @@
473 473 /// stops appearing on every future sample of that class.
474 474 pub fn dismiss_suggestion(&mut self, classification: &str, tag: &str) {
475 475 let entry = self
476 - .detail.dismissed_suggestions
476 + .detail
477 + .dismissed_suggestions
477 478 .entry(classification.to_string())
478 479 .or_default();
479 480 if !entry.iter().any(|t| t == tag) {
@@ -514,7 +515,8 @@
514 515 /// during exploration can start fresh.
515 516 pub fn reset_dismissed_suggestions(&mut self) {
516 517 let n: usize = self
517 - .detail.dismissed_suggestions
518 + .detail
519 + .dismissed_suggestions
518 520 .values()
519 521 .map(std::vec::Vec::len)
520 522 .sum();
@@ -3137,7 +3137,10 @@
3137 3137
3138 3138 state.trigger_preview(&hash);
3139 3139 // Synchronous state set immediately on dispatch.
3140 - assert_eq!(state.preview.previewing_hash.as_deref(), Some(hash.as_str()));
3140 + assert_eq!(
3141 + state.preview.previewing_hash.as_deref(),
3142 + Some(hash.as_str())
3143 + );
3141 3144 assert!(
3142 3145 state.status.starts_with("Playing:"),
3143 3146 "status was: {}",
@@ -3187,7 +3190,10 @@
3187 3190
3188 3191 // First toggle: not playing -> starts preview.
3189 3192 state.toggle_preview();
3190 - assert_eq!(state.preview.previewing_hash.as_deref(), Some(hash.as_str()));
3193 + assert_eq!(
3194 + state.preview.previewing_hash.as_deref(),
3195 + Some(hash.as_str())
3196 + );
3191 3197 await_playing(&state);
3192 3198
3193 3199 // Second toggle: playing -> stops.
@@ -3204,7 +3210,10 @@
3204 3210 state.refresh_contents();
3205 3211
3206 3212 state.trigger_preview("deadbeef");
3207 - assert!(state.preview.previewing_hash.is_none(), "no preview should start");
3213 + assert!(
3214 + state.preview.previewing_hash.is_none(),
3215 + "no preview should start"
3216 + );
3208 3217 assert!(
3209 3218 !state.status.is_empty(),
3210 3219 "an error status should be surfaced"
@@ -23,21 +23,25 @@
23 23 if let Some(ref waveform_data) = state.detail.selected_waveform {
24 24 // Compute playback position as a 0.0–1.0 fraction for the waveform cursor.
25 25 // Only valid when the currently-playing hash matches this node's hash.
26 - let playback_pos = if state.preview.previewing_hash.as_deref() == node.node.sample_hash.as_deref() {
27 - let playback = state.shared.preview.lock();
28 - if playback.playing {
29 - if let Some(ref buf) = playback.buffer {
30 - // During streaming, the buffer grows so use the metadata estimate
31 - // for a stable cursor. Fall back to current buffer size otherwise.
32 - let total_frames = if playback.streaming {
33 - playback
34 - .total_frames_estimate
35 - .unwrap_or(playback.decoded_frames)
36 - } else {
37 - buf.data.len() / 2
38 - };
39 - if total_frames > 0 {
40 - Some((playback.position_frac / total_frames as f64) as f32)
26 + let playback_pos =
27 + if state.preview.previewing_hash.as_deref() == node.node.sample_hash.as_deref() {
28 + let playback = state.shared.preview.lock();
29 + if playback.playing {
30 + if let Some(ref buf) = playback.buffer {
31 + // During streaming, the buffer grows so use the metadata estimate
32 + // for a stable cursor. Fall back to current buffer size otherwise.
33 + let total_frames = if playback.streaming {
34 + playback
35 + .total_frames_estimate
36 + .unwrap_or(playback.decoded_frames)
37 + } else {
38 + buf.data.len() / 2
39 + };
40 + if total_frames > 0 {
41 + Some((playback.position_frac / total_frames as f64) as f32)
42 + } else {
43 + None
44 + }
41 45 } else {
42 46 None
43 47 }
@@ -46,10 +50,7 @@
46 50 }
47 51 } else {
48 52 None
49 - }
50 - } else {
51 - None
52 - };
53 + };
53 54
54 55 let resp = waveform::draw_waveform(ui, waveform_data, playback_pos, 120.0);
55 56 // Hover indicator: paint a vertical action line at the cursor X
@@ -320,7 +321,8 @@
320 321 {
321 322 let class_str = class.to_string();
322 323 let dismissed_for_class = state
323 - .detail.dismissed_suggestions
324 + .detail
325 + .dismissed_suggestions
324 326 .get(&class_str)
325 327 .cloned()
326 328 .unwrap_or_default();
@@ -389,7 +391,8 @@
389 391 const UNDO_WINDOW: f32 = 5.0;
390 392 // Bind the tag directly so there's no re-fetch + expect() in render.
391 393 let undo_tag = state
392 - .detail.last_dismissed_suggestion
394 + .detail
395 + .last_dismissed_suggestion
393 396 .as_ref()
394 397 .filter(|(c, _, at)| {
395 398 c == &class_str && at.elapsed().as_secs_f32() < UNDO_WINDOW
@@ -460,11 +463,13 @@
460 463 // these the button "works" but always returns zero results,
461 464 // which reads as a broken feature instead of a missing prereq.
462 465 let has_spectral = state
463 - .detail.selected_analysis
466 + .detail
467 + .selected_analysis
464 468 .as_ref()
465 469 .is_some_and(|a| a.spectral_centroid.is_some() || a.spectral_bandwidth.is_some());
466 470 let has_fingerprint = state
467 - .detail.selected_analysis
471 + .detail
472 + .selected_analysis
468 473 .as_ref()
469 474 .is_some_and(|a| a.fingerprint.is_some());
470 475 egui::CollapsingHeader::new("Discovery")
@@ -260,7 +260,8 @@
260 260 ui.label(egui::RichText::new("Trim").strong());
261 261
262 262 let sample_rate = state
263 - .detail.selected_analysis
263 + .detail
264 + .selected_analysis
264 265 .as_ref()
265 266 .map_or(44100, |a| a.sample_rate);
266 267 let total = state.edit.total_frames;
@@ -307,7 +308,11 @@
307 308 ui.label(egui::RichText::new("Levels").strong());
308 309
309 310 // Current peak display + gain clipping warning
310 - let current_peak = state.detail.selected_analysis.as_ref().and_then(|a| a.peak_db);
311 + let current_peak = state
312 + .detail
313 + .selected_analysis
314 + .as_ref()
315 + .and_then(|a| a.peak_db);
311 316 if let Some(peak) = current_peak {
312 317 let predicted = peak + state.edit.gain_db;
313 318 if predicted > 0.0 {
@@ -464,7 +469,8 @@
464 469 // range only if duration is missing (un-analyzed sample). Prevents the
465 470 // silent-failure / undefined-behaviour case where positions exceed length.
466 471 let duration_ms_cap = state
467 - .detail.selected_analysis
472 + .detail
473 + .selected_analysis
468 474 .as_ref()
469 475 .map_or(f64::MAX, |a| a.duration * 1000.0);
470 476
@@ -190,8 +190,11 @@
190 190 ui.separator();
191 191
192 192 ui.label(
193 - egui::RichText::new(format!("Root: {}", note_name(state.preview.instrument_root_note)))
194 - .color(theme::content_secondary()),
193 + egui::RichText::new(format!(
194 + "Root: {}",
195 + note_name(state.preview.instrument_root_note)
196 + ))
197 + .color(theme::content_secondary()),
195 198 );
196 199
197 200 ui.separator();
@@ -407,7 +410,8 @@
407 410 }
408 411 // Release notes that are no longer under the pointer (drag across keys)
409 412 let to_release: Vec<u8> = state
410 - .preview.piano_held_notes
413 + .preview
414 + .piano_held_notes
411 415 .iter()
412 416 .filter(|&&n| n != note)
413 417 .copied()
@@ -449,7 +453,8 @@
449 453 egui::vec2(key_width - 1.0, white_height),
450 454 );
451 455
452 - let is_active = active_notes.contains(&note) || state.preview.piano_held_notes.contains(&note);
456 + let is_active =
457 + active_notes.contains(&note) || state.preview.piano_held_notes.contains(&note);
453 458 let is_root = note == state.preview.instrument_root_note;
454 459
455 460 let fill = if is_active {
@@ -476,8 +481,8 @@
476 481
477 482 // Draw black keys on top
478 483 for &(key_rect, black_note) in &black_key_rects {
479 - let is_active =
480 - active_notes.contains(&black_note) || state.preview.piano_held_notes.contains(&black_note);
484 + let is_active = active_notes.contains(&black_note)
485 + || state.preview.piano_held_notes.contains(&black_note);
481 486 let is_root = black_note == state.preview.instrument_root_note;
482 487
483 488 let fill = if is_active {
@@ -700,7 +700,8 @@
700 700 .color(theme::content_secondary()),
701 701 );
702 702 let dismissed_total: usize = state
703 - .detail.dismissed_suggestions
703 + .detail
704 + .dismissed_suggestions
704 705 .values()
705 706 .map(std::vec::Vec::len)
706 707 .sum();
@@ -229,7 +229,13 @@
229 229 state.preview.show_midi_window = !state.preview.show_midi_window;
230 230 }
231 231
232 - if widgets::toolbar_toggle(ui, "Loop", state.preview.loop_enabled, "Toggle loop (L)", None) {
232 + if widgets::toolbar_toggle(
233 + ui,
234 + "Loop",
235 + state.preview.loop_enabled,
236 + "Toggle loop (L)",
237 + None,
238 + ) {
233 239 state.toggle_loop();
234 240 }
235 241
@@ -305,7 +311,10 @@
305 311 ui.close();
306 312 }
307 313 if ui
308 - .button(format!("{}Loop (L)", active_dot(state.preview.loop_enabled)))
314 + .button(format!(
315 + "{}Loop (L)",
316 + active_dot(state.preview.loop_enabled)
317 + ))
309 318 .clicked()
310 319 {
311 320 state.toggle_loop();