Skip to content

Commit 37bddd4

Browse files
authored
Merge pull request #3431 from rommapp/copilot/fix-fullscreen-emulation-ios
Add iOS pseudo-fullscreen shim for EmulatorJS player
2 parents d4bc241 + 31e6d99 commit 37bddd4

2 files changed

Lines changed: 120 additions & 0 deletions

File tree

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type { Events } from "@/types/emitter";
2020
import { getSupportedEJSCores } from "@/utils";
2121
import CacheDialog from "@/views/Player/EmulatorJS/CacheDialog.vue";
2222
import Player from "@/views/Player/EmulatorJS/Player.vue";
23+
import { installIOSFullscreenShim } from "./utils";
2324
2425
const { t } = useI18n();
2526
const { xs, mdAndUp, smAndDown } = useDisplay();
@@ -38,6 +39,7 @@ const selectedDisc = ref<number | null>(null);
3839
const selectedCore = ref<string | null>(null);
3940
const selectedFirmware = ref<FirmwareSchema | null>(null);
4041
const supportedCores = ref<string[]>([]);
42+
const removeIOSFullscreenShim = ref<(() => void) | null>(null);
4143
const gameRunning = ref(false);
4244
const fullScreenOnPlay = useLocalStorage("emulation.fullScreenOnPlay", true);
4345
@@ -58,6 +60,9 @@ const compatibleStates = computed(
5860
);
5961
6062
async function onPlay() {
63+
removeIOSFullscreenShim.value?.();
64+
removeIOSFullscreenShim.value = installIOSFullscreenShim();
65+
6166
if (rom.value && auth.scopes.includes("roms.user.write")) {
6267
romApi.updateUserRomProps({
6368
romId: rom.value.id,
@@ -102,6 +107,8 @@ async function onPlay() {
102107
playing.value = true;
103108
fullScreen.value = fullScreenOnPlay.value;
104109
} catch (err) {
110+
removeIOSFullscreenShim.value?.();
111+
removeIOSFullscreenShim.value = null;
105112
console.error("[Play] Emulator load failure:", err);
106113
}
107114
}
@@ -261,6 +268,8 @@ onMounted(async () => {
261268
262269
onBeforeUnmount(async () => {
263270
window.EJS_emulator?.callEvent("exit");
271+
removeIOSFullscreenShim.value?.();
272+
removeIOSFullscreenShim.value = null;
264273
emitter?.off("saveSelected", selectSave);
265274
emitter?.off("stateSelected", selectState);
266275
});

frontend/src/views/Player/EmulatorJS/utils.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Bowser from "bowser";
12
import { type SaveSchema } from "@/__generated__";
23
import { type StateSchema } from "@/__generated__";
34
import saveApi from "@/services/api/save";
@@ -145,6 +146,116 @@ export function loadEmulatorJSState(state: Uint8Array) {
145146
window.EJS_emulator.gameManager.loadState(state);
146147
}
147148

149+
const IOS_FULLSCREEN_NAV_SELECTOR =
150+
".v-app-bar, .v-bottom-navigation, .v-navigation-drawer";
151+
const IOS_FULLSCREEN_STYLE = `
152+
[data-ios-fullscreen-active] {
153+
position: fixed !important;
154+
inset: 0 !important;
155+
width: 100vw !important;
156+
height: 100svh !important;
157+
z-index: 99999 !important;
158+
background: #000 !important;
159+
}
160+
[data-ios-fullscreen-hidden] { display: none !important; }
161+
`;
162+
163+
function isIOSFullscreenShimRequired() {
164+
const osName = Bowser.getParser(navigator.userAgent).getOSName(true);
165+
return (
166+
osName === "ios" ||
167+
// iPadOS 13+ reports as macOS with touch support, so fall back to that check.
168+
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)
169+
);
170+
}
171+
172+
export function installIOSFullscreenShim() {
173+
if (!isIOSFullscreenShimRequired()) {
174+
return () => {};
175+
}
176+
177+
const proto = HTMLElement.prototype;
178+
const overrides: Array<{
179+
target: object;
180+
key: PropertyKey;
181+
prev?: PropertyDescriptor;
182+
}> = [];
183+
const override = (
184+
target: object,
185+
key: PropertyKey,
186+
descriptor: PropertyDescriptor,
187+
) => {
188+
overrides.push({
189+
target,
190+
key,
191+
prev: Object.getOwnPropertyDescriptor(target, key),
192+
});
193+
Object.defineProperty(target, key, { configurable: true, ...descriptor });
194+
};
195+
196+
const styleEl = document.createElement("style");
197+
styleEl.textContent = IOS_FULLSCREEN_STYLE;
198+
document.head.appendChild(styleEl);
199+
200+
let fullscreenElement: HTMLElement | null = null;
201+
202+
const dispatchChange = (target: HTMLElement) => {
203+
document.dispatchEvent(new Event("fullscreenchange"));
204+
target.dispatchEvent(new Event("fullscreenchange"));
205+
};
206+
207+
const enter = (el: HTMLElement) => {
208+
if (fullscreenElement === el) return Promise.resolve();
209+
if (fullscreenElement) void exit();
210+
211+
el.setAttribute("data-ios-fullscreen-active", "");
212+
document
213+
.querySelectorAll<HTMLElement>(IOS_FULLSCREEN_NAV_SELECTOR)
214+
.forEach((nav) => nav.setAttribute("data-ios-fullscreen-hidden", ""));
215+
fullscreenElement = el;
216+
dispatchChange(el);
217+
return Promise.resolve();
218+
};
219+
220+
const exit = () => {
221+
const el = fullscreenElement;
222+
if (!el) return Promise.resolve();
223+
el.removeAttribute("data-ios-fullscreen-active");
224+
document
225+
.querySelectorAll<HTMLElement>("[data-ios-fullscreen-hidden]")
226+
.forEach((nav) => nav.removeAttribute("data-ios-fullscreen-hidden"));
227+
fullscreenElement = null;
228+
dispatchChange(el);
229+
return Promise.resolve();
230+
};
231+
232+
override(document, "fullscreenEnabled", { get: () => true });
233+
override(document, "fullscreenElement", { get: () => fullscreenElement });
234+
override(document, "exitFullscreen", { value: exit, writable: true });
235+
override(proto, "requestFullscreen", {
236+
value: function (this: HTMLElement) {
237+
return enter(this);
238+
},
239+
writable: true,
240+
});
241+
override(proto, "webkitRequestFullscreen", {
242+
value: function (this: HTMLElement) {
243+
void enter(this);
244+
},
245+
writable: true,
246+
});
247+
248+
return () => {
249+
void exit();
250+
styleEl.remove();
251+
while (overrides.length) {
252+
const { target, key, prev } = overrides.pop()!;
253+
if (prev) Object.defineProperty(target, key, prev);
254+
else Reflect.deleteProperty(target, key);
255+
}
256+
};
257+
}
258+
148259
export function createQuickLoadButton(): HTMLButtonElement {
149260
const button = document.createElement("button");
150261
button.type = "button";

0 commit comments

Comments
 (0)