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:
@@ -1,5 +1,7 @@
|
||||
/** The album detail sheet: cover, metadata, and a numbered track list. */
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import type { Album } from "../api/types";
|
||||
import { isBook, unitLabel } from "../lib/covers";
|
||||
import { clock } from "../lib/format";
|
||||
@@ -9,6 +11,8 @@ interface Props {
|
||||
album: Album;
|
||||
currentAlbumId: string | null;
|
||||
currentTrackIndex: number;
|
||||
/** The track Ctrl+j/k has highlighted, so the keyboard has something visible to move. */
|
||||
selectedIndex: number;
|
||||
onClose: () => void;
|
||||
onPlay: (trackIndex: number) => void;
|
||||
}
|
||||
@@ -17,13 +21,32 @@ export function AlbumModal({
|
||||
album,
|
||||
currentAlbumId,
|
||||
currentTrackIndex,
|
||||
selectedIndex,
|
||||
onClose,
|
||||
onPlay,
|
||||
}: Props) {
|
||||
const book = isBook(album);
|
||||
const sheet = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// Keep the Ctrl+j/k highlight on screen as it moves past the fold, the same way the
|
||||
// browse grid keeps its own keyboard selection visible.
|
||||
useEffect(() => {
|
||||
const box = sheet.current;
|
||||
const element = box?.querySelector<HTMLElement>(`[data-nav-index="${selectedIndex}"]`);
|
||||
if (!box || !element) return;
|
||||
const top = element.offsetTop - box.offsetTop;
|
||||
const bottom = top + element.offsetHeight;
|
||||
const pad = 12;
|
||||
if (top - pad < box.scrollTop) box.scrollTop = Math.max(0, top - pad);
|
||||
else if (bottom + pad > box.scrollTop + box.clientHeight) {
|
||||
box.scrollTop = bottom + pad - box.clientHeight;
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
return (
|
||||
<div className="overlay" style={{ zIndex: 4, background: "oklch(15% 0.03 210 / .6)" }} onClick={onClose}>
|
||||
<div
|
||||
ref={sheet}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
style={{
|
||||
background: "oklch(96% 0.012 210)",
|
||||
@@ -128,6 +151,7 @@ export function AlbumModal({
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
data-nav-index={index}
|
||||
onClick={() => onPlay(index)}
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -142,6 +166,8 @@ export function AlbumModal({
|
||||
background: current
|
||||
? "oklch(70% 0.16 340 / .16)"
|
||||
: "oklch(30% 0.03 210 / .05)",
|
||||
outline: index === selectedIndex ? "3px solid var(--accent)" : "none",
|
||||
outlineOffset: 2,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -3,16 +3,20 @@
|
||||
|
||||
const KEYS: Array<[caps: string[], label: string]> = [
|
||||
[["LEER"], "Play / Pause"],
|
||||
[["CTRL+H", "CTRL+L"], "Song zurück / vor"],
|
||||
[["CTRL+J", "CTRL+K"], "Leiser / lauter"],
|
||||
[["SHIFT+H", "SHIFT+L"], "Song zurück / vor"],
|
||||
[["SHIFT+J", "SHIFT+K"], "Leiser / lauter"],
|
||||
[["SHIFT+M"], "Stumm"],
|
||||
[["⇧←", "⇧→"], "15 Sekunden zurück / vor"],
|
||||
[["← ↑ ↓ →"], "Auswahl bewegen"],
|
||||
[["CTRL+H/J/K/L"], "Auswahl bewegen (Raster oder Titelliste)"],
|
||||
[["CTRL+D", "CTRL+U"], "Eine halbe Seite weiter / zurück"],
|
||||
[["A-Z"], "Album oder Hörbuch suchen"],
|
||||
[["?"], "Einzelne Titel suchen"],
|
||||
[["F1"], "Diese Hilfe"],
|
||||
[["TAB"], "Musik / Hörbücher / Podcasts"],
|
||||
[["A", "0-9"], "Aktuellen Titel einer Fernbedienungs-Taste zuweisen"],
|
||||
[["← ↑ ↓ →"], "Auswahl bewegen"],
|
||||
[["ENTER"], "Auswahl abspielen"],
|
||||
[["⇧ENTER"], "Titelliste öffnen"],
|
||||
[["A", "0-9"], "Aktuellen Titel einer Fernbedienungs-Taste zuweisen"],
|
||||
[["F1"], "Diese Hilfe"],
|
||||
[["ESC"], "Schließen / Suche löschen"],
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user