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",
"navigation": "Navigation",
"ui": "Oberfläche",
"focus": "Immersiv"
"focus": "Immersiv",
"mini": "Mini-Player"
},
"actions": {
"playbackToggle": "Wiedergabe / Pause",
@@ -1417,10 +1418,16 @@
"uiEscape": "Schließen / Verwerfen",
"uiShowShortcuts": "Tastenkürzel anzeigen",
"uiOpenLink": "Musik-Link öffnen",
"uiMiniPlayer": "Mini-Player umschalten",
"focusSeekForward": "Vorspulen (5s)",
"focusSeekBack": "Zurückspulen (5s)",
"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": {
"playbackToggle": "Wiedergabe umschalten",
@@ -1435,10 +1442,16 @@
"uiEscape": "Aktives Panel oder Modal schließen",
"uiShowShortcuts": "Tastenkürzel-Modal anzeigen",
"uiOpenLink": "Einen Qobuz-Link in der App öffnen",
"uiMiniPlayer": "Mini-Player-Modus umschalten",
"focusSeekForward": "5 Sekunden vorspulen",
"focusSeekBack": "5 Sekunden zurückspulen",
"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": {
+16 -3
View File
@@ -1464,7 +1464,8 @@
"playback": "Playback",
"navigation": "Navigation",
"ui": "Interface",
"focus": "Immersive"
"focus": "Immersive",
"mini": "Mini Player"
},
"actions": {
"playbackToggle": "Play / Pause",
@@ -1479,10 +1480,16 @@
"uiEscape": "Close / Dismiss",
"uiShowShortcuts": "Show Shortcuts",
"uiOpenLink": "Open Music Link",
"uiMiniPlayer": "Toggle Mini Player",
"focusSeekForward": "Seek Forward (5s)",
"focusSeekBack": "Seek Back (5s)",
"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": {
"playbackToggle": "Toggle playback",
@@ -1497,10 +1504,16 @@
"uiEscape": "Close active panel or modal",
"uiShowShortcuts": "Show keyboard shortcuts modal",
"uiOpenLink": "Open a Qobuz link in the app",
"uiMiniPlayer": "Toggle mini player mode",
"focusSeekForward": "Seek forward 5 seconds",
"focusSeekBack": "Seek back 5 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": {
+16 -3
View File
@@ -1464,7 +1464,8 @@
"playback": "Reproducción",
"navigation": "Navegación",
"ui": "Interfaz",
"focus": "Inmersivo"
"focus": "Inmersivo",
"mini": "Mini Reproductor"
},
"actions": {
"playbackToggle": "Reproducir / Pausar",
@@ -1479,10 +1480,16 @@
"uiEscape": "Cerrar / Descartar",
"uiShowShortcuts": "Mostrar Atajos",
"uiOpenLink": "Abrir Enlace Musical",
"uiMiniPlayer": "Alternar Mini Reproductor",
"focusSeekForward": "Avanzar (5s)",
"focusSeekBack": "Retroceder (5s)",
"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": {
"playbackToggle": "Alternar reproducción",
@@ -1497,10 +1504,16 @@
"uiEscape": "Cerrar panel activo o modal",
"uiShowShortcuts": "Mostrar modal de atajos de teclado",
"uiOpenLink": "Abrir un enlace de Qobuz en la app",
"uiMiniPlayer": "Alternar modo mini reproductor",
"focusSeekForward": "Avanzar 5 segundos",
"focusSeekBack": "Retroceder 5 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": {
+16 -3
View File
@@ -1402,7 +1402,8 @@
"playback": "Lecture",
"navigation": "Navigation",
"ui": "Interface",
"focus": "Immersif"
"focus": "Immersif",
"mini": "Mini Lecteur"
},
"actions": {
"playbackToggle": "Lecture / Pause",
@@ -1417,10 +1418,16 @@
"uiEscape": "Fermer / Annuler",
"uiShowShortcuts": "Afficher les raccourcis",
"uiOpenLink": "Ouvrir un lien musical",
"uiMiniPlayer": "Basculer le mini lecteur",
"focusSeekForward": "Avancer (5s)",
"focusSeekBack": "Reculer (5s)",
"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": {
"playbackToggle": "Basculer la lecture",
@@ -1435,10 +1442,16 @@
"uiEscape": "Fermer le panneau actif ou la modale",
"uiShowShortcuts": "Afficher la modale des raccourcis clavier",
"uiOpenLink": "Ouvrir un lien Qobuz dans l'application",
"uiMiniPlayer": "Basculer le mode mini lecteur",
"focusSeekForward": "Avancer de 5 secondes",
"focusSeekBack": "Reculer de 5 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": {
+18
View File
@@ -194,3 +194,21 @@ export async function setMiniplayerAlwaysOnTop(alwaysOnTop: boolean): Promise<Al
export async function closeMiniplayerWindow(): Promise<void> {
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
// ============================================================================
export type KeybindingCategory = 'playback' | 'navigation' | 'ui' | 'focus';
export type KeybindingCategory = 'playback' | 'navigation' | 'ui' | 'focus' | 'mini';
export interface KeybindingAction {
/** Unique identifier, e.g., 'playback.toggle' */
@@ -53,6 +53,7 @@ export const CATEGORIES: CategoryInfo[] = [
{ id: 'navigation', label: 'Navigation', description: 'App navigation' },
{ id: 'ui', label: 'Interface', description: 'UI elements' },
{ id: 'focus', label: 'Immersive', description: 'Immersive mode controls' },
{ id: 'mini', label: 'Mini Player', description: 'Mini player controls' },
];
export const ACTIONS: KeybindingAction[] = [
@@ -145,6 +146,13 @@ export const ACTIONS: KeybindingAction[] = [
defaultShortcut: 'Ctrl+l',
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
{
@@ -179,6 +187,48 @@ export const ACTIONS: KeybindingAction[] = [
description: 'Seek back 10 seconds',
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 */
@@ -412,7 +462,7 @@ export function getConflictingAction(
*/
export function handleKeydown(
event: KeyboardEvent,
context: { focusMode?: boolean } = {}
context: { focusMode?: boolean; miniPlayerMode?: boolean } = {}
): boolean {
// Ignore if we're in an input
if (isInputTarget(event)) {
@@ -431,6 +481,9 @@ export function handleKeydown(
if (action.category === 'focus' && !context.focusMode) {
return false;
}
if (action.category === 'mini' && !context.miniPlayerMode) {
return false;
}
}
// Execute callback
+1
View File
@@ -3015,6 +3015,7 @@
});
registerAction('ui.sidebar', toggleSidebar);
registerAction('ui.focusMode', toggleFocusMode);
registerAction('ui.miniPlayer', () => { void enterMiniplayerMode(); });
registerAction('ui.queue', toggleQueue);
registerAction('ui.escape', handleUIEscape);
registerAction('ui.showShortcuts', () => { isShortcutsModalOpen = true; });
+29 -3
View File
@@ -48,7 +48,12 @@
setMiniPlayerOpen,
setMiniPlayerSurface
} 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';
let playerState = $state<PlayerState>(getPlayerState());
@@ -60,6 +65,7 @@
let unlistenMoved: (() => void) | null = null;
let unlistenResized: (() => void) | null = null;
let unlistenCloseRequested: (() => void) | null = null;
let removeKeydownListener: (() => void) | null = null;
let unsubscribePlayer: (() => void) | null = null;
let unsubscribeQueue: (() => void) | null = null;
@@ -219,9 +225,27 @@
unlistenCloseRequested = await appWindow.onCloseRequested((event) => {
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');
});
@@ -233,7 +257,9 @@
unlistenMoved?.();
unlistenResized?.();
unlistenCloseRequested?.();
removeKeydownListener?.();
unregisterAll();
stopActiveLineUpdates();
stopWatching();
stopQueueEventListener();
@@ -433,7 +459,7 @@
}}
onTogglePin={handleToggleAlwaysOnTop}
onExpand={exitMiniplayerMode}
onClose={exitMiniplayerMode}
onClose={closeAppFromMiniplayer}
onStartDrag={handleStartDrag}
onTogglePlay={handleTogglePlay}
onSkipBack={handleSkipBack}