feat(mini-player): user-bindable keyboard shortcut to toggle mini player

Adds an 'open-mini-player' entry to the in-app keybindings (Settings →
Shortcuts), default unbound. Pressing the chord from the main window
opens the mini and minimises main; pressing it from the mini hides the
mini and restores main — both paths invoke open_mini_player which
already handles the toggle.

The mini window has its own keydown listener (same pattern as Space /
arrows for play/skip) that reads the binding from useKeybindingsStore.
Binding changes in main propagate to an open mini through the existing
storage-event sync (now also covers psysonic_keybindings) so the user
doesn't need to restart the mini after rebinding.

Localized in all 8 supported locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-19 13:05:06 +02:00
parent 8cc115cade
commit 60da17f7cc
12 changed files with 27 additions and 1 deletions
+4
View File
@@ -812,6 +812,9 @@ function TauriEventBridge() {
win.isFullscreen().then(fs => win.setFullscreen(!fs));
break;
}
case 'open-mini-player':
invoke('open_mini_player').catch(() => {});
break;
}
};
window.addEventListener('keydown', onKey);
@@ -983,6 +986,7 @@ export default function App() {
if (!e.key) return;
if (e.key === 'psysonic_theme') useThemeStore.persist.rehydrate();
else if (e.key === 'psysonic_font') useFontStore.persist.rehydrate();
else if (e.key === 'psysonic_keybindings') useKeybindingsStore.persist.rehydrate();
else if (e.key === 'psysonic_language' && e.newValue) {
import('./i18n').then(m => m.default.changeLanguage(e.newValue!));
}
+11
View File
@@ -5,6 +5,7 @@ import { Play, Pause, SkipBack, SkipForward, Pin, PinOff, Maximize2, X, ListMusi
import CachedImage from './CachedImage';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { useKeybindingsStore, matchInAppBinding } from '../store/keybindingsStore';
import { useDragDrop } from '../contexts/DragDropContext';
import MiniContextMenu from './MiniContextMenu';
import type { MiniSyncPayload, MiniControlAction, MiniTrackInfo } from '../utils/miniPlayerBridge';
@@ -178,11 +179,21 @@ export default function MiniPlayer() {
}, [alwaysOnTop]);
// Keyboard: Space → toggle, ← / → → prev / next. Ignore when typing.
// Also honour the user-configured 'open-mini-player' shortcut so the
// same chord that opens the mini from main also closes it from here.
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
const tgt = e.target as HTMLElement | null;
const tag = tgt?.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || tgt?.isContentEditable) return;
const openMiniBinding = useKeybindingsStore.getState().bindings['open-mini-player'];
if (matchInAppBinding(e, openMiniBinding)) {
e.preventDefault();
invoke('open_mini_player').catch(() => {});
return;
}
if (e.key === ' ' || e.code === 'Space') {
e.preventDefault();
emit('mini:control', 'toggle').catch(() => {});
+1
View File
@@ -642,6 +642,7 @@ export const deTranslation = {
shortcutOpenFolderBrowser: '{{folderBrowser}} öffnen',
shortcutFullscreenPlayer: 'Vollbild-Player',
shortcutNativeFullscreen: 'Nativer Vollbildmodus',
shortcutOpenMiniPlayer: 'Mini-Player öffnen',
tabSystem: 'System',
tabGeneral: 'Allgemein',
ratingsSectionTitle: 'Bewertungen',
+1
View File
@@ -667,6 +667,7 @@ export const enTranslation = {
shortcutOpenFolderBrowser: 'Open {{folderBrowser}}',
shortcutFullscreenPlayer: 'Fullscreen player',
shortcutNativeFullscreen: 'Native fullscreen',
shortcutOpenMiniPlayer: 'Open mini player',
playbackTitle: 'Playback',
replayGain: 'Replay Gain',
replayGainDesc: 'Normalize track volume using ReplayGain metadata',
+1
View File
@@ -658,6 +658,7 @@ export const esTranslation = {
shortcutOpenFolderBrowser: 'Abrir {{folderBrowser}}',
shortcutFullscreenPlayer: 'Reproductor pantalla completa',
shortcutNativeFullscreen: 'Pantalla completa nativa',
shortcutOpenMiniPlayer: 'Abrir mini reproductor',
playbackTitle: 'Reproducción',
replayGain: 'Replay Gain',
replayGainDesc: 'Normalizar volumen de pistas usando metadatos ReplayGain',
+1
View File
@@ -630,6 +630,7 @@ export const frTranslation = {
shortcutOpenFolderBrowser: 'Ouvrir {{folderBrowser}}',
shortcutFullscreenPlayer: 'Lecteur plein écran',
shortcutNativeFullscreen: 'Plein écran natif',
shortcutOpenMiniPlayer: 'Ouvrir le mini-lecteur',
tabSystem: 'Système',
tabGeneral: 'Général',
ratingsSectionTitle: 'Notes',
+1
View File
@@ -652,6 +652,7 @@ export const nbTranslation = {
shortcutOpenFolderBrowser: 'Åpne {{folderBrowser}}',
shortcutFullscreenPlayer: 'Fullskjermsspiller',
shortcutNativeFullscreen: 'Naturlig fullskjerm',
shortcutOpenMiniPlayer: 'Åpne minispiller',
playbackTitle: 'Avspilling',
replayGain: 'Replay Gain',
replayGainDesc: 'Normaliser sporvolumet ved hjelp av ReplayGain-metadata',
+1
View File
@@ -629,6 +629,7 @@ export const nlTranslation = {
shortcutOpenFolderBrowser: '{{folderBrowser}} openen',
shortcutFullscreenPlayer: 'Volledigschermspeler',
shortcutNativeFullscreen: 'Systeemvolledig scherm',
shortcutOpenMiniPlayer: 'Mini-speler openen',
tabSystem: 'Systeem',
tabGeneral: 'Algemeen',
ratingsSectionTitle: 'Beoordelingen',
+1
View File
@@ -682,6 +682,7 @@ export const ruTranslation = {
shortcutOpenFolderBrowser: 'Открыть {{folderBrowser}}',
shortcutFullscreenPlayer: 'Полноэкранный плеер',
shortcutNativeFullscreen: 'Системный полный экран',
shortcutOpenMiniPlayer: 'Открыть мини-плеер',
playbackTitle: 'Воспроизведение',
replayGain: 'Replay Gain',
replayGainDesc: 'Выравнивание громкости по метаданным ReplayGain',
+1
View File
@@ -648,6 +648,7 @@ export const zhTranslation = {
shortcutOpenFolderBrowser: '打开{{folderBrowser}}',
shortcutFullscreenPlayer: '全屏播放器',
shortcutNativeFullscreen: '原生全屏',
shortcutOpenMiniPlayer: '打开迷你播放器',
playbackTitle: '播放',
replayGain: '回放增益',
replayGainDesc: '使用 ReplayGain 元数据标准化曲目音量',
+1
View File
@@ -2010,6 +2010,7 @@ export default function Settings() {
['open-folder-browser', t('settings.shortcutOpenFolderBrowser', { folderBrowser: t('sidebar.folderBrowser') })],
['fullscreen-player', t('settings.shortcutFullscreenPlayer')],
['native-fullscreen', t('settings.shortcutNativeFullscreen')],
['open-mini-player', t('settings.shortcutOpenMiniPlayer')],
] as [KeyAction, string][]).map(([action, label]) => {
const bound = kb.bindings[action];
const isListening = listeningFor === action;
+3 -1
View File
@@ -12,7 +12,8 @@ export type KeyAction =
| 'toggle-queue'
| 'open-folder-browser'
| 'fullscreen-player'
| 'native-fullscreen';
| 'native-fullscreen'
| 'open-mini-player';
/** Physical keys only — ignore for binding capture */
export const MODIFIER_KEY_CODES = [
@@ -35,6 +36,7 @@ export const DEFAULT_BINDINGS: Bindings = {
'open-folder-browser': null,
'fullscreen-player': null,
'native-fullscreen': 'F11',
'open-mini-player': null,
};
interface KeybindingsState {