Skip to main content

max / audiofiles

Flip the loose-files warning to the described screen The first screen to serve rather than sit beside one. `quasi::integrity` answers it now, `overlays::draw_loose_files_warning` and `LooseFilesAction` are gone, and the `quasi` feature joins the default set because a screen depends on it. Parity was asserted before the deletion and the test went with the module it compared against: the two sides agreed on every control, with one named difference. The shipped Locate ends in an ellipsis promising a dialog; the described one does not, because what a route does after it is called is the host's and a host with no picker opens nothing. The real behaviour change, argued in `integrity`'s header rather than smoothed over: the shipped overlay was modal and stopped you. A described screen cannot raise itself, so the count is said in the status band and the screen is one act away from it. Which of the two is right wants an eyeball, and neither is sayable today.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-22 17:03 UTC
Signed with PGP, not checked
Commit: 84c613dfa075e0c0f5046d1b6a47126e0e91ce09
Parent: 1b77cf0
5 files changed, +48 insertions, -87 deletions
@@ -5,7 +5,12 @@
5 5 edition.workspace = true
6 6
7 7 [features]
8 - default = ["device-profiles"]
8 + # `quasi` is on by default as of 2026-08-22, when the loose-files warning became
9 + # the first screen to serve from the description rather than sit beside one. Off,
10 + # that screen and every one flipped after it is simply missing. The feature
11 + # itself goes when the last flip lands; until then it is a switch with one
12 + # position that is still spelled.
13 + default = ["device-profiles", "quasi"]
9 14 device-profiles = ["dep:audiofiles-rhai", "dep:rayon"]
10 15 # The described screens, off by default. On, `crate::quasi` compiles and the
11 16 # shipped egui panels in `crate::ui` are untouched; off, none of it is built.
@@ -154,7 +154,7 @@
154 154 overlays::draw_dir_rename_modal(ctx, state);
155 155 }
156 156 if state.loose_files.show_loose_files_warning {
157 - overlays::draw_loose_files_warning(ctx, state);
157 + crate::quasi::panel::draw_integrity(ctx, state);
158 158 }
159 159 if state.import_wf.pending_import_preflight.is_some() {
160 160 overlays::draw_import_preflight(ctx, state);
@@ -59,6 +59,7 @@
59 59 queue: Option<Runtime>,
60 60 sweep: Option<Runtime>,
61 61 filters: Option<Runtime>,
62 + integrity: Option<Runtime>,
62 63 /// Whether the described main window is open.
63 64 pub show_shell: bool,
64 65 /// Whether the described detail panel is open.
@@ -453,6 +454,41 @@
453 454 }
454 455 }
455 456
457 + /// Draw the loose-files warning, and act on whatever was pressed.
458 + ///
459 + /// The first screen to serve rather than sit beside one, 2026-08-22. What the
460 + /// shipped overlay did that this does not is stop you: it drew as a modal over
461 + /// a dimmed app, and a described screen cannot raise itself, so the count is
462 + /// said in the status band and this is one act away from it. `integrity`'s
463 + /// header argues that difference; it is recorded rather than smoothed over.
464 + ///
465 + /// Refreshed every frame, because the worker that re-checks the vault moves the
466 + /// count with nothing pressed.
467 + pub fn draw_integrity(ctx: &egui::Context, state: &mut BrowserState) {
468 + let intents = RefCell::new(Vec::new());
469 + let mut runtime = state.described.integrity.take();
470 + let host = Host {
471 + state,
472 + sync: None,
473 + themes: themes(),
474 + intents: &intents,
475 + };
476 + let closed = window(
477 + ctx,
478 + "Loose-files mode warning",
479 + &mut runtime,
480 + &host,
481 + "/library/loose-files",
482 + true,
483 + );
484 + state.described.integrity = runtime;
485 + apply(ctx, state, None, intents.into_inner());
486 + if closed {
487 + state.described.integrity = None;
488 + state.dismiss_loose_files_warning();
489 + }
490 + }
491 +
456 492 /// Do what a described screen asked the app to do to itself.
457 493 ///
458 494 /// **The frame boundary.** A route holds `&BrowserState` and cannot select a
@@ -466,6 +466,11 @@
466 466 /// record of what that flip changed. There is deliberately no "ignore whatever
467 467 /// differs" option: an unexplained difference is the thing this file exists to
468 468 /// find.
469 + ///
470 + /// One allowance recurs and is worth knowing before it surprises you: a shipped
471 + /// modal drawn in an `egui::Window` announces a button carrying the window's own
472 + /// name, because that is its title bar's collapsing control. It is chrome, in
473 + /// the same class as a `CollapsingHeader`, and a modal test drops it by name.
469 474 #[derive(Debug, Clone, Default)]
470 475 pub(super) struct Parity {
471 476 dropped: Vec<String>,
@@ -453,91 +453,6 @@
453 453 }
454 454 }
455 455
456 - /// Draw the loose-files mode integrity warning overlay.
457 - ///
458 - /// Three-button layout (C-2): Locate (recover sources), Purge (delete the
459 - /// registry entries plus their tags / analysis / history), Cancel (dismiss
460 - /// without acting). Locate is the recovery path that the prior two-button
461 - /// version was missing entirely.
462 - pub fn draw_loose_files_warning(ctx: &egui::Context, state: &mut BrowserState) {
463 - let count = state.loose_files.loose_files_missing_count;
464 - if count == 0 {
465 - return;
466 - }
467 -
468 - let prompt = format!(
469 - "{count} sample{} in this vault {} missing source {}.",
470 - if count == 1 { "" } else { "s" },
471 - if count == 1 { "has a" } else { "have" },
472 - if count == 1 { "file" } else { "files" },
473 - );
474 -
475 - let mut action: Option<LooseFilesAction> = None;
476 - widgets::modal_window(ctx, "Loose-files mode warning", false, Some(480.0), |ui| {
477 - ui.label(egui::RichText::new(&prompt).strong());
478 - ui.add_space(theme::space::bound());
479 - ui.label(
480 - "The original files may have been moved or deleted. These samples \
481 - cannot be played or exported until the files are restored.",
482 - );
483 - ui.add_space(theme::space::bound());
484 - // C-2: name the blast radius of Purge explicitly. Tags, analysis, and
485 - // history are the data the user has invested time in, they should
486 - // see what Purge takes before reaching for it.
487 - ui.label(
488 - egui::RichText::new(
489 - "Tags, analysis results, and history for these samples will \
490 - be permanently deleted by Purge.",
491 - )
492 - .small()
493 - .color(theme::warning()),
494 - );
495 - ui.add_space(theme::space::peer());
496 - ui.horizontal(|ui| {
497 - if ui
498 - .button("Cancel")
499 - .on_hover_text("Dismiss without acting")
500 - .clicked()
501 - {
502 - action = Some(LooseFilesAction::Cancel);
503 - }
504 - if ui
505 - .button("Locate missing files...")
506 - .on_hover_text(
507 - "Pick a folder; audiofiles will hash-verify and re-point any \
508 - samples that match. Tags and analysis are preserved.",
509 - )
510 - .clicked()
511 - {
512 - action = Some(LooseFilesAction::Locate);
513 - }
514 - if widgets::danger_button(ui, "Purge").clicked() {
515 - action = Some(LooseFilesAction::Purge);
516 - }
517 - });
518 - });
519 -
520 - match action {
521 - Some(LooseFilesAction::Cancel) => state.dismiss_loose_files_warning(),
522 - Some(LooseFilesAction::Purge) => state.purge_missing_loose_files(),
523 - Some(LooseFilesAction::Locate) => {
524 - state
525 - .dialogs
526 - .pick_folder("Locate missing sample files", |s, p| {
527 - s.locate_missing_loose_files(&p);
528 - });
529 - }
530 - None => {}
531 - }
532 - }
533 -
534 - /// Outcome of the loose-files-warning dialog (C-2).
535 - enum LooseFilesAction {
536 - Cancel,
537 - Locate,
538 - Purge,
539 - }
540 -
541 456 /// Draw the active bulk modal (tag, move, or rename).
542 457 pub fn draw_bulk_modal(ctx: &egui::Context, state: &mut BrowserState) {
543 458 let modal_kind = match &state.bulk_modal {