Merge pull request #992 from HOSS1E/add-mute-loop-shortcuts

feat(studio): add M (mute) and Shift+L (loop) keyboard shortcuts
This commit is contained in:
Miguel Ángel
2026-05-21 00:02:32 +02:00
committed by GitHub
3 changed files with 72 additions and 0 deletions
@@ -15,6 +15,8 @@ const SHORTCUT_SECTIONS = [
{ key: "J", label: "Play backward" },
{ key: "K", label: "Stop" },
{ key: "L", label: "Play forward" },
{ key: "M", label: "Toggle mute" },
{ key: "⇧L", label: "Toggle loop" },
{ key: "←/→", label: "Step 1 frame" },
{ key: "⇧←/⇧→", label: "Step 10 frames" },
],
@@ -172,3 +172,58 @@ describe("usePlaybackKeyboard — keyboard layout independence (#834)", () => {
expect(spies.play).toHaveBeenCalledTimes(1);
});
});
describe("usePlaybackKeyboard — mute & loop shortcuts (#905)", () => {
it("M toggles audioMuted", () => {
const { dispatch } = setupHook();
expect(usePlayerStore.getState().audioMuted).toBe(false);
act(() => {
dispatch(keydown({ code: "KeyM", key: "m" }));
});
expect(usePlayerStore.getState().audioMuted).toBe(true);
act(() => {
dispatch(keydown({ code: "KeyM", key: "m" }));
});
expect(usePlayerStore.getState().audioMuted).toBe(false);
});
it("M does NOT toggle audioMuted above 1x playback (matches button gating)", () => {
const { dispatch } = setupHook();
usePlayerStore.setState({ playbackRate: 2, audioMuted: false });
act(() => {
dispatch(keydown({ code: "KeyM", key: "m" }));
});
expect(usePlayerStore.getState().audioMuted).toBe(false);
});
it("Shift+L toggles loopEnabled without starting forward shuttle", () => {
const { dispatch, spies } = setupHook();
expect(usePlayerStore.getState().loopEnabled).toBe(false);
act(() => {
dispatch(keydown({ code: "KeyL", key: "L", shiftKey: true }));
});
expect(usePlayerStore.getState().loopEnabled).toBe(true);
expect(spies.play).not.toHaveBeenCalled();
act(() => {
dispatch(keydown({ code: "KeyL", key: "L", shiftKey: true }));
});
expect(usePlayerStore.getState().loopEnabled).toBe(false);
});
it("Plain L still starts forward shuttle (regression guard)", () => {
const { dispatch, spies } = setupHook();
act(() => {
dispatch(keydown({ code: "KeyL", key: "l" }));
});
expect(spies.play).toHaveBeenCalledTimes(1);
expect(usePlayerStore.getState().loopEnabled).toBe(false);
});
});
@@ -108,6 +108,21 @@ export function usePlaybackKeyboard({
return;
}
if (e.repeat) return;
if (key === "m") {
e.preventDefault();
const state = usePlayerStore.getState();
// Audio is force-muted above 1x playback — match the mute button's gating.
if (state.playbackRate <= 1) {
state.setAudioMuted(!state.audioMuted);
}
return;
}
if (key === "l" && e.shiftKey) {
e.preventDefault();
const state = usePlayerStore.getState();
state.setLoopEnabled(!state.loopEnabled);
return;
}
if (key === "k") {
e.preventDefault();
pause();