The history stack has been global since the Android back gesture landed -- every navigation is an entry and `popstate` restores any of them in either direction. What the report describes as "back is tab-scoped" is that the only way back was a detail view's own button, which leaves the screen with the view it belongs to: click over to Tracks and the album you were reading is still one entry away with nothing on screen saying so. `<nav-history>` is that affordance, plus `nav.back` / `nav.forward` on Alt+Left / Alt+Right -- the browser's own combination, and clear of the bare arrows that seek, since a binding matches on its full canonical string. Forward is not back negated, which is why the old `pushedEntries` counter is gone rather than extended: `popstate` carries no direction and fires identically both ways, so one counter decremented on every pop reads a forward as a second back. Each entry carries its index and the shell keeps the current one and a high-water mark, which also survives a jump of more than one. The buttons dispatch the events the rest of the app already dispatches rather than calling `history` themselves -- the shell owns the guard that stops a press at the root leaving the app, and a second caller reaching for history is how the old `navStack` came to disagree with the platform. Below 900px the control stands down: the top bar is what runs out of room first below that, and nothing becomes unreachable -- the shortcuts are global at every width and the phone has the platform's gesture. Closes #6
185 lines
5.1 KiB
TypeScript
185 lines
5.1 KiB
TypeScript
/**
|
|
* What each shortcut action is called, where it applies, and what it is
|
|
* bound to out of the box.
|
|
*
|
|
* This lived as a private static in `config-page`, which meant Settings
|
|
* was the only place in the app the key bindings were written down —
|
|
* and Settings renders three of the four categories, so the autotag
|
|
* keys were written down nowhere at all. The `?` overlay and the
|
|
* Settings editor now read one table.
|
|
*/
|
|
|
|
export interface ShortcutMeta {
|
|
label: string;
|
|
category: string;
|
|
scope: string;
|
|
defaultKey: string;
|
|
}
|
|
|
|
/** The categories, in the order both surfaces show them. */
|
|
export const SHORTCUT_CATEGORIES = [
|
|
'Player',
|
|
'Navigation',
|
|
'App',
|
|
'Autotag',
|
|
] as const;
|
|
|
|
export const SHORTCUT_META: Record<string, ShortcutMeta> = {
|
|
'player.playPause': {
|
|
label: 'Play / Pause',
|
|
category: 'Player',
|
|
scope: 'global',
|
|
defaultKey: 'Space',
|
|
},
|
|
'player.next': {
|
|
label: 'Next Track',
|
|
category: 'Player',
|
|
scope: 'global',
|
|
defaultKey: 'N',
|
|
},
|
|
'player.previous': {
|
|
label: 'Previous Track',
|
|
category: 'Player',
|
|
scope: 'global',
|
|
defaultKey: 'P',
|
|
},
|
|
'player.volumeUp': {
|
|
label: 'Volume Up',
|
|
category: 'Player',
|
|
scope: 'global',
|
|
defaultKey: 'Up',
|
|
},
|
|
'player.volumeDown': {
|
|
label: 'Volume Down',
|
|
category: 'Player',
|
|
scope: 'global',
|
|
defaultKey: 'Down',
|
|
},
|
|
'player.seekForward': {
|
|
label: 'Seek Forward',
|
|
category: 'Player',
|
|
scope: 'global',
|
|
defaultKey: 'Right',
|
|
},
|
|
'player.seekBack': {
|
|
label: 'Seek Back',
|
|
category: 'Player',
|
|
scope: 'global',
|
|
defaultKey: 'Left',
|
|
},
|
|
'player.shuffle': {
|
|
label: 'Toggle Shuffle',
|
|
category: 'Player',
|
|
scope: 'global',
|
|
defaultKey: 'S',
|
|
},
|
|
'player.repeat': {
|
|
label: 'Cycle Repeat',
|
|
category: 'Player',
|
|
scope: 'global',
|
|
defaultKey: 'R',
|
|
},
|
|
'player.mute': {
|
|
label: 'Toggle Mute',
|
|
category: 'Player',
|
|
scope: 'global',
|
|
defaultKey: 'M',
|
|
},
|
|
'nav.search': {
|
|
label: 'Focus Search',
|
|
category: 'Navigation',
|
|
scope: 'global',
|
|
defaultKey: '/',
|
|
},
|
|
'nav.searchAlt': {
|
|
label: 'Focus Search (Alt)',
|
|
category: 'Navigation',
|
|
scope: 'global',
|
|
defaultKey: 'Ctrl+F',
|
|
},
|
|
'nav.queue': {
|
|
label: 'Toggle Queue',
|
|
category: 'Navigation',
|
|
scope: 'global',
|
|
defaultKey: 'Q',
|
|
},
|
|
'nav.back': {
|
|
label: 'Back',
|
|
category: 'Navigation',
|
|
scope: 'global',
|
|
defaultKey: 'Alt+Left',
|
|
},
|
|
'nav.forward': {
|
|
label: 'Forward',
|
|
category: 'Navigation',
|
|
scope: 'global',
|
|
defaultKey: 'Alt+Right',
|
|
},
|
|
'app.shortcuts': {
|
|
label: 'Keyboard Shortcuts',
|
|
category: 'App',
|
|
scope: 'global',
|
|
defaultKey: '?',
|
|
},
|
|
'app.selectAll': {
|
|
label: 'Select All',
|
|
category: 'App',
|
|
scope: 'global',
|
|
defaultKey: 'Ctrl+A',
|
|
},
|
|
'tracklist.play': {
|
|
label: 'Play Selected',
|
|
category: 'Navigation',
|
|
scope: 'panel:track-list',
|
|
defaultKey: 'Enter',
|
|
},
|
|
'tracklist.delete': {
|
|
label: 'Remove from Library',
|
|
category: 'Navigation',
|
|
scope: 'panel:track-list',
|
|
defaultKey: 'Delete',
|
|
},
|
|
'autotag.apply': {
|
|
label: 'Apply Match',
|
|
category: 'Autotag',
|
|
scope: 'panel:autotag',
|
|
defaultKey: 'A',
|
|
},
|
|
'autotag.skip': {
|
|
label: 'Skip Folder',
|
|
category: 'Autotag',
|
|
scope: 'panel:autotag',
|
|
defaultKey: 'S',
|
|
},
|
|
'autotag.leave': {
|
|
label: 'Leave As Is',
|
|
category: 'Autotag',
|
|
scope: 'panel:autotag',
|
|
defaultKey: 'L',
|
|
},
|
|
'autotag.paste': {
|
|
label: 'Paste Release URL',
|
|
category: 'Autotag',
|
|
scope: 'panel:autotag',
|
|
defaultKey: 'U',
|
|
},
|
|
'autotag.search': {
|
|
label: 'Search Candidates',
|
|
category: 'Autotag',
|
|
scope: 'panel:autotag',
|
|
defaultKey: 'F',
|
|
},
|
|
'autotag.next': {
|
|
label: 'Next Folder',
|
|
category: 'Autotag',
|
|
scope: 'panel:autotag',
|
|
defaultKey: 'Down',
|
|
},
|
|
'autotag.previous': {
|
|
label: 'Previous Folder',
|
|
category: 'Autotag',
|
|
scope: 'panel:autotag',
|
|
defaultKey: 'Up',
|
|
},
|
|
};
|