Make useLocalStorage actually work

This commit is contained in:
Gwilym Inzani 2023-07-05 11:59:32 +01:00 committed by Corwin
parent 472d641cfc
commit 683afbf133
No known key found for this signature in database
3 changed files with 138 additions and 118 deletions

View file

@ -1,5 +1,5 @@
import React, { useState } from "react"; import { useRef, useState } from "react";
import { Mgba } from "./mgba"; import { Mgba, MgbaHandle } from "./mgba";
import { BindingsControl, DefaultBindingsSet, Bindings } from "./bindings"; import { BindingsControl, DefaultBindingsSet, Bindings } from "./bindings";
import { styled } from "styled-components"; import { styled } from "styled-components";
import { useOnKeyUp } from "./useOnKeyUp.hook"; import { useOnKeyUp } from "./useOnKeyUp.hook";
@ -16,24 +16,28 @@ const VolumeLabel = styled.label`
margin-bottom: 20px; margin-bottom: 20px;
`; `;
const CloseButton = styled.button` const ActionButton = styled.button`
width: 100%; width: 100%;
margin-top: 20px; margin-top: 20px;
`; `;
function App() { function App() {
const [volumeState, setVolume] = useState(1.0); const [{ volume, bindings }, setState] = useLocalStorage(
const [bindingsState, setBindings] = useState(DefaultBindingsSet()); { volume: 1.0, bindings: DefaultBindingsSet() },
const { volume, bindings } = useLocalStorage(
{ volume: volumeState, bindings: bindingsState },
"agbrswebplayer" "agbrswebplayer"
); );
const setVolume = (newVolume: number) =>
setState({ volume: newVolume, bindings });
const setBindings = (newBindings: Bindings) =>
setState({ volume, bindings: newBindings });
const [paused, setPaused] = useState(false); const [paused, setPaused] = useState(false);
const [showBindings, setShowBindings] = useState(false); const [showBindings, setShowBindings] = useState(false);
const mgbaRef = useRef<MgbaHandle>(null);
useOnKeyUp("Escape", () => { useOnKeyUp("Escape", () => {
setShowBindings(!showBindings); setShowBindings(!showBindings);
}); });
@ -48,9 +52,11 @@ function App() {
volume={volume} volume={volume}
setVolume={setVolume} setVolume={setVolume}
hide={() => setShowBindings(false)} hide={() => setShowBindings(false)}
restart={() => mgbaRef.current?.restart()}
/> />
)} )}
<Mgba <Mgba
ref={mgbaRef}
gameUrl="/game.gba" gameUrl="/game.gba"
volume={volume} volume={volume}
controls={bindings.Actual} controls={bindings.Actual}
@ -67,6 +73,7 @@ function BindingsWindow({
volume, volume,
setVolume, setVolume,
hide, hide,
restart,
}: { }: {
bindings: Bindings; bindings: Bindings;
setBindings: (b: Bindings) => void; setBindings: (b: Bindings) => void;
@ -74,6 +81,7 @@ function BindingsWindow({
volume: number; volume: number;
setVolume: (v: number) => void; setVolume: (v: number) => void;
hide: () => void; hide: () => void;
restart: () => void;
}) { }) {
return ( return (
<BindingsDialog open onClose={hide}> <BindingsDialog open onClose={hide}>
@ -97,7 +105,8 @@ function BindingsWindow({
setBindings={setBindings} setBindings={setBindings}
setPaused={setPaused} setPaused={setPaused}
/> />
<CloseButton onClick={hide}>Close</CloseButton> <ActionButton onClick={restart}>Restart</ActionButton>
<ActionButton onClick={hide}>Close</ActionButton>
</BindingsDialog> </BindingsDialog>
); );
} }

View file

@ -1,4 +1,11 @@
import { FC, useEffect, useRef, useState } from "react"; import {
FC,
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import mGBA from "./vendor/mgba"; import mGBA from "./vendor/mgba";
import { KeyBindings } from "./bindings"; import { KeyBindings } from "./bindings";
import { styled } from "styled-components"; import { styled } from "styled-components";
@ -28,89 +35,101 @@ const MgbaCanvas = styled.canvas`
max-height: 100%; max-height: 100%;
`; `;
export const Mgba: FC<MgbaProps> = ({ gameUrl, volume, controls, paused }) => { export interface MgbaHandle {
const canvas = useRef(null); restart: () => void;
const mgbaModule = useRef<Module>({}); }
const [state, setState] = useState(MgbaState.Uninitialised); export const Mgba = forwardRef<MgbaHandle, MgbaProps>(
const [gameLoaded, setGameLoaded] = useState(false); ({ gameUrl, volume, controls, paused }, ref) => {
const canvas = useRef(null);
const mgbaModule = useRef<Module>({});
useEffect(() => { const [state, setState] = useState(MgbaState.Uninitialised);
if (state !== MgbaState.Initialised) return; const [gameLoaded, setGameLoaded] = useState(false);
(async () => {
const game = await fetch(gameUrl);
const gameData = await game.arrayBuffer();
const gamePath = `${MGBA_ROM_DIRECTORY}/${gameUrl}`; useEffect(() => {
mgbaModule.current.FS.writeFile(gamePath, new Uint8Array(gameData)); if (state !== MgbaState.Initialised) return;
mgbaModule.current.loadGame(gamePath); (async () => {
setGameLoaded(true); const game = await fetch(gameUrl);
})(); const gameData = await game.arrayBuffer();
}, [state, gameUrl]);
// init mgba const gamePath = `${MGBA_ROM_DIRECTORY}/${gameUrl}`;
useEffect(() => { mgbaModule.current.FS.writeFile(gamePath, new Uint8Array(gameData));
(async () => { mgbaModule.current.loadGame(gamePath);
if (canvas === null) return; setGameLoaded(true);
if (state !== MgbaState.Uninitialised) return; })();
}, [state, gameUrl]);
setState(MgbaState.Initialising); // init mgba
useEffect(() => {
(async () => {
if (canvas === null) return;
if (state !== MgbaState.Uninitialised) return;
mgbaModule.current = { setState(MgbaState.Initialising);
canvas: canvas.current,
locateFile: (file: string) => { mgbaModule.current = {
if (file === "mgba.wasm") { canvas: canvas.current,
return "/vendor/mgba.wasm"; locateFile: (file: string) => {
} if (file === "mgba.wasm") {
return file; return "/vendor/mgba.wasm";
}, }
return file;
},
};
mGBA(mgbaModule.current).then((module: Module) => {
mgbaModule.current = module;
module.FSInit();
setState(MgbaState.Initialised);
});
})();
if (state === MgbaState.Initialised)
return () => {
try {
mgbaModule.current.quitGame();
mgbaModule.current.quitMgba();
} catch {}
};
}, [state]);
useEffect(() => {
if (!gameLoaded) return;
const controlEntries = Object.entries(controls);
for (const [key, value] of controlEntries) {
const binding =
value === "Enter"
? "Return"
: value.toLowerCase().replace("arrow", "").replace("key", "");
mgbaModule.current.bindKey(binding, key);
}
}, [controls, gameLoaded]);
useEffect(() => {
if (!gameLoaded) return;
mgbaModule.current.setVolume(volume ?? 1.0);
}, [gameLoaded, volume]);
useEffect(() => {
if (!gameLoaded) return;
if (paused) {
mgbaModule.current.pauseGame();
} else {
mgbaModule.current.resumeGame();
}
}, [gameLoaded, paused]);
useImperativeHandle(ref, () => {
return {
restart: () => mgbaModule.current.quickReload(),
}; };
});
mGBA(mgbaModule.current).then((module: Module) => { return <MgbaCanvas ref={canvas} />;
mgbaModule.current = module; }
module.FSInit(); );
setState(MgbaState.Initialised);
});
})();
if (state === MgbaState.Initialised)
return () => {
try {
mgbaModule.current.quitGame();
mgbaModule.current.quitMgba();
} catch {}
};
}, [state]);
useEffect(() => {
if (!gameLoaded) return;
const controlEntries = Object.entries(controls);
for (const [key, value] of controlEntries) {
const binding =
value === "Enter"
? "Return"
: value.toLowerCase().replace("arrow", "").replace("key", "");
mgbaModule.current.bindKey(binding, key);
}
}, [controls, gameLoaded]);
useEffect(() => {
if (!gameLoaded) return;
mgbaModule.current.setVolume(volume ?? 1.0);
}, [gameLoaded, volume]);
useEffect(() => {
if (!gameLoaded) return;
if (paused) {
mgbaModule.current.pauseGame();
} else {
mgbaModule.current.resumeGame();
}
}, [gameLoaded, paused]);
return <MgbaCanvas ref={canvas} />;
};

