fix: add ios fullscreen shim for emulatorjs

Co-authored-by: gantoine <3247106+gantoine@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-05-25 12:02:08 +00:00
committed by GitHub
parent 667672bba5
commit ce7ffaf552
2 changed files with 204 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ import type { Events } from "@/types/emitter";
import { getSupportedEJSCores } from "@/utils";
import CacheDialog from "@/views/Player/EmulatorJS/CacheDialog.vue";
import Player from "@/views/Player/EmulatorJS/Player.vue";
import { installIOSFullscreenShim } from "./utils";
const { t } = useI18n();
const { xs, mdAndUp, smAndDown } = useDisplay();
@@ -38,6 +39,7 @@ const selectedDisc = ref<number | null>(null);
const selectedCore = ref<string | null>(null);
const selectedFirmware = ref<FirmwareSchema | null>(null);
const supportedCores = ref<string[]>([]);
const removeIOSFullscreenShim = ref<(() => void) | null>(null);
const gameRunning = ref(false);
const fullScreenOnPlay = useLocalStorage("emulation.fullScreenOnPlay", true);
@@ -58,6 +60,9 @@ const compatibleStates = computed(
);
async function onPlay() {
removeIOSFullscreenShim.value?.();
removeIOSFullscreenShim.value = installIOSFullscreenShim();
if (rom.value && auth.scopes.includes("roms.user.write")) {
romApi.updateUserRomProps({
romId: rom.value.id,
@@ -102,6 +107,8 @@ async function onPlay() {
playing.value = true;
fullScreen.value = fullScreenOnPlay.value;
} catch (err) {
removeIOSFullscreenShim.value?.();
removeIOSFullscreenShim.value = null;
console.error("[Play] Emulator load failure:", err);
}
}
@@ -261,6 +268,8 @@ onMounted(async () => {
onBeforeUnmount(async () => {
window.EJS_emulator?.callEvent("exit");
removeIOSFullscreenShim.value?.();
removeIOSFullscreenShim.value = null;
emitter?.off("saveSelected", selectSave);
emitter?.off("stateSelected", selectState);
});

View File

@@ -145,6 +145,201 @@ export function loadEmulatorJSState(state: Uint8Array) {
window.EJS_emulator.gameManager.loadState(state);
}
type FullscreenCapableHTMLElement = typeof HTMLElement.prototype & {
webkitRequestFullscreen?: () => void;
};
const IOS_FULLSCREEN_NAV_SELECTOR =
".v-app-bar, .v-bottom-navigation, .v-navigation-drawer";
function isIOSFullscreenShimRequired() {
return (
/iP(ad|hone|od)/.test(navigator.userAgent) ||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)
);
}
function restoreProperty(
target: object,
property: PropertyKey,
descriptor?: PropertyDescriptor,
) {
if (descriptor) {
Object.defineProperty(target, property, descriptor);
return;
}
Reflect.deleteProperty(target, property);
}
export function installIOSFullscreenShim() {
if (!isIOSFullscreenShimRequired()) {
return () => {};
}
const htmlElementPrototype =
HTMLElement.prototype as FullscreenCapableHTMLElement;
const fullscreenEnabledDescriptor = Object.getOwnPropertyDescriptor(
document,
"fullscreenEnabled",
);
const fullscreenElementDescriptor = Object.getOwnPropertyDescriptor(
document,
"fullscreenElement",
);
const exitFullscreenDescriptor = Object.getOwnPropertyDescriptor(
document,
"exitFullscreen",
);
const requestFullscreenDescriptor = Object.getOwnPropertyDescriptor(
htmlElementPrototype,
"requestFullscreen",
);
const webkitRequestFullscreenDescriptor = Object.getOwnPropertyDescriptor(
htmlElementPrototype,
"webkitRequestFullscreen",
);
const navDisplayMap = new Map<
HTMLElement,
{ value: string; priority: string }
>();
let pseudoElement: HTMLElement | null = null;
let originalStyle = "";
let hadStyleAttribute = false;
const dispatchFullscreenChange = (element: HTMLElement | null) => {
document.dispatchEvent(new Event("fullscreenchange"));
element?.dispatchEvent(new Event("fullscreenchange"));
};
const hideNavigation = () => {
document
.querySelectorAll<HTMLElement>(IOS_FULLSCREEN_NAV_SELECTOR)
.forEach((element) => {
navDisplayMap.set(element, {
value: element.style.getPropertyValue("display"),
priority: element.style.getPropertyPriority("display"),
});
element.style.setProperty("display", "none", "important");
});
};
const showNavigation = () => {
navDisplayMap.forEach((display, element) => {
if (display.value) {
element.style.setProperty("display", display.value, display.priority);
} else {
element.style.removeProperty("display");
}
});
navDisplayMap.clear();
};
const setFullscreenElement = (element: HTMLElement | null) => {
Object.defineProperty(document, "fullscreenElement", {
configurable: true,
get: () => element,
});
};
const exitPseudoFullscreen = () => {
if (!pseudoElement) {
return Promise.resolve();
}
const currentElement = pseudoElement;
if (hadStyleAttribute) {
currentElement.setAttribute("style", originalStyle);
} else {
currentElement.removeAttribute("style");
}
showNavigation();
setFullscreenElement(null);
pseudoElement = null;
originalStyle = "";
hadStyleAttribute = false;
dispatchFullscreenChange(currentElement);
return Promise.resolve();
};
const enterPseudoFullscreen = (element: HTMLElement) => {
if (pseudoElement === element) {
return Promise.resolve();
}
if (pseudoElement) {
void exitPseudoFullscreen();
}
pseudoElement = element;
hadStyleAttribute = element.hasAttribute("style");
originalStyle = element.getAttribute("style") ?? "";
element.style.cssText =
`${originalStyle}${originalStyle ? ";" : ""}` +
"position:fixed!important;top:0!important;left:0!important;" +
"width:100vw!important;height:100svh!important;z-index:99999!important;" +
"background:#000!important;";
hideNavigation();
setFullscreenElement(element);
dispatchFullscreenChange(element);
return Promise.resolve();
};
Object.defineProperty(document, "fullscreenEnabled", {
configurable: true,
get: () => true,
});
setFullscreenElement(null);
Object.defineProperty(document, "exitFullscreen", {
configurable: true,
value: () => exitPseudoFullscreen(),
writable: true,
});
Object.defineProperty(htmlElementPrototype, "requestFullscreen", {
configurable: true,
value: function requestFullscreen(this: HTMLElement) {
return enterPseudoFullscreen(this);
},
writable: true,
});
if (
webkitRequestFullscreenDescriptor ||
"webkitRequestFullscreen" in htmlElementPrototype
) {
Object.defineProperty(htmlElementPrototype, "webkitRequestFullscreen", {
configurable: true,
value: function webkitRequestFullscreen(this: HTMLElement) {
void enterPseudoFullscreen(this);
},
writable: true,
});
}
return () => {
void exitPseudoFullscreen();
restoreProperty(
htmlElementPrototype,
"webkitRequestFullscreen",
webkitRequestFullscreenDescriptor,
);
restoreProperty(
htmlElementPrototype,
"requestFullscreen",
requestFullscreenDescriptor,
);
restoreProperty(document, "exitFullscreen", exitFullscreenDescriptor);
restoreProperty(document, "fullscreenElement", fullscreenElementDescriptor);
restoreProperty(document, "fullscreenEnabled", fullscreenEnabledDescriptor);
};
}
export function createQuickLoadButton(): HTMLButtonElement {
const button = document.createElement("button");
button.type = "button";