From 85615121b25ff8922ffdd04df8c4f461748dcf6c Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sun, 16 Mar 2025 18:24:20 -0500 Subject: [PATCH] Added language provider, fixed race condition on emulator --- public/emulator.html | 8 ++++- src/components/Emulator.tsx | 34 +++++++++++++++------ src/graphql/game.ts | 2 +- src/lib/game.ts | 3 ++ src/lib/language.ts | 10 +++++++ src/pages/_app.tsx | 5 +++- src/pages/_document.tsx | 1 + src/providers/LanguageProvider.tsx | 47 ++++++++++++++++++++++++++++++ 8 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 src/lib/language.ts create mode 100644 src/providers/LanguageProvider.tsx diff --git a/public/emulator.html b/public/emulator.html index 9140bfd..2e93c50 100644 --- a/public/emulator.html +++ b/public/emulator.html @@ -34,6 +34,10 @@ \ No newline at end of file diff --git a/src/components/Emulator.tsx b/src/components/Emulator.tsx index e45aa31..ea4bfd0 100644 --- a/src/components/Emulator.tsx +++ b/src/components/Emulator.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import styles from './Emulator.module.scss'; import { GAME_SYSTEM_CORES, GameSystem, GameSystemCore } from '@/lib/game'; +import { useLanguage } from '@/providers/LanguageProvider'; type SendEmulatorMessageInit = { message:'init'; @@ -21,6 +22,7 @@ type SendEmulatorMessage = ( ); type ReceiveEmulatorMessage = ( + { message: 'iframe_loaded' } | { message: 'start' } | { message: 'ready' } | { message: 'save' } | @@ -36,6 +38,7 @@ export type EmulatorProps = { export const Emulator:React.FC = props => { const iframeRef = useRef(null); const [ hasInit, setHasInit ] = useState(false); + const { t } = useLanguage(); const send = (msg:SendEmulatorMessage) => { iframeRef.current?.contentWindow?.postMessage(msg, '*'); @@ -45,14 +48,6 @@ export const Emulator:React.FC = props => { if(hasInit) return; setHasInit(true); - send({ - message: 'init', - core: GAME_SYSTEM_CORES[props.system], - system: props.system, - gameId: props.gameId, - gameName: props.gameName - }); - window.onmessage = e => { if(!e.data) { console.error('Invalid message received:', e.data); @@ -61,6 +56,17 @@ export const Emulator:React.FC = props => { const msg = e.data as ReceiveEmulatorMessage; 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 'ready': case 'load': @@ -71,7 +77,17 @@ export const Emulator:React.FC = props => { break } }; - }, [ iframeRef ]); + + window.onbeforeunload = (e: BeforeUnloadEvent) => { + e.preventDefault(); + return t('emulator.exit_unsaved'); + }; + + + return () => { + window.onbeforeunload = null; + }; + }, [ ]); return (
diff --git a/src/graphql/game.ts b/src/graphql/game.ts index f3d5f8b..b07755e 100644 --- a/src/graphql/game.ts +++ b/src/graphql/game.ts @@ -1,6 +1,6 @@ -import { GAME_SYSTEMS } from "@/lib/game"; import { ApolloError, gql } from "apollo-server-micro"; import { pageTypeDefs } from "./page"; +import { GAME_SYSTEMS } from "@/lib/game"; export const gameTypeDefs = gql` ${pageTypeDefs} diff --git a/src/lib/game.ts b/src/lib/game.ts index 8d36f3f..b04b607 100644 --- a/src/lib/game.ts +++ b/src/lib/game.ts @@ -7,9 +7,12 @@ export const GAME_SYSTEM_CORES = { 'gba': 'mgba' }; + export type GameSystem = keyof typeof GAME_SYSTEM_CORES; export type GameSystemCore = typeof GAME_SYSTEM_CORES[GameSystem]; +export const GAME_SYSTEMS = Object.keys(GAME_SYSTEM_CORES) as GameSystem[]; + export type GameLight = { id:string; name:string; diff --git a/src/lib/language.ts b/src/lib/language.ts new file mode 100644 index 0000000..507581f --- /dev/null +++ b/src/lib/language.ts @@ -0,0 +1,10 @@ +export const LANGUAGE_ENGLISH = { + 'emulator.exit_unsaved': 'Your game may not be saved. Are you sure you want to exit?', +}; + +export const LANGUAGES = { + 'en': LANGUAGE_ENGLISH +} + +export type LanguageKey = keyof typeof LANGUAGE_ENGLISH; +export type Language = keyof typeof LANGUAGES; \ No newline at end of file diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index c865490..849395f 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,11 +1,14 @@ import React from 'react'; import { AppProps } from 'next/app'; import './globals.scss'; +import { LanguageProvider } from '@/providers/LanguageProvider'; const RootLayout:React.FC = ({ Component, pageProps }) => { return ( <> - + + + ); } diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index 6518309..795e129 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -1,3 +1,4 @@ +import { LanguageProvider } from '@/providers/LanguageProvider'; import { Html, Head, Main, NextScript, DocumentProps } from 'next/document'; const RootDocument:React.FC = props => { diff --git a/src/providers/LanguageProvider.tsx b/src/providers/LanguageProvider.tsx new file mode 100644 index 0000000..c3e284b --- /dev/null +++ b/src/providers/LanguageProvider.tsx @@ -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(undefined); + +export const LanguageProvider: React.FC<{ children: ReactNode }> = ({ + children +}) => { + const [ lang, setLang ] = useState('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 ( + + {children} + + ); +}; + +export const useLanguage = (): LanguageContextType => { + const context = useContext(LanguageContext); + if (!context) { + throw new Error('useLanguage must be used within a LanguageProvider'); + } + return context; +};