- Fix custom tag keys being silently dropped by TagLib — unknown keys are now uppercased before write since TagLib requires all-caps Vorbis field names
- Add real-time validation for invalid characters (= and non-ASCII) in custom tag keys
- Show 'Field already exists' error on Enter/+ attempt rather than silently doing nothing
- Resolve typed tag labels to canonical keys (e.g. 'Album Sort' → albumSort) to prevent duplicate entries under a different key
- Atomic duplicate check in hook functional updater prevents race conditions from rapid adds
- Scroll to and focus newly added field after it is inserted
- Consolidate FIELD_LABEL_OVERRIDES, FIELD_TYPE_OVERRIDES, and PICARD_EXTRAS into a single TAG_CONFIG object; extract resolveTagKey helper
* feat(player): add right-click audio output device menu to volume icon
Right-clicking the volume/speaker icon in the player bar now opens a
context menu listing available audio output devices, with the active
device checkmarked and a System default reset option.
Selecting a device writes to the existing audioDeviceId (web) or
mpvAudioDeviceId (mpv) playback settings, so the choice is applied live
and persisted across restarts via the settings store. Unlike the
settings-page selector, the menu is not disabled during playback.
* refactor(player): drop redundant audio menu label, document wrapper div
Address review feedback: remove the self-explanatory ContextMenu.Label
from the audio output menu, and add a comment explaining why the volume
icon is wrapped in a div (Mantine Tooltip does not forward Radix's
asChild onContextMenu/ref to the button).
applyStartupSeek() compared the armed target song against
getQueue().items[player.index], but player.index is a position in the
shuffled order while getQueue().items is always the default order. With
shuffle enabled the lookup resolves to the wrong song, the uniqueId
check fails, and the startup resume-seek is silently cancelled — the
track then plays from 0:00 and the progress poller overwrites the
persisted timestamp.
Use getCurrentSong() instead, which maps the index through
queue.shuffled, matching how the seek target is armed.
Fixes#2202
Co-authored-by: Maurits <WhoCarrot@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- Romaji conversion joined all synced lyric lines into one string. Because the block contained kana somewhere, hasKana passed for the entire array of lyrics.