feat: add miniplayer keyboard shortcuts and fix X button behavior

Add Shift+M toggle shortcut for mini player mode (main and mini window).
Add 1-5 number keys for surface switching (micro/compact/artwork/queue/lyrics)
in mini player context. Add playback shortcuts (Space, Ctrl+Arrow) to mini
player window. Fix X button to close app (or hide to tray) instead of
duplicating the Expand button behavior. Closes #134.
This commit is contained in:
vicrodh committed 2026-03-01 14:04:18 -06:00
1 parent 0f1f6b3716
commit 976e5226b4
8 files changed
+167 -17

No files matched your search

+16 -3
View File
@@ -1402,7 +1402,8 @@
"playback": "Wiedergabe", "playback": "Wiedergabe",
"navigation": "Navigation", "navigation": "Navigation",
"ui": "Oberfläche", "ui": "Oberfläche",
"focus": "Immersiv" "focus": "Immersiv",
"mini": "Mini-Player"
}, },
"actions": { "actions": {
"playbackToggle": "Wiedergabe / Pause", "playbackToggle": "Wiedergabe / Pause",
@@ -1417,10 +1418,16 @@
"uiEscape": "Schließen / Verwerfen", "uiEscape": "Schließen / Verwerfen",
"uiShowShortcuts": "Tastenkürzel anzeigen", "uiShowShortcuts": "Tastenkürzel anzeigen",
"uiOpenLink": "Musik-Link öffnen", "uiOpenLink": "Musik-Link öffnen",
"uiMiniPlayer": "Mini-Player umschalten",
"focusSeekForward": "Vorspulen (5s)", "focusSeekForward": "Vorspulen (5s)",
"focusSeekBack": "Zurückspulen (5s)", "focusSeekBack": "Zurückspulen (5s)",
"focusSeekForwardLong": "Vorspulen (10s)", "focusSeekForwardLong": "Vorspulen (10s)",
"focusSeekBackLong": "Zurückspulen (10s)" "focusSeekBackLong": "Zurückspulen (10s)",
"miniMicro": "Mikro-Ansicht",
"miniCompact": "Kompaktansicht",
"miniArtwork": "Cover-Ansicht",
"miniQueue": "Warteschlange-Ansicht",
"miniLyrics": "Text-Ansicht"
}, },
"descriptions": { "descriptions": {
"playbackToggle": "Wiedergabe umschalten", "playbackToggle": "Wiedergabe umschalten",
@@ -1435,10 +1442,16 @@
"uiEscape": "Aktives Panel oder Modal schließen", "uiEscape": "Aktives Panel oder Modal schließen",
"uiShowShortcuts": "Tastenkürzel-Modal anzeigen", "uiShowShortcuts": "Tastenkürzel-Modal anzeigen",
"uiOpenLink": "Einen Qobuz-Link in der App öffnen", "uiOpenLink": "Einen Qobuz-Link in der App öffnen",
"uiMiniPlayer": "Mini-Player-Modus umschalten",
"focusSeekForward": "5 Sekunden vorspulen", "focusSeekForward": "5 Sekunden vorspulen",
"focusSeekBack": "5 Sekunden zurückspulen", "focusSeekBack": "5 Sekunden zurückspulen",
"focusSeekForwardLong": "10 Sekunden vorspulen", "focusSeekForwardLong": "10 Sekunden vorspulen",
"focusSeekBackLong": "10 Sekunden zurückspulen" "focusSeekBackLong": "10 Sekunden zurückspulen",
"miniMicro": "Zur Mikro-Ansicht wechseln",
"miniCompact": "Zur Kompaktansicht wechseln",
"miniArtwork": "Zur Cover-Ansicht wechseln",
"miniQueue": "Zur Warteschlange-Ansicht wechseln",
"miniLyrics": "Zur Text-Ansicht wechseln"
} }
}, },
"dacWizard": { "dacWizard": {
+16 -3
View File
@@ -1464,7 +1464,8 @@
"playback": "Playback", "playback": "Playback",
"navigation": "Navigation", "navigation": "Navigation",
"ui": "Interface", "ui": "Interface",
"focus": "Immersive" "focus": "Immersive",
"mini": "Mini Player"
}, },
"actions": { "actions": {
"playbackToggle": "Play / Pause", "playbackToggle": "Play / Pause",
@@ -1479,10 +1480,16 @@
"uiEscape": "Close / Dismiss", "uiEscape": "Close / Dismiss",
"uiShowShortcuts": "Show Shortcuts", "uiShowShortcuts": "Show Shortcuts",
"uiOpenLink": "Open Music Link", "uiOpenLink": "Open Music Link",
"uiMiniPlayer": "Toggle Mini Player",
"focusSeekForward": "Seek Forward (5s)", "focusSeekForward": "Seek Forward (5s)",
"focusSeekBack": "Seek Back (5s)", "focusSeekBack": "Seek Back (5s)",
"focusSeekForwardLong": "Seek Forward (10s)", "focusSeekForwardLong": "Seek Forward (10s)",
"focusSeekBackLong": "Seek Back (10s)" "focusSeekBackLong": "Seek Back (10s)",
"miniMicro": "Micro View",
"miniCompact": "Compact View",
"miniArtwork": "Artwork View",
"miniQueue": "Queue View",
"miniLyrics": "Lyrics View"
}, },
"descriptions": { "descriptions": {
"playbackToggle": "Toggle playback", "playbackToggle": "Toggle playback",
@@ -1497,10 +1504,16 @@
"uiEscape": "Close active panel or modal", "uiEscape": "Close active panel or modal",
"uiShowShortcuts": "Show keyboard shortcuts modal", "uiShowShortcuts": "Show keyboard shortcuts modal",
"uiOpenLink": "Open a Qobuz link in the app", "uiOpenLink": "Open a Qobuz link in the app",
"uiMiniPlayer": "Toggle mini player mode",
"focusSeekForward": "Seek forward 5 seconds", "focusSeekForward": "Seek forward 5 seconds",
"focusSeekBack": "Seek back 5 seconds", "focusSeekBack": "Seek back 5 seconds",
"focusSeekForwardLong": "Seek forward 10 seconds", "focusSeekForwardLong": "Seek forward 10 seconds",
"focusSeekBackLong": "Seek back 10 seconds" "focusSeekBackLong": "Seek back 10 seconds",
"miniMicro": "Switch to micro view",
"miniCompact": "Switch to compact view",
"miniArtwork": "Switch to artwork view",
"miniQueue": "Switch to queue view",
"miniLyrics": "Switch to lyrics view"
} }
}, },
"dacWizard": { "dacWizard": {
+16 -3
View File
@@ -1464,7 +1464,8 @@
"playback": "Reproducción", "playback": "Reproducción",
"navigation": "Navegación", "navigation": "Navegación",
"ui": "Interfaz", "ui": "Interfaz",
"focus": "Inmersivo" "focus": "Inmersivo",
"mini": "Mini Reproductor"
}, },
"actions": { "actions": {
"playbackToggle": "Reproducir / Pausar", "playbackToggle": "Reproducir / Pausar",
@@ -1479,10 +1480,16 @@
"uiEscape": "Cerrar / Descartar", "uiEscape": "Cerrar / Descartar",
"uiShowShortcuts": "Mostrar Atajos", "uiShowShortcuts": "Mostrar Atajos",
"uiOpenLink": "Abrir Enlace Musical", "uiOpenLink": "Abrir Enlace Musical",
"uiMiniPlayer": "Alternar Mini Reproductor",
"focusSeekForward": "Avanzar (5s)", "focusSeekForward": "Avanzar (5s)",
"focusSeekBack": "Retroceder (5s)", "focusSeekBack": "Retroceder (5s)",
"focusSeekForwardLong": "Avanzar (10s)", "focusSeekForwardLong": "Avanzar (10s)",
"focusSeekBackLong": "Retroceder (10s)" "focusSeekBackLong": "Retroceder (10s)",
"miniMicro": "Vista Micro",
"miniCompact": "Vista Compacta",
"miniArtwork": "Vista Carátula",
"miniQueue": "Vista Cola",
"miniLyrics": "Vista Letras"
}, },
"descriptions": { "descriptions": {
"playbackToggle": "Alternar reproducción", "playbackToggle": "Alternar reproducción",
@@ -1497,10 +1504,16 @@
"uiEscape": "Cerrar panel activo o modal", "uiEscape": "Cerrar panel activo o modal",
"uiShowShortcuts": "Mostrar modal de atajos de teclado", "uiShowShortcuts": "Mostrar modal de atajos de teclado",
"uiOpenLink": "Abrir un enlace de Qobuz en la app", "uiOpenLink": "Abrir un enlace de Qobuz en la app",
"uiMiniPlayer": "Alternar modo mini reproductor",
"focusSeekForward": "Avanzar 5 segundos", "focusSeekForward": "Avanzar 5 segundos",
"focusSeekBack": "Retroceder 5 segundos", "focusSeekBack": "Retroceder 5 segundos",
"focusSeekForwardLong": "Avanzar 10 segundos", "focusSeekForwardLong": "Avanzar 10 segundos",
"focusSeekBackLong": "Retroceder 10 segundos" "focusSeekBackLong": "Retroceder 10 segundos",
"miniMicro": "Cambiar a vista micro",
"miniCompact": "Cambiar a vista compacta",
"miniArtwork": "Cambiar a vista carátula",
"miniQueue": "Cambiar a vista cola",
"miniLyrics": "Cambiar a vista letras"
} }
}, },
"dacWizard": { "dacWizard": {
+16 -3
View File
@@ -1402,7 +1402,8 @@
"playback": "Lecture", "playback": "Lecture",
"navigation": "Navigation", "navigation": "Navigation",
"ui": "Interface", "ui": "Interface",
"focus": "Immersif" "focus": "Immersif",
"mini": "Mini Lecteur"
}, },
"actions": { "actions": {
"playbackToggle": "Lecture / Pause", "playbackToggle": "Lecture / Pause",
@@ -1417,10 +1418,16 @@
"uiEscape": "Fermer / Annuler", "uiEscape": "Fermer / Annuler",
"uiShowShortcuts": "Afficher les raccourcis", "uiShowShortcuts": "Afficher les raccourcis",
"uiOpenLink": "Ouvrir un lien musical", "uiOpenLink": "Ouvrir un lien musical",
"uiMiniPlayer": "Basculer le mini lecteur",
"focusSeekForward": "Avancer (5s)", "focusSeekForward": "Avancer (5s)",
"focusSeekBack": "Reculer (5s)", "focusSeekBack": "Reculer (5s)",
"focusSeekForwardLong": "Avancer (10s)", "focusSeekForwardLong": "Avancer (10s)",
"focusSeekBackLong": "Reculer (10s)" "focusSeekBackLong": "Reculer (10s)",
"miniMicro": "Vue Micro",
"miniCompact": "Vue Compacte",
"miniArtwork": "Vue Pochette",
"miniQueue": "Vue File d'attente",
"miniLyrics": "Vue Paroles"
}, },
"descriptions": { "descriptions": {
"playbackToggle": "Basculer la lecture", "playbackToggle": "Basculer la lecture",
@@ -1435,10 +1442,16 @@
"uiEscape": "Fermer le panneau actif ou la modale", "uiEscape": "Fermer le panneau actif ou la modale",
"uiShowShortcuts": "Afficher la modale des raccourcis clavier", "uiShowShortcuts": "Afficher la modale des raccourcis clavier",
"uiOpenLink": "Ouvrir un lien Qobuz dans l'application", "uiOpenLink": "Ouvrir un lien Qobuz dans l'application",
"uiMiniPlayer": "Basculer le mode mini lecteur",
"focusSeekForward": "Avancer de 5 secondes", "focusSeekForward": "Avancer de 5 secondes",
"focusSeekBack": "Reculer de 5 secondes", "focusSeekBack": "Reculer de 5 secondes",
"focusSeekForwardLong": "Avancer de 10 secondes", "focusSeekForwardLong": "Avancer de 10 secondes",
"focusSeekBackLong": "Reculer de 10 secondes" "focusSeekBackLong": "Reculer de 10 secondes",
"miniMicro": "Passer à la vue micro",
"miniCompact": "Passer à la vue compacte",
"miniArtwork": "Passer à la vue pochette",
"miniQueue": "Passer à la vue file d'attente",
"miniLyrics": "Passer à la vue paroles"
} }
}, },
"dacWizard": { "dacWizard": {
+18
View File
@@ -194,3 +194,21 @@ export async function setMiniplayerAlwaysOnTop(alwaysOnTop: boolean): Promise<Al
export async function closeMiniplayerWindow(): Promise<void> { export async function closeMiniplayerWindow(): Promise<void> {
await exitMiniplayerMode(); await exitMiniplayerMode();
} }
/**
* Close the app from the mini player.
*
* Triggers the main window's close handler which either:
* - Hides all windows to tray (if close-to-tray is enabled)
* - Quits the app (if close-to-tray is disabled)
*/
export async function closeAppFromMiniplayer(): Promise<void> {
try {
const mainWindow = await Window.getByLabel(MAIN_WINDOW_LABEL);
if (mainWindow) {
await mainWindow.close();
}
} catch (err) {
console.error('[MiniPlayer] Failed to close app:', err);
}
}
+55 -2
View File
@@ -21,7 +21,7 @@ export { eventToShortcut, formatShortcutDisplay, isInputTarget };
// Types // Types
// ============================================================================ // ============================================================================
export type KeybindingCategory = 'playback' | 'navigation' | 'ui' | 'focus'; export type KeybindingCategory = 'playback' | 'navigation' | 'ui' | 'focus' | 'mini';
export interface KeybindingAction { export interface KeybindingAction {
/** Unique identifier, e.g., 'playback.toggle' */ /** Unique identifier, e.g., 'playback.toggle' */
@@ -53,6 +53,7 @@ export const CATEGORIES: CategoryInfo[] = [
{ id: 'navigation', label: 'Navigation', description: 'App navigation' }, { id: 'navigation', label: 'Navigation', description: 'App navigation' },
{ id: 'ui', label: 'Interface', description: 'UI elements' }, { id: 'ui', label: 'Interface', description: 'UI elements' },
{ id: 'focus', label: 'Immersive', description: 'Immersive mode controls' }, { id: 'focus', label: 'Immersive', description: 'Immersive mode controls' },
{ id: 'mini', label: 'Mini Player', description: 'Mini player controls' },
]; ];
export const ACTIONS: KeybindingAction[] = [ export const ACTIONS: KeybindingAction[] = [
@@ -145,6 +146,13 @@ export const ACTIONS: KeybindingAction[] = [
defaultShortcut: 'Ctrl+l', defaultShortcut: 'Ctrl+l',
description: 'Paste and open a Qobuz link', description: 'Paste and open a Qobuz link',
}, },
{
id: 'ui.miniPlayer',
label: 'Toggle Mini Player',
category: 'ui',
defaultShortcut: 'Shift+M',
description: 'Toggle mini player mode',
},
// Focus Mode specific // Focus Mode specific
{ {
@@ -179,6 +187,48 @@ export const ACTIONS: KeybindingAction[] = [
description: 'Seek back 10 seconds', description: 'Seek back 10 seconds',
contextual: true, contextual: true,
}, },
// Mini Player specific
{
id: 'mini.micro',
label: 'Micro View',
category: 'mini',
defaultShortcut: '1',
description: 'Switch to micro view',
contextual: true,
},
{
id: 'mini.compact',
label: 'Compact View',
category: 'mini',
defaultShortcut: '2',
description: 'Switch to compact view',
contextual: true,
},
{
id: 'mini.artwork',
label: 'Artwork View',
category: 'mini',
defaultShortcut: '3',
description: 'Switch to artwork view',
contextual: true,
},
{
id: 'mini.queue',
label: 'Queue View',
category: 'mini',
defaultShortcut: '4',
description: 'Switch to queue view',
contextual: true,
},
{
id: 'mini.lyrics',
label: 'Lyrics View',
category: 'mini',
defaultShortcut: '5',
description: 'Switch to lyrics view',
contextual: true,
},
]; ];
/** Map of default bindings for quick lookup */ /** Map of default bindings for quick lookup */
@@ -412,7 +462,7 @@ export function getConflictingAction(
*/ */
export function handleKeydown( export function handleKeydown(
event: KeyboardEvent, event: KeyboardEvent,
context: { focusMode?: boolean } = {} context: { focusMode?: boolean; miniPlayerMode?: boolean } = {}
): boolean { ): boolean {
// Ignore if we're in an input // Ignore if we're in an input
if (isInputTarget(event)) { if (isInputTarget(event)) {
@@ -431,6 +481,9 @@ export function handleKeydown(
if (action.category === 'focus' && !context.focusMode) { if (action.category === 'focus' && !context.focusMode) {
return false; return false;
} }
if (action.category === 'mini' && !context.miniPlayerMode) {
return false;
}
} }
// Execute callback // Execute callback
+1
View File
@@ -3015,6 +3015,7 @@
}); });
registerAction('ui.sidebar', toggleSidebar); registerAction('ui.sidebar', toggleSidebar);
registerAction('ui.focusMode', toggleFocusMode); registerAction('ui.focusMode', toggleFocusMode);
registerAction('ui.miniPlayer', () => { void enterMiniplayerMode(); });
registerAction('ui.queue', toggleQueue); registerAction('ui.queue', toggleQueue);
registerAction('ui.escape', handleUIEscape); registerAction('ui.escape', handleUIEscape);
registerAction('ui.showShortcuts', () => { isShortcutsModalOpen = true; }); registerAction('ui.showShortcuts', () => { isShortcutsModalOpen = true; });
+29 -3
View File
@@ -48,7 +48,12 @@
setMiniPlayerOpen, setMiniPlayerOpen,
setMiniPlayerSurface setMiniPlayerSurface
} from '$lib/stores/uiStore'; } from '$lib/stores/uiStore';
import { exitMiniplayerMode, setMiniplayerAlwaysOnTop } from '$lib/services/miniplayerService'; import { exitMiniplayerMode, closeAppFromMiniplayer, setMiniplayerAlwaysOnTop } from '$lib/services/miniplayerService';
import {
registerAction,
unregisterAll,
handleKeydown as keybindingHandler
} from '$lib/stores/keybindingsStore';
import { playTrack } from '$lib/services/playbackService'; import { playTrack } from '$lib/services/playbackService';
let playerState = $state<PlayerState>(getPlayerState()); let playerState = $state<PlayerState>(getPlayerState());
@@ -60,6 +65,7 @@
let unlistenMoved: (() => void) | null = null; let unlistenMoved: (() => void) | null = null;
let unlistenResized: (() => void) | null = null; let unlistenResized: (() => void) | null = null;
let unlistenCloseRequested: (() => void) | null = null; let unlistenCloseRequested: (() => void) | null = null;
let removeKeydownListener: (() => void) | null = null;
let unsubscribePlayer: (() => void) | null = null; let unsubscribePlayer: (() => void) | null = null;
let unsubscribeQueue: (() => void) | null = null; let unsubscribeQueue: (() => void) | null = null;
@@ -219,9 +225,27 @@
unlistenCloseRequested = await appWindow.onCloseRequested((event) => { unlistenCloseRequested = await appWindow.onCloseRequested((event) => {
event.preventDefault(); event.preventDefault();
void exitMiniplayerMode(); void closeAppFromMiniplayer();
}); });
// Keyboard shortcuts
registerAction('playback.toggle', handleTogglePlay);
registerAction('playback.next', handleSkipForward);
registerAction('playback.prev', handleSkipBack);
registerAction('ui.miniPlayer', exitMiniplayerMode);
registerAction('ui.escape', exitMiniplayerMode);
registerAction('mini.micro', () => { void handleSurfaceChange('micro'); });
registerAction('mini.compact', () => { void handleSurfaceChange('compact'); });
registerAction('mini.artwork', () => { void handleSurfaceChange('artwork'); });
registerAction('mini.queue', () => { void handleSurfaceChange('queue'); });
registerAction('mini.lyrics', () => { void handleSurfaceChange('lyrics'); });
function onKeydown(event: KeyboardEvent) {
keybindingHandler(event, { miniPlayerMode: true });
}
document.addEventListener('keydown', onKeydown);
removeKeydownListener = () => document.removeEventListener('keydown', onKeydown);
console.info('[MiniPlayer] Window ready'); console.info('[MiniPlayer] Window ready');
}); });
@@ -233,7 +257,9 @@
unlistenMoved?.(); unlistenMoved?.();
unlistenResized?.(); unlistenResized?.();
unlistenCloseRequested?.(); unlistenCloseRequested?.();
removeKeydownListener?.();
unregisterAll();
stopActiveLineUpdates(); stopActiveLineUpdates();
stopWatching(); stopWatching();
stopQueueEventListener(); stopQueueEventListener();
@@ -433,7 +459,7 @@
}} }}
onTogglePin={handleToggleAlwaysOnTop} onTogglePin={handleToggleAlwaysOnTop}
onExpand={exitMiniplayerMode} onExpand={exitMiniplayerMode}
onClose={exitMiniplayerMode} onClose={closeAppFromMiniplayer}
onStartDrag={handleStartDrag} onStartDrag={handleStartDrag}
onTogglePlay={handleTogglePlay} onTogglePlay={handleTogglePlay}
onSkipBack={handleSkipBack} onSkipBack={handleSkipBack}