Add a vim-style keyboard layer: Ctrl navigates, Shift controls transport
Ctrl+h/j/k/l move the highlight in whatever list is on screen - the browse grid, search results, or an open album's track list - the same job the arrow keys already do, just reachable without leaving the home row. Ctrl+d/u add vim's own half-page jump. Shift+h/j/k/l/m are transport (previous/next, volume, mute) from anywhere, including the room page, matching the muscle memory of other vim-ish media apps; every other Shift+letter still reaches search untouched; only the shifted letter form of those five keys is intercepted. Shift+Enter opens an album's track list (the keyboard equivalent of clicking its cover, which had no key of its own until now) instead of playing it outright; Ctrl+j/k then move a visible highlight through that list, and Enter plays whichever track is highlighted. Falls back to plain ENTER's behaviour for a category tile or a podcast episode, neither of which has a track list to show. Mute (Shift+M) is new - nothing was bound to it before. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -239,6 +239,9 @@ export function App() {
|
||||
case "pop":
|
||||
playPop(action.freq);
|
||||
break;
|
||||
case "mute":
|
||||
setVolume(state && state.volume > 0 ? 0 : UNMUTE_PERCENT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -246,8 +249,8 @@ export function App() {
|
||||
);
|
||||
|
||||
// Held in a ref so the listener is installed once rather than on every state change.
|
||||
const latest = useRef({ ui, results, run });
|
||||
latest.current = { ui, results, run };
|
||||
const latest = useRef({ ui, results, run, openAlbumTrackCount: 0 });
|
||||
latest.current = { ui, results, run, openAlbumTrackCount: openAlbum?.tracks.length ?? 0 };
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -256,7 +259,7 @@ export function App() {
|
||||
if (target && /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName)) return;
|
||||
|
||||
const current = latest.current;
|
||||
const actions = handleKey(event, current.ui, current.results);
|
||||
const actions = handleKey(event, current.ui, current.results, current.openAlbumTrackCount);
|
||||
if (!actions.length) return;
|
||||
event.preventDefault();
|
||||
current.run(actions);
|
||||
@@ -300,7 +303,7 @@ export function App() {
|
||||
return;
|
||||
}
|
||||
playPop(420);
|
||||
setUi((previous) => ({ ...previous, openAlbumId: album.id, selIndex: navIndex }));
|
||||
setUi((previous) => ({ ...previous, openAlbumId: album.id, selIndex: navIndex, modalTrackIndex: 0 }));
|
||||
};
|
||||
|
||||
/** Clicking the title text (as opposed to the cover) starts the album right away -
|
||||
@@ -313,7 +316,13 @@ export function App() {
|
||||
const onOpenCurrentAlbum = () => {
|
||||
if (!currentAlbum) return;
|
||||
playPop(420);
|
||||
setUi((previous) => ({ ...previous, openAlbumId: currentAlbum.id }));
|
||||
// currentAlbum is by definition whatever's playing, so its own track_index is the
|
||||
// track already highlighted - the modal opens showing where you actually are.
|
||||
setUi((previous) => ({
|
||||
...previous,
|
||||
openAlbumId: currentAlbum.id,
|
||||
modalTrackIndex: state?.track_index ?? 0,
|
||||
}));
|
||||
};
|
||||
|
||||
const onPlaySong = (hit: SongHit) => play(hit.album.id, hit.index);
|
||||
@@ -435,6 +444,7 @@ export function App() {
|
||||
album={openAlbum}
|
||||
currentAlbumId={state.album_id}
|
||||
currentTrackIndex={state.track_index}
|
||||
selectedIndex={ui.modalTrackIndex}
|
||||
onClose={() => setUi((previous) => ({ ...previous, openAlbumId: null }))}
|
||||
onPlay={(trackIndex) => play(openAlbum.id, trackIndex)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user