Added language provider, fixed race condition on emulator

This commit is contained in:
2025-03-16 18:24:20 -05:00
parent 60db15e932
commit 85615121b2
8 changed files with 98 additions and 12 deletions

View File

@ -34,6 +34,10 @@
<script type="text/javascript" language="javascript"> <script type="text/javascript" language="javascript">
let scriptElement = null; let scriptElement = null;
let initInterval = setInterval(() => {
send({ message: 'iframe_loaded' });
}, 500);
EJS_player = "#game"; EJS_player = "#game";
EJS_pathtodata = "https://cdn.emulatorjs.org/stable/data/"; EJS_pathtodata = "https://cdn.emulatorjs.org/stable/data/";
EJS_language = "en-US"; EJS_language = "en-US";
@ -77,6 +81,8 @@
scriptElement = document.createElement('script'); scriptElement = document.createElement('script');
scriptElement.src = 'https://cdn.emulatorjs.org/stable/data/loader.js'; scriptElement.src = 'https://cdn.emulatorjs.org/stable/data/loader.js';
document.body.appendChild(scriptElement); document.body.appendChild(scriptElement);
clearInterval(initInterval);
} }
const send = data => { const send = data => {
@ -145,7 +151,7 @@
EJS_onGameStart = () => { EJS_onGameStart = () => {
send({ message: 'start' }); send({ message: 'start' });
} }
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import styles from './Emulator.module.scss'; import styles from './Emulator.module.scss';
import { GAME_SYSTEM_CORES, GameSystem, GameSystemCore } from '@/lib/game'; import { GAME_SYSTEM_CORES, GameSystem, GameSystemCore } from '@/lib/game';
import { useLanguage } from '@/providers/LanguageProvider';
type SendEmulatorMessageInit = { type SendEmulatorMessageInit = {
message:'init'; message:'init';
@ -21,6 +22,7 @@ type SendEmulatorMessage = (
); );
type ReceiveEmulatorMessage = ( type ReceiveEmulatorMessage = (
{ message: 'iframe_loaded' } |
{ message: 'start' } | { message: 'start' } |
{ message: 'ready' } | { message: 'ready' } |
{ message: 'save' } | { message: 'save' } |
@ -36,6 +38,7 @@ export type EmulatorProps = {
export const Emulator:React.FC<EmulatorProps> = props => { export const Emulator:React.FC<EmulatorProps> = props => {
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
const [ hasInit, setHasInit ] = useState(false); const [ hasInit, setHasInit ] = useState(false);
const { t } = useLanguage();
const send = (msg:SendEmulatorMessage) => { const send = (msg:SendEmulatorMessage) => {
iframeRef.current?.contentWindow?.postMessage(msg, '*'); iframeRef.current?.contentWindow?.postMessage(msg, '*');
@ -45,14 +48,6 @@ export const Emulator:React.FC<EmulatorProps> = props => {
if(hasInit) return; if(hasInit) return;
setHasInit(true); setHasInit(true);
send({
message: 'init',
core: GAME_SYSTEM_CORES[props.system],
system: props.system,
gameId: props.gameId,
gameName: props.gameName
});
window.onmessage = e => { window.onmessage = e => {
if(!e.data) { if(!e.data) {
console.error('Invalid message received:', e.data); console.error('Invalid message received:', e.data);
@ -61,6 +56,17 @@ export const Emulator:React.FC<EmulatorProps> = props => {
const msg = e.data as ReceiveEmulatorMessage; const msg = e.data as ReceiveEmulatorMessage;
switch(msg.message) { switch(msg.message) {
case 'iframe_loaded':
if(hasInit) return;
send({
message: 'init',
core: GAME_SYSTEM_CORES[props.system],
system: props.system,
gameId: props.gameId,
gameName: props.gameName
});
break;
case 'start': case 'start':
case 'ready': case 'ready':
case 'load': case 'load':
@ -71,7 +77,17 @@ export const Emulator:React.FC<EmulatorProps> = props => {
break break
} }
}; };
}, [ iframeRef ]);
window.onbeforeunload = (e: BeforeUnloadEvent) => {
e.preventDefault();
return t('emulator.exit_unsaved');
};
return () => {
window.onbeforeunload = null;
};
}, [ ]);
return ( return (
<div className={styles.emulator}> <div className={styles.emulator}>

View File

@ -1,6 +1,6 @@
import { GAME_SYSTEMS } from "@/lib/game";
import { ApolloError, gql } from "apollo-server-micro"; import { ApolloError, gql } from "apollo-server-micro";
import { pageTypeDefs } from "./page"; import { pageTypeDefs } from "./page";
import { GAME_SYSTEMS } from "@/lib/game";
export const gameTypeDefs = gql` export const gameTypeDefs = gql`
${pageTypeDefs} ${pageTypeDefs}

View File

@ -7,9 +7,12 @@ export const GAME_SYSTEM_CORES = <const>{
'gba': 'mgba' 'gba': 'mgba'
}; };
export type GameSystem = keyof typeof GAME_SYSTEM_CORES; export type GameSystem = keyof typeof GAME_SYSTEM_CORES;
export type GameSystemCore = typeof GAME_SYSTEM_CORES[GameSystem]; export type GameSystemCore = typeof GAME_SYSTEM_CORES[GameSystem];
export const GAME_SYSTEMS = Object.keys(GAME_SYSTEM_CORES) as GameSystem[];
export type GameLight = { export type GameLight = {
id:string; id:string;
name:string; name:string;

10
src/lib/language.ts Normal file
View File

@ -0,0 +1,10 @@
export const LANGUAGE_ENGLISH = <const>{
'emulator.exit_unsaved': 'Your game may not be saved. Are you sure you want to exit?',
};
export const LANGUAGES = <const>{
'en': LANGUAGE_ENGLISH
}
export type LanguageKey = keyof typeof LANGUAGE_ENGLISH;
export type Language = keyof typeof LANGUAGES;

View File

@ -1,11 +1,14 @@
import React from 'react'; import React from 'react';
import { AppProps } from 'next/app'; import { AppProps } from 'next/app';
import './globals.scss'; import './globals.scss';
import { LanguageProvider } from '@/providers/LanguageProvider';
const RootLayout:React.FC<AppProps> = ({ Component, pageProps }) => { const RootLayout:React.FC<AppProps> = ({ Component, pageProps }) => {
return ( return (
<> <>
<Component {...pageProps} /> <LanguageProvider>
<Component {...pageProps} />
</LanguageProvider>
</> </>
); );
} }

View File

@ -1,3 +1,4 @@
import { LanguageProvider } from '@/providers/LanguageProvider';
import { Html, Head, Main, NextScript, DocumentProps } from 'next/document'; import { Html, Head, Main, NextScript, DocumentProps } from 'next/document';
const RootDocument:React.FC<DocumentProps> = props => { const RootDocument:React.FC<DocumentProps> = props => {

View File

@ -0,0 +1,47 @@
import { Language, LanguageKey, LANGUAGES } from '@/lib/language';
import React, { createContext, useContext, useState, ReactNode } from 'react';
type LanguageContextType = {
lang:Language;
t:(key:LanguageKey, p?:{ [key:string]:string }) => string;
};
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
export const LanguageProvider: React.FC<{ children: ReactNode }> = ({
children
}) => {
const [ lang, setLang ] = useState<Language>('en');
const t:LanguageContextType['t'] = (key, p) => {
const str = LANGUAGES[lang][key];
if(!str) {
console.error(`Invalid language key: ${key}`);
return '';
}
if(!p) return str;
return Object.keys(p).reduce((acc, k, v) => {
return acc.replace(`{{${k}}}`, p[k]);
}, str);
};
return (
<LanguageContext.Provider value={{
lang,
t
}}>
{children}
</LanguageContext.Provider>
);
};
export const useLanguage = (): LanguageContextType => {
const context = useContext(LanguageContext);
if (!context) {
throw new Error('useLanguage must be used within a LanguageProvider');
}
return context;
};