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:
8 files changed
+167
-17
No files matched your search
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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; });
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user