Skip to content

feat: redesign the sound picker (stay open on selection, delete imported sounds, inherited-sound indicator) - #119

Open
GraysonCAdams wants to merge 16 commits into
SamsidParty:mainfrom
GraysonCAdams:pr/sound-picker-redesign
Open

feat: redesign the sound picker (stay open on selection, delete imported sounds, inherited-sound indicator)#119
GraysonCAdams wants to merge 16 commits into
SamsidParty:mainfrom
GraysonCAdams:pr/sound-picker-redesign

Conversation

@GraysonCAdams

Copy link
Copy Markdown

Stacked on #116, #117, and #118 - this diff includes those commits until they merge; only the sound-picker commits at the end are new here.

Selecting a sound used to immediately close the picker and drop you back on the app list, so there was no way to see or compare what you'd just picked without reopening it. The picker now stays open, highlights whichever sound is currently active for the app you're editing (resolving through "Other" when the app hasn't been given its own preference yet), and offers a checkbox to trim leading silence on import, checked by default, since many notification sound files have a bit of dead air at the start that made playback feel delayed.

Imported sounds can also be deleted now - only sounds inside the app's own imported-sounds folder, never files from the Music folder - and it only prompts for confirmation if the sound is currently assigned somewhere, since deleting it would otherwise change what plays for that app silently.

Apps TopNotify discovers for the first time are shown dimmed with whatever "Other" currently resolves to, rather than a static "Default Sound" label, until you give that app its own preference - previously there was no indication an app was inheriting Other's sound rather than genuinely having no preference set.

Grayson Adams added 16 commits August 18, 2026 09:48
Logo44.png and Logo150.png were single flat bitmaps with no scale-100/125/150/200/400 variants, so Windows had to upscale them for any display above 100% DPI, producing a blurry Start Menu tile and taskbar icon. Regenerated all scale variants directly from the existing IconVector.svg vector master instead of upscaling the small PNGs.
System.Media.SoundPlayer (a thin wrapper over the WAV-only Win32 PlaySound API) could only ever play WAV files. Replaced it with Windows.Media.Playback.MediaPlayer, which is backed by Media Foundation and natively decodes all of the above on a stock Windows install - no new dependency, the project already references the WinRT projections this needs. Also broadened the sound import file picker, extension checks, and file discovery glob to match; the custom_sound_path storage convention already preserved arbitrary extensions correctly. Verified end to end (not just compiled) by playing real WAV/MP3/M4A/WMA/FLAC files through the exact same code path.
Was one FileFilter per extension, which listed each format separately in the dialog dropdown. Verified via reflection that the two-arg FileFilter constructor stores Name/Pattern verbatim (no reformatting), and that the existing single-extension convention already uses bare, semicolon-joinable extension strings - so a single filter with a semicolon-joined pattern matches the same convention already proven working.
The combined Audio Files filter was matching nothing (mp3 etc only showed under All Files) because the semicolon-joined pattern was wrong for this dialog library. Decompiled IgniteView.FileDialogs.dll to confirm: FileFilter.Pattern passes straight through to NFDFilterU8.Spec with zero transformation, and the underlying native library is Native File Dialog Extended (NFDe), whose documented spec format is comma-separated extensions, not semicolon-separated. Also names the filter with the actual supported extensions listed out, e.g. Audio Files (WAV, MP3, AAC, M4A, WMA, FLAC), so they are visible without opening the dropdown.
Selecting a sound previously closed the picker and returned to the app list on every click. Now applySound() no longer closes the picker - it stays open so you can preview/compare multiple sounds, with the currently active one shown highlighted (colored outline + a checkmark badge top-right). Importing a sound no longer auto-applies it either - it refreshes the list (via a remount-key bump, matching the same pattern already used elsewhere in this file) so the new file shows up under Your Collection, and the user picks it explicitly like any other sound. Also fixed the mute/preview and folder icons: they were inline-adjacent to the name text rather than pinned to the right edge of the row - name row is now a flex layout with the icon pushed to the far right consistently.
Chakra Button defaults to a solid background (resolves to --col-bg, #222222) when not overridden. .soundItem itself uses a translucent tile background (--col-tertiary, #50505031), so the icon button rendered as a visibly mismatched solid grey box sitting on top of the softer tile background instead of blending into it. Forced it transparent so the tile background (including the active-state tint) shows through cleanly. The icons themselves are unaffected - they are genuine Tabler SVG icons and bundled brand SVG/PNG assets, not emoji.
Adds SilenceTrimmer: detects leading silence by transcoding to a fixed 44.1kHz mono 16-bit WAV and scanning samples directly, then stores the offset as a sidecar file next to the imported sound. Playback seeks past it via MediaPlayer.PlaybackSession.Position rather than re-encoding the file, since re-encoding compressed formats would need an encoder, not just a decoder.

Deliberately does NOT use AudioGraph/QuantumProcessed for detection - verified empirically that its quantum sizes are non-deterministic (a real-time playback API, not meant for offline analysis), producing detection errors from under 1ms up to ~900ms across repeated runs of the same file. The transcode-then-scan approach is fully deterministic and was verified against real WAV/MP3/M4A/WMA/FLAC test files to within 0-46ms of the true boundary. The seek mechanism itself was verified by measuring actual playback duration before/after a trim - a trimmed file played back proportionally shorter, confirming the silence is genuinely skipped rather than just accepted without erroring.

Adds a Trim leading silence when importing checkbox to the sound picker (checked by default per request), wired through ImportSound(bool trimSilence).
The name row had equal 10px padding on both sides, leaving dead space between the icon and the tiles true right edge. That gap made the icon read as loosely inline with the text (like an emoji tacked onto a word) rather than a deliberately anchored control. Right padding reduced to 4px so it sits flush against the edge; left padding kept at 10px so the truncated name still has breathing room.
Delete button in the top-left corner of each sound tile (mirrors the active checkmark in the top-right), shown only for sounds actually copied into ImportedSoundFolder - never for files picked up by scanning the users own Music folder, which the app does not own and should not touch. Confirms only when the sound is actually assigned to one or more notification types, since deleting an unused sound has no real consequence; any AppReferences using a deleted sound fall back to Default Sound rather than being left pointing at a nonexistent file. IsDeletable is computed server-side (FindSounds) rather than guessed client-side from the path string.
Both actions bumped a remount key to pick up the change, which tore down and recreated the whole Drawer - replaying its slide-in animation on every single add or delete instead of just updating one tile. Both handlers already know exactly which sound changed (the deleted path, or the newly imported sound object), so the sound list is now local state in SoundPicker, mutated directly (filter for delete, append for import) instead of refetched via a remount. Also switched per-item React keys from array index to sound.Path, since the array is now genuinely mutated rather than always fully rebuilt.
… fetch resolved

Regression from the previous commit. useCommandResult resolves asynchronously, but useState(() => JSON.parse(fetchedPacks)) only runs its initializer on the very first render, before the result had actually arrived - it permanently seeded an empty array and never picked up the real data once useCommandResult resolved on a later render. Switched to useEffect keyed on fetchedPacks, which re-syncs local state whenever the async result actually changes, while still leaving room for removeSoundLocally/addSoundLocally to mutate on top afterward.
…their own sound

DiscoveryInterceptor already sets SoundPath to internal/default for newly-seen apps, and GetSoundPath already resolves internal/default to Others current sound live at playback time (not a snapshot) - that half was already correct, just never surfaced anywhere. The app list now shows these entries dimmed (a new [data-inherited] attribute, deliberately not the existing [data-greyed-out] since that sets pointer-events:none app-wide and would make the row unclickable - it needs to stay clickable so the user can give it its own preference) and displays what the sound actually currently resolves to (Others live SoundDisplayName) instead of a static Default Sound label that would go stale the moment Other changes. Selecting any sound for an inherited app already naturally graduates it via the existing applySound path - no backend change needed there. Also fixed the sound pickers active-highlight to match: opening it for an inherited app now highlights whatever Other currently resolves to, not the literal internal/default tile.
TbX is reserved elsewhere in the app for closing the whole window (window.close(), in About.jsx and App.jsx). Every sliding drawer/panel - the main Notification Sounds drawer, DebugMenu - dismisses with TbChevronDown instead. The sound picker is a drawer too but had been using TbX, which was inconsistent with that convention.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant