max / audiofiles
5 files changed,
+97 insertions,
-11 deletions
| @@ -23,7 +23,7 @@ pub fn draw_browser( | |||
| 23 | 23 | ctx.request_repaint(); | |
| 24 | 24 | } | |
| 25 | 25 | handle_keyboard(ctx, state); | |
| 26 | - | draw_normal_browser(ctx, state); | |
| 26 | + | draw_normal_browser(ctx, state, sync_manager); | |
| 27 | 27 | } | |
| 28 | 28 | ImportMode::ConfigureImport { .. } => { | |
| 29 | 29 | import_screens::draw_configure_import(ctx, state); | |
| @@ -134,12 +134,16 @@ pub fn draw_browser( | |||
| 134 | 134 | } | |
| 135 | 135 | ||
| 136 | 136 | /// Draw the main browser layout: toolbar, footer, sidebar, detail panel, and file list. | |
| 137 | - | fn draw_normal_browser(ctx: &egui::Context, state: &mut BrowserState) { | |
| 137 | + | fn draw_normal_browser( | |
| 138 | + | ctx: &egui::Context, | |
| 139 | + | state: &mut BrowserState, | |
| 140 | + | sync_manager: Option<&audiofiles_sync::SyncManager>, | |
| 141 | + | ) { | |
| 138 | 142 | // Top toolbar (breadcrumb + search) | |
| 139 | 143 | egui::TopBottomPanel::top("toolbar") | |
| 140 | 144 | .exact_height(56.0) | |
| 141 | 145 | .show(ctx, |ui| { | |
| 142 | - | toolbar::draw_toolbar(ui, state); | |
| 146 | + | toolbar::draw_toolbar(ui, state, sync_manager); | |
| 143 | 147 | }); | |
| 144 | 148 | ||
| 145 | 149 | // Bottom footer |
| @@ -16,6 +16,10 @@ fn available_disk_space(path: &Path) -> Option<u64> { | |||
| 16 | 16 | use std::os::unix::ffi::OsStrExt; | |
| 17 | 17 | ||
| 18 | 18 | let c_path = CString::new(path.as_os_str().as_bytes()).ok()?; | |
| 19 | + | // SAFETY: `statvfs` is a POSIX FFI call. `c_path` is a valid NUL-terminated | |
| 20 | + | // C string (from CString::new). `stat` is zero-initialized, which is a valid | |
| 21 | + | // representation for libc::statvfs. The pointer to `stat` is valid for the | |
| 22 | + | // duration of the call. | |
| 19 | 23 | unsafe { | |
| 20 | 24 | let mut stat: libc::statvfs = std::mem::zeroed(); | |
| 21 | 25 | if libc::statvfs(c_path.as_ptr(), &mut stat) == 0 { | |
| @@ -31,6 +35,10 @@ fn available_disk_space(path: &Path) -> Option<u64> { | |||
| 31 | 35 | use std::os::windows::ffi::OsStrExt; | |
| 32 | 36 | let wide: Vec<u16> = path.as_os_str().encode_wide().chain(std::iter::once(0)).collect(); | |
| 33 | 37 | let mut free_bytes: u64 = 0; | |
| 38 | + | // SAFETY: `GetDiskFreeSpaceExW` is a Win32 FFI call. `wide` is a valid | |
| 39 | + | // NUL-terminated UTF-16 string (from encode_wide + chain(once(0))). | |
| 40 | + | // `free_bytes` is a valid aligned u64 for the out-parameter. The pointer | |
| 41 | + | // to `wide` is valid for the duration of the call. | |
| 34 | 42 | unsafe { | |
| 35 | 43 | if windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW( | |
| 36 | 44 | windows::core::PCWSTR(wide.as_ptr()), |
| @@ -138,6 +138,10 @@ fn draw_features_tab(ui: &mut egui::Ui) { | |||
| 138 | 138 | ||
| 139 | 139 | ui.heading("Cloud Sync"); | |
| 140 | 140 | ui.label("Sync metadata (tags, organization) across devices. Set up in the Sync panel (toolbar). Metadata sync is free. Blob sync (sample files) is tiered by storage."); | |
| 141 | + | ui.add_space(8.0); | |
| 142 | + | ||
| 143 | + | ui.heading("System Tray"); | |
| 144 | + | ui.label("audiofiles runs in the system tray when you close the window. Right-click the tray icon for Show Window and Quit. Playback continues in the background while minimized."); | |
| 141 | 145 | }); | |
| 142 | 146 | } | |
| 143 | 147 |
| @@ -6,9 +6,13 @@ use crate::state::BrowserState; | |||
| 6 | 6 | use crate::ui::theme; | |
| 7 | 7 | ||
| 8 | 8 | /// Draw the breadcrumb bar with VFS selector, path segments, search bar, and import button. | |
| 9 | - | pub fn draw_toolbar(ui: &mut egui::Ui, state: &mut BrowserState) { | |
| 9 | + | pub fn draw_toolbar( | |
| 10 | + | ui: &mut egui::Ui, | |
| 11 | + | state: &mut BrowserState, | |
| 12 | + | sync_manager: Option<&audiofiles_sync::SyncManager>, | |
| 13 | + | ) { | |
| 10 | 14 | ui.horizontal(|ui| { | |
| 11 | - | draw_breadcrumb(ui, state); | |
| 15 | + | draw_breadcrumb(ui, state, sync_manager); | |
| 12 | 16 | }); | |
| 13 | 17 | ||
| 14 | 18 | // Similarity mode banner | |
| @@ -206,7 +210,11 @@ pub fn draw_toolbar(ui: &mut egui::Ui, state: &mut BrowserState) { | |||
| 206 | 210 | /// segments for each ancestor directory, and a right-aligned Import button. | |
| 207 | 211 | /// | |
| 208 | 212 | /// Clicking a non-terminal breadcrumb segment navigates to that directory. | |
| 209 | - | fn draw_breadcrumb(ui: &mut egui::Ui, state: &mut BrowserState) { | |
| 213 | + | fn draw_breadcrumb( | |
| 214 | + | ui: &mut egui::Ui, | |
| 215 | + | state: &mut BrowserState, | |
| 216 | + | sync_manager: Option<&audiofiles_sync::SyncManager>, | |
| 217 | + | ) { | |
| 210 | 218 | // Logo | |
| 211 | 219 | ui.label( | |
| 212 | 220 | egui::RichText::new("af/") | |
| @@ -346,8 +354,41 @@ fn draw_breadcrumb(ui: &mut egui::Ui, state: &mut BrowserState) { | |||
| 346 | 354 | state.start_export_flow(None); | |
| 347 | 355 | } | |
| 348 | 356 | ||
| 349 | - | if ui.button("Sync") | |
| 350 | - | .on_hover_text("Cloud sync settings") | |
| 357 | + | // Sync button with status indicator | |
| 358 | + | let sync_label = if let Some(sync) = sync_manager { | |
| 359 | + | let status = sync.status(); | |
| 360 | + | match status.state { | |
| 361 | + | audiofiles_sync::SyncState::Syncing => "Sync \u{21BB}".to_string(), // ↻ | |
| 362 | + | audiofiles_sync::SyncState::Ready if status.pending_changes > 0 => { | |
| 363 | + | format!("Sync ({})", status.pending_changes) | |
| 364 | + | } | |
| 365 | + | audiofiles_sync::SyncState::Disconnected => "Sync \u{2013}".to_string(), // – | |
| 366 | + | _ => "Sync \u{2713}".to_string(), // ✓ | |
| 367 | + | } | |
| 368 | + | } else { | |
| 369 | + | "Sync".to_string() | |
| 370 | + | }; | |
| 371 | + | let sync_tooltip = if let Some(sync) = sync_manager { | |
| 372 | + | let status = sync.status(); | |
| 373 | + | match status.state { | |
| 374 | + | audiofiles_sync::SyncState::Syncing => "Syncing...".to_string(), | |
| 375 | + | audiofiles_sync::SyncState::Ready if status.pending_changes > 0 => { | |
| 376 | + | format!("{} pending changes", status.pending_changes) | |
| 377 | + | } | |
| 378 | + | audiofiles_sync::SyncState::Ready => { | |
| 379 | + | match status.last_sync_at { | |
| 380 | + | Some(ref t) => format!("Synced: {t}"), | |
| 381 | + | None => "Connected, not yet synced".to_string(), | |
| 382 | + | } | |
| 383 | + | } | |
| 384 | + | audiofiles_sync::SyncState::Disconnected => "Not connected".to_string(), | |
| 385 | + | _ => "Cloud sync settings".to_string(), | |
| 386 | + | } | |
| 387 | + | } else { | |
| 388 | + | "Cloud sync settings".to_string() | |
| 389 | + | }; | |
| 390 | + | if ui.button(&sync_label) | |
| 391 | + | .on_hover_text(&sync_tooltip) | |
| 351 | 392 | .clicked() | |
| 352 | 393 | { | |
| 353 | 394 | state.sync.show_panel = !state.sync.show_panel; |
| @@ -3,7 +3,17 @@ | |||
| 3 | 3 | ## Status | |
| 4 | 4 | Done: All pre-beta phases + Phase 11. Active: None. Next: Vocal layer 2, sample forge (phases 10-16). | |
| 5 | 5 | ||
| 6 | - | v0.4.0. Audit grade A. 773 tests. Discoverability upgraded C+ → A-. | |
| 6 | + | v0.4.0. Audit grade A (Run 20, 2026-05-04). 780 tests. All remediations complete. | |
| 7 | + | ||
| 8 | + | --- | |
| 9 | + | ||
| 10 | + | ## Audit Run 20 (2026-05-04) | |
| 11 | + | ||
| 12 | + | All items resolved: | |
| 13 | + | - Split app/main.rs: activation.rs (198L), vault_setup.rs (218L), main.rs 1296→899L | |
| 14 | + | - Fixed Relaxed → Acquire/Release in analysis/worker.rs (6 atomic ops) | |
| 15 | + | - Added 7 sync tests (download query, upload query, resolve upsert/delete edge cases) | |
| 16 | + | - Aligned import_directory_recursive: added sorting, audio filtering, skipped-dir checks | |
| 7 | 17 | ||
| 8 | 18 | --- | |
| 9 | 19 | ||
| @@ -179,14 +189,14 @@ Overall grade: B+. Grades: Complexity B, Completeness B, Learnability B+, Discov | |||
| 179 | 189 | - [ ] Or: add "Recently Deleted" trash section in sidebar with recovery | |
| 180 | 190 | - [ ] Bulk duplicate (create copies of selected samples) | |
| 181 | 191 | - [x] Copy metadata: apply tags/BPM/key from one sample to selected others | |
| 182 | - | - [ ] Sync status indicator in toolbar: synced/syncing/pending count | |
| 192 | + | - [x] Sync status indicator in toolbar: synced/syncing/pending count — button label shows ✓/↻/count/–, tooltip shows detail | |
| 183 | 193 | - [ ] Show sync conflict resolution when two devices edit same sample | |
| 184 | 194 | - [ ] Smart folders should be dynamic (re-compute on visit, not static snapshots) | |
| 185 | 195 | ||
| 186 | 196 | ### Documentation | |
| 187 | 197 | ||
| 188 | 198 | - [x] Expand help overlay beyond shortcuts: add "Features" tab with search, filters, collections, tags, import, export | |
| 189 | - | - [ ] Document system tray integration in settings or help | |
| 199 | + | - [x] Document system tray integration in settings or help — added to Features tab in help overlay | |
| 190 | 200 | - [x] Show device profile count in export dialog header | |
| 191 | 201 | ||
| 192 | 202 | ## UX Audit Findings (2026-05-03) | |
| @@ -207,6 +217,25 @@ Overall grade: B+. Grades: Complexity B, Completeness B+, Learnability C+, Disco | |||
| 207 | 217 | ||
| 208 | 218 | --- | |
| 209 | 219 | ||
| 220 | + | ## Rust-Fuzz Findings (2026-05-04) | |
| 221 | + | ||
| 222 | + | Rust quality audit: unsafe discipline, memory efficiency, error handling, smart pointers. | |
| 223 | + | Overall grade: A-. Unsafe: CLEAN. Memory: SOME WASTE. Errors: ELEGANT. Pointers: JUSTIFIED. | |
| 224 | + | ||
| 225 | + | ### Must Fix | |
| 226 | + | - [x] [rust-fuzz] `bulk_ops.rs:84-103` — `selected_nodes()` clones full structs; add field-specific accessors that extract ids/hashes without cloning | |
| 227 | + | - [x] [rust-fuzz] `import_workflow.rs:585,598` — full hash Vec cloned to escape `if let` borrow; use `std::mem::replace` to move data out | |
| 228 | + | ||
| 229 | + | ### Should Fix | |
| 230 | + | - [x] [rust-fuzz] `export_screens.rs:19,34` — add `// SAFETY:` comments to `statvfs` and `GetDiskFreeSpaceExW` unsafe blocks | |
| 231 | + | - [ ] [rust-fuzz] `file_list_menus.rs:255,327` — `selected_nodes()` in drag path; iterate indices by reference instead | |
| 232 | + | - [ ] [rust-fuzz] `bulk_ops.rs:248-382` — use `Option::take()` instead of cloning to escape `if let` borrows (4 sites) | |
| 233 | + | - [ ] [rust-fuzz] `sidebar.rs:361` — tag list cloned every frame when search empty; pass reference or cache tree | |
| 234 | + | - [ ] [rust-fuzz] `export/mod.rs:171` — silent row-drop via `.filter_map(|r| r.ok())`; add `tracing::warn!` | |
| 235 | + | - [ ] [rust-fuzz] `vault_setup.rs:195`, `license.rs:247` — silent mkdir/trial-save failures; log warnings | |
| 236 | + | ||
| 237 | + | --- | |
| 238 | + | ||
| 210 | 239 | ## Shared Code Extraction (Cross-Project) | |
| 211 | 240 | - [ ] Updater UI: extract updater.js from GO/BB into shared module | |
| 212 | 241 | - [ ] Saved queries: unify GO saved views, BB query feeds, AF smart folders |