refactor(shortcuts): split shortcutActions.ts into focused modules (#683)

shortcutActions.ts becomes a barrel; the subsystem splits into:

- shortcutTypes.ts            shared types
- shortcutActionRegistry.ts   SHORTCUT_ACTION_REGISTRY + action id types
- shortcutDispatch.ts         runtime + CLI command dispatch
- shortcutBindings.ts         derived in-app / global binding tables

Pure code-move, no behaviour change. All call sites import from
config/shortcutActions unchanged.
This commit is contained in:
Frank Stellmacher
2026-05-14 12:51:10 +02:00
committed by GitHub
parent 1dd74f0fa1
commit 7e99f030a7
5 changed files with 573 additions and 519 deletions
+378
View File
@@ -0,0 +1,378 @@
import { star, unstar } from '../api/subsonicStarRating';
import { getSong } from '../api/subsonicLibrary';
import { songToTrack } from '../utils/songToTrack';
import { invoke } from '@tauri-apps/api/core';
import { getCurrentWindow } from '@tauri-apps/api/window';
import i18n from '../i18n';
import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore';
import { useLyricsStore } from '../store/lyricsStore';
import { showToast } from '../utils/toast';
import type { ActionContext, ShortcutSlot, ShortcutActionMeta } from './shortcutTypes';
let cliPremuteVolume: number | null = null;
const withPreviewPolicy = (
action: 'play' | 'pause' | 'stop' | 'play-pause' | 'next' | 'prev',
options: ActionContext,
fn: () => void
) => {
const previewing = usePreviewStore.getState().previewingId !== null;
if (previewing && options.previewPolicy === 'ignore') return;
if (previewing && options.previewPolicy === 'stop') {
usePreviewStore.getState().stopPreview();
}
fn();
};
function focusLiveSearchInput(): boolean {
const input = document.getElementById('live-search-input') as HTMLInputElement | null;
if (!input) return false;
input.focus();
input.select();
return true;
}
export const SHORTCUT_ACTION_REGISTRY = {
'play': {
getLabel: t => t('settings.shortcutPlayPause'),
runInMiniWindow: false,
run: ({ previewPolicy }) => withPreviewPolicy('play', { navigate: () => {}, previewPolicy }, () => {
const state = usePlayerStore.getState();
if (!state.isPlaying) state.resume();
}),
cli: { verb: 'play', description: 'play' },
},
'pause': {
getLabel: t => t('settings.shortcutPlayPause'),
runInMiniWindow: false,
run: ({ previewPolicy }) => withPreviewPolicy('pause', { navigate: () => {}, previewPolicy }, () => {
const state = usePlayerStore.getState();
if (state.isPlaying) state.pause();
}),
cli: { verb: 'pause', description: 'pause' },
},
'stop': {
getLabel: t => t('settings.shortcutPlayPause'),
runInMiniWindow: false,
run: ({ previewPolicy }) => withPreviewPolicy('stop', { navigate: () => {}, previewPolicy }, () => {
usePlayerStore.getState().stop();
}),
cli: { verb: 'stop', description: 'stop' },
},
'play-pause': {
getLabel: t => t('settings.shortcutPlayPause'),
inApp: { defaultBinding: 'Space' },
global: { defaultBinding: null },
runInMiniWindow: true,
run: ({ previewPolicy }) => withPreviewPolicy('play-pause', { navigate: () => {}, previewPolicy }, () => {
usePlayerStore.getState().togglePlay();
}),
},
next: {
getLabel: t => t('settings.shortcutNext'),
inApp: { defaultBinding: null },
global: { defaultBinding: null },
runInMiniWindow: true,
run: ({ previewPolicy }) => withPreviewPolicy('next', { navigate: () => {}, previewPolicy }, () => {
usePlayerStore.getState().next();
}),
cli: { verb: 'next', description: 'next track' },
},
prev: {
getLabel: t => t('settings.shortcutPrev'),
inApp: { defaultBinding: null },
global: { defaultBinding: null },
runInMiniWindow: true,
run: ({ previewPolicy }) => withPreviewPolicy('prev', { navigate: () => {}, previewPolicy }, () => {
usePlayerStore.getState().previous();
}),
cli: { verb: 'prev', description: 'previous track' },
},
'volume-up': {
getLabel: t => t('settings.shortcutVolumeUp'),
inApp: { defaultBinding: null },
global: { defaultBinding: null },
runInMiniWindow: false,
run: () => {
const state = usePlayerStore.getState();
state.setVolume(Math.min(1, state.volume + 0.05));
},
},
'volume-down': {
getLabel: t => t('settings.shortcutVolumeDown'),
inApp: { defaultBinding: null },
global: { defaultBinding: null },
runInMiniWindow: false,
run: () => {
const state = usePlayerStore.getState();
state.setVolume(Math.max(0, state.volume - 0.05));
},
},
'seek-forward': {
getLabel: t => t('settings.shortcutSeekForward'),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: () => {
const state = usePlayerStore.getState();
const duration = state.currentTrack?.duration ?? 0;
if (!duration) return;
state.seek(Math.min(1, (state.currentTime + 10) / duration));
},
},
'seek-backward': {
getLabel: t => t('settings.shortcutSeekBackward'),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: () => {
const state = usePlayerStore.getState();
const duration = state.currentTrack?.duration ?? 0;
if (!duration) return;
state.seek(Math.max(0, (state.currentTime - 10) / duration));
},
},
'toggle-queue': {
getLabel: t => t('settings.shortcutToggleQueue'),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: () => {
usePlayerStore.getState().toggleQueue();
},
},
'open-folder-browser': {
getLabel: t => t('settings.shortcutOpenFolderBrowser', { folderBrowser: t('sidebar.folderBrowser') }),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: ({ navigate }) => {
navigate('/folders', { state: { folderBrowserRevealTs: Date.now() } });
},
},
'fullscreen-player': {
getLabel: t => t('settings.shortcutFullscreenPlayer'),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: () => {
usePlayerStore.getState().toggleFullscreen();
},
},
'native-fullscreen': {
getLabel: t => t('settings.shortcutNativeFullscreen'),
inApp: { defaultBinding: 'F11' },
runInMiniWindow: false,
run: () => {
const win = getCurrentWindow();
win.isFullscreen().then(fs => win.setFullscreen(!fs));
},
},
'open-mini-player': {
getLabel: t => t('settings.shortcutOpenMiniPlayer'),
inApp: { defaultBinding: null },
runInMiniWindow: true,
run: () => {
invoke('open_mini_player').catch(() => {});
},
},
'start-search': {
getLabel: t => t('settings.shortcutStartSearch', { defaultValue: 'Start a search' }),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: ({ navigate }) => {
if (focusLiveSearchInput()) return;
navigate('/');
requestAnimationFrame(() => {
window.setTimeout(() => { focusLiveSearchInput(); }, 80);
});
},
},
'start-advanced-search': {
getLabel: t => t('settings.shortcutStartAdvancedSearch', { defaultValue: 'Start an advanced search' }),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: ({ navigate }) => {
navigate('/search/advanced');
},
},
'toggle-sidebar': {
getLabel: t => t('settings.shortcutToggleSidebar', { defaultValue: 'Toggle sidebar' }),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: () => {
window.dispatchEvent(new Event('psy:toggle-sidebar'));
},
},
'mute-sound': {
getLabel: t => t('settings.shortcutMuteSound', { defaultValue: 'Mute sound' }),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: () => {
const state = usePlayerStore.getState();
if (state.volume <= 0) {
const restore = cliPremuteVolume ?? 0.8;
cliPremuteVolume = null;
state.setVolume(restore);
return;
}
cliPremuteVolume = state.volume;
state.setVolume(0);
},
},
'toggle-equalizer': {
getLabel: t => t('settings.shortcutToggleEqualizer', { defaultValue: 'Open / Toggle Equalizer' }),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: () => {
window.dispatchEvent(new Event('psy:toggle-equalizer'));
},
},
'toggle-repeat': {
getLabel: t => t('settings.shortcutToggleRepeat', { defaultValue: 'Toggle repeat' }),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: () => {
usePlayerStore.getState().toggleRepeat();
},
},
'open-now-playing': {
getLabel: t => t('settings.shortcutOpenNowPlaying', { defaultValue: 'Open "Now Playing"' }),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: ({ navigate }) => {
navigate('/now-playing');
},
},
'show-lyrics': {
getLabel: t => t('settings.shortcutShowLyrics', { defaultValue: 'Show lyrics' }),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: () => {
const player = usePlayerStore.getState();
player.setQueueVisible(true);
useLyricsStore.getState().showLyrics();
},
},
'favorite-current-track': {
getLabel: t => t('settings.shortcutFavoriteCurrentTrack', { defaultValue: 'Add current track to favorites' }),
inApp: { defaultBinding: null },
runInMiniWindow: false,
run: () => {
const track = usePlayerStore.getState().currentTrack;
if (!track) {
showToast(i18n.t('contextMenu.cliMixNeedsTrack', { defaultValue: 'Load a track first.' }), 5000, 'error');
return;
}
star(track.id, 'song')
.then(() => usePlayerStore.getState().setStarredOverride(track.id, true))
.catch(err => {
console.error('Favorite current track failed', err);
showToast(i18n.t('contextMenu.cliStarFailed', { defaultValue: 'Could not add the track to favorites.' }), 5000, 'error');
});
},
},
'open-help': {
getLabel: t => t('settings.shortcutOpenHelp', { defaultValue: 'Help' }),
inApp: { defaultBinding: 'F1', hidden: true },
runInMiniWindow: false,
run: ({ navigate }) => {
navigate('/help');
},
},
'shuffle': {
getLabel: t => t('settings.shortcutNext'),
runInMiniWindow: false,
run: () => {
usePlayerStore.getState().shuffleQueue();
},
cli: { verb: 'shuffle', description: 'shuffle' },
},
'mute': {
getLabel: t => t('settings.shortcutVolumeDown'),
runInMiniWindow: false,
run: () => {
const state = usePlayerStore.getState();
if (state.volume > 0) cliPremuteVolume = state.volume;
state.setVolume(0);
},
cli: { verb: 'mute', description: 'mute' },
},
'unmute': {
getLabel: t => t('settings.shortcutVolumeUp'),
runInMiniWindow: false,
run: () => {
const restore = cliPremuteVolume ?? 0.8;
cliPremuteVolume = null;
usePlayerStore.getState().setVolume(restore);
},
cli: { verb: 'unmute', description: 'unmute' },
},
'star': {
getLabel: t => t('settings.shortcutPlayPause'),
runInMiniWindow: false,
run: () => {
const track = usePlayerStore.getState().currentTrack;
if (!track) {
showToast(i18n.t('contextMenu.cliMixNeedsTrack'), 5000, 'error');
return;
}
star(track.id, 'song')
.then(() => usePlayerStore.getState().setStarredOverride(track.id, true))
.catch(err => {
console.error('CLI star failed', err);
showToast(i18n.t('contextMenu.cliStarFailed', { defaultValue: 'Star/unstar failed.' }), 5000, 'error');
});
},
cli: { verb: 'star', description: 'star' },
},
'unstar': {
getLabel: t => t('settings.shortcutPlayPause'),
runInMiniWindow: false,
run: () => {
const track = usePlayerStore.getState().currentTrack;
if (!track) {
showToast(i18n.t('contextMenu.cliMixNeedsTrack'), 5000, 'error');
return;
}
unstar(track.id, 'song')
.then(() => usePlayerStore.getState().setStarredOverride(track.id, false))
.catch(err => {
console.error('CLI star failed', err);
showToast(i18n.t('contextMenu.cliStarFailed', { defaultValue: 'Star/unstar failed.' }), 5000, 'error');
});
},
cli: { verb: 'unstar', description: 'unstar' },
},
'reload': {
getLabel: t => t('settings.shortcutPlayPause'),
runInMiniWindow: false,
run: () => {
const store = usePlayerStore.getState();
const { currentTrack, queue, stop, resetAudioPause, playTrack, initializeFromServerQueue } = store;
stop();
resetAudioPause();
invoke('audio_stop')
.catch(() => {})
.then(async () => {
if (currentTrack) {
try {
const fresh = await getSong(currentTrack.id);
const t = fresh ? songToTrack(fresh) : currentTrack;
playTrack(t, queue, true);
} catch {
playTrack(currentTrack, queue, true);
}
} else {
await initializeFromServerQueue();
}
});
},
cli: { verb: 'reload', description: 'reload' },
},
} as const satisfies Record<string, ShortcutActionMeta>;
export type ShortcutAction = keyof typeof SHORTCUT_ACTION_REGISTRY;
export type KeyAction = {
[Action in ShortcutAction]: (typeof SHORTCUT_ACTION_REGISTRY)[Action] extends { inApp: ShortcutSlot } ? Action : never
}[ShortcutAction];
export type GlobalAction = {
[Action in ShortcutAction]: (typeof SHORTCUT_ACTION_REGISTRY)[Action] extends { global: ShortcutSlot } ? Action : never
}[ShortcutAction];