RemoveFromLibrary deletes the audio_files rows the way the scan's own orphan cleanup does and records each path in excluded_paths. The exclusion is not an enhancement: without it the next scan finds the file, sees no row and imports it again, so the button undoes itself. The soft scan compares files on disk against rows in the database, so surveyAudioFiles and countAudioFiles both take the exclusion set — otherwise an excluded path makes the two disagree forever and queues a full scan on every launch. Deleting a row cascades to queue_tracks, so the removal calls the same CompactQueue hook RemoveLibrary does. Also lands the requested badge: library-status-indicator is a button again where it can act, utils/library-status.ts states once what owning and wanting mean, and the long-declared queued state finally has a producer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm
86 lines
2.7 KiB
Go
86 lines
2.7 KiB
Go
// Package shortcuts manages keyboard shortcut configuration.
|
|
package shortcuts
|
|
|
|
// Config holds user-customized keyboard shortcut bindings.
|
|
// Keys are action IDs (e.g. "player.playPause"), values are
|
|
// key combo strings in canonical format (e.g. "Ctrl+F", "Space").
|
|
type Config struct {
|
|
Bindings map[string]string `toml:"Bindings"`
|
|
}
|
|
|
|
// DefaultBindings returns the default keyboard shortcut bindings.
|
|
// Follows hybrid style: Space/arrows for player, Ctrl+key for app actions.
|
|
func DefaultBindings() map[string]string {
|
|
return map[string]string{
|
|
// Player controls (Global scope, no modifier)
|
|
"player.playPause": "Space",
|
|
"player.next": "N",
|
|
"player.previous": "P",
|
|
"player.volumeUp": "Up",
|
|
"player.volumeDown": "Down",
|
|
"player.seekForward": "Right",
|
|
"player.seekBack": "Left",
|
|
"player.shuffle": "S",
|
|
"player.repeat": "R",
|
|
"player.mute": "M",
|
|
|
|
// Navigation (Global scope)
|
|
"nav.search": "/",
|
|
"nav.searchAlt": "Ctrl+F",
|
|
"nav.queue": "Q",
|
|
|
|
// App actions
|
|
"app.selectAll": "Ctrl+A",
|
|
"app.shortcuts": "?",
|
|
|
|
// Panel-specific (track list). `tracklist.delete` spent six
|
|
// phases advertised in Settings with nothing on the other end of
|
|
// it, because "remove from library" did not exist and it was not
|
|
// clear what it would remove. It now removes the row and
|
|
// excludes the path from future scans, and leaves the file on
|
|
// disk — and the key only *opens the confirmation*, never
|
|
// performs the removal, which is the only version defensible one
|
|
// keystroke from a focused row.
|
|
"tracklist.play": "Enter",
|
|
"tracklist.delete": "Delete",
|
|
|
|
// Panel-specific (autotag review). These are the keys the
|
|
// autotag page used to bind on its own document listener, which
|
|
// fired from every other page and could not arbitrate with the
|
|
// global bindings above. As panel bindings they apply only
|
|
// while that page is the one on screen.
|
|
"autotag.apply": "A",
|
|
"autotag.skip": "S",
|
|
"autotag.leave": "L",
|
|
"autotag.paste": "U",
|
|
"autotag.search": "F",
|
|
"autotag.next": "Down",
|
|
"autotag.previous": "Up",
|
|
}
|
|
}
|
|
|
|
// ApplyDefaults fills any missing bindings with defaults.
|
|
// Existing user customizations are preserved.
|
|
func (c *Config) ApplyDefaults() {
|
|
if c.Bindings == nil {
|
|
c.Bindings = DefaultBindings()
|
|
|
|
return
|
|
}
|
|
|
|
defaults := DefaultBindings()
|
|
for action, key := range defaults {
|
|
if _, exists := c.Bindings[action]; !exists {
|
|
c.Bindings[action] = key
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate checks that the config is well-formed.
|
|
func (c *Config) Validate() error {
|
|
c.ApplyDefaults()
|
|
// No validation errors possible — any string is a valid binding.
|
|
// Conflict detection is a frontend UX concern, not a config error.
|
|
return nil
|
|
}
|