View file

@ -1,36 +1,28 @@
import { useRef, useLayoutEffect, useEffect } from "react"; import { useCallback, useEffect, useState } from "react";
export const useLocalStorage = <T>(currentValue: T, appName: string): T => { export const useLocalStorage = <T>(
const initialValue = useRef<T>(); defaultValue: T,
appName: string
const isFirstRun = !initialValue.current; ): [T, (newValue: T) => void] => {
const [value, setValue] = useState(() => {
useLayoutEffect(() => { try {
if (!initialValue.current) { const storageValue = localStorage.getItem(appName);
try { if (storageValue) {
const storageValue = localStorage.getItem(appName); return JSON.parse(storageValue);
if (storageValue) { } else {
initialValue.current = JSON.parse(storageValue); return defaultValue;
} else {
initialValue.current = currentValue;
}
} catch {
initialValue.current = currentValue;
} }
} catch {
return defaultValue;
} }
});
const setStoredValue = useCallback((newValue: T) => {
setValue(newValue);
try {
localStorage.setItem(appName, JSON.stringify(newValue));
} catch {}
}, []); }, []);
useEffect(() => { return [value, setStoredValue];
try {
if (initialValue.current && currentValue) {
localStorage.setItem(appName, JSON.stringify(currentValue));
}
} catch {}
}, [currentValue]);
if (isFirstRun) {
return initialValue.current ?? currentValue;
} else {
return currentValue;
}
}; };