Skip to content

Commit ef07ea5

Browse files
committed
fix(v2): mute global hotkeys while a game owns the keyboard
The g-prefix sequences and the / search shortcut listened during play, so pressing common game keys (g then h/p/c, or /) navigated away and killed the session. Gate the handler on the playing store flag. EmulatorJS set the flag on play but never cleared it on unmount, leaving pad and hotkey navigation dead after exiting a game. Ruffle never set it at all, so Flash sessions had no protection. Both players now flag the session on play and release it on unmount.
1 parent 549ed33 commit ef07ea5

3 files changed

Lines changed: 29 additions & 2 deletions

File tree

frontend/src/v2/composables/useGlobalHotkeys/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
// g c → collections index
1010
//
1111
// Two-key sequences (Gmail-style) have a 1.2s idle timeout. Everything is
12-
// guarded against input fields and contenteditable.
12+
// guarded against input fields, contenteditable, and a running game
13+
// (the playing store flag), so hotkeys never fire mid-session.
1314
import { onBeforeUnmount } from "vue";
1415
import { useRouter } from "vue-router";
1516
import { ROUTES } from "@/plugins/router";
17+
import storePlaying from "@/stores/playing";
1618

1719
let installed = false;
1820

@@ -35,13 +37,18 @@ export function useGlobalHotkeys() {
3537
installed = true;
3638

3739
const router = useRouter();
40+
const playingStore = storePlaying();
3841
let pendingPrefix: string | null = null;
3942
let pendingAt = 0;
4043
const PREFIX_IDLE_MS = 1200;
4144

4245
function onKey(e: KeyboardEvent) {
4346
if (e.metaKey || e.ctrlKey || e.altKey) return;
4447
if (isEditable(e.target)) return;
48+
// While a game is running the emulator owns the keyboard. "g" and
49+
// "h" are common game keys; without this gate a two-key sequence
50+
// (or "/") would navigate away and kill the session.
51+
if (playingStore.playing) return;
4552

4653
const now = performance.now();
4754
if (pendingPrefix && now - pendingAt > PREFIX_IDLE_MS) {

frontend/src/v2/views/Player/EmulatorJS.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,9 @@ onBeforeUnmount(() => {
473473
// the user never exited the game to the config screen first.
474474
stopActivityHeartbeat();
475475
emitActivityStop();
476+
// Hand the keyboard and gamepad back to the UI; the flag otherwise
477+
// stays true and pad/hotkey navigation is dead until a reload.
478+
playing.value = false;
476479
window.EJS_emulator?.callEvent("exit");
477480
removeIOSFullscreenShim.value?.();
478481
removeIOSFullscreenShim.value = null;

frontend/src/v2/views/Player/Ruffle.vue

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44
// `src/views/Player/RuffleRS/Base.vue` so playback stays identical; only the
55
// chrome is v2. No shared state with EJS — Flash has its own config.
66
import { RBtn, RCard, RIcon, RSwitch } from "@v2/lib";
7-
import { computed, nextTick, onMounted, ref, watch } from "vue";
7+
import {
8+
computed,
9+
nextTick,
10+
onBeforeUnmount,
11+
onMounted,
12+
ref,
13+
watch,
14+
} from "vue";
815
import { useI18n } from "vue-i18n";
916
import { useRoute, useRouter } from "vue-router";
1017
import { ROUTES } from "@/plugins/router";
1118
import romApi from "@/services/api/rom";
19+
import storePlaying from "@/stores/playing";
1220
import storeRoms, { type DetailedRom, type SimpleRom } from "@/stores/roms";
1321
import type { RuffleSourceAPI } from "@/types/ruffle";
1422
import { getDownloadPath } from "@/utils";
@@ -25,6 +33,7 @@ const { t } = useI18n();
2533
const route = useRoute();
2634
const router = useRouter();
2735
const { fullscreenOnPlay } = useFullscreenPref();
36+
const playingStore = storePlaying();
2837
2938
const rom = ref<DetailedRom | null>(null);
3039
const gameRunning = ref(false);
@@ -106,6 +115,9 @@ const platformLabel = computed(
106115
107116
function onPlay() {
108117
gameRunning.value = true;
118+
// Flash games are keyboard-driven; flag the session so global hotkeys
119+
// and pad-to-UI translation stay muted while the game owns input.
120+
playingStore.setPlaying(true);
109121
110122
nextTick(() => {
111123
if (!rom.value) return;
@@ -182,6 +194,11 @@ onMounted(async () => {
182194
};
183195
document.body.appendChild(script);
184196
});
197+
198+
onBeforeUnmount(() => {
199+
// Hand the keyboard and gamepad back to the UI on any exit path.
200+
playingStore.setPlaying(false);
201+
});
185202
</script>
186203

187204
<template>

0 commit comments

Comments
 (0)