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 { Mgba } from "./mgba";
import { useRef, useState } from "react";
import { Mgba, MgbaHandle } from "./mgba";
import { BindingsControl, DefaultBindingsSet, Bindings } from "./bindings";
import { styled } from "styled-components";
import { useOnKeyUp } from "./useOnKeyUp.hook";
@ -16,24 +16,28 @@ const VolumeLabel = styled.label`
margin-bottom: 20px;
`;
const CloseButton = styled.button`
const ActionButton = styled.button`
width: 100%;
margin-top: 20px;
`;
function App() {
const [volumeState, setVolume] = useState(1.0);
const [bindingsState, setBindings] = useState(DefaultBindingsSet());
const { volume, bindings } = useLocalStorage(
{ volume: volumeState, bindings: bindingsState },
const [{ volume, bindings }, setState] = useLocalStorage(
{ volume: 1.0, bindings: DefaultBindingsSet() },
"agbrswebplayer"
);
const setVolume = (newVolume: number) =>
setState({ volume: newVolume, bindings });
const setBindings = (newBindings: Bindings) =>
setState({ volume, bindings: newBindings });
const [paused, setPaused] = useState(false);
const [showBindings, setShowBindings] = useState(false);
const mgbaRef = useRef<MgbaHandle>(null);
useOnKeyUp("Escape", () => {
setShowBindings(!showBindings);
});
@ -48,9 +52,11 @@ function App() {
volume={volume}
setVolume={setVolume}
hide={() => setShowBindings(false)}
restart={() => mgbaRef.current?.restart()}
/>
)}
<Mgba
ref={mgbaRef}
gameUrl="/game.gba"
volume={volume}
controls={bindings.Actual}
@ -67,6 +73,7 @@ function BindingsWindow({
volume,
setVolume,
hide,
restart,
}: {
bindings: Bindings;
setBindings: (b: Bindings) => void;
@ -74,6 +81,7 @@ function BindingsWindow({
volume: number;
setVolume: (v: number) => void;
hide: () => void;
restart: () => void;
}) {
return (
<BindingsDialog open onClose={hide}>
@ -97,7 +105,8 @@ function BindingsWindow({
setBindings={setBindings}
setPaused={setPaused}
/>
<CloseButton onClick={hide}>Close</CloseButton>
<ActionButton onClick={restart}>Restart</ActionButton>
<ActionButton onClick={hide}>Close</ActionButton>
</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 { KeyBindings } from "./bindings";
import { styled } from "styled-components";
@ -28,89 +35,101 @@ const MgbaCanvas = styled.canvas`
max-height: 100%;
`;
export const Mgba: FC<MgbaProps> = ({ gameUrl, volume, controls, paused }) => {
const canvas = useRef(null);
const mgbaModule = useRef<Module>({});
export interface MgbaHandle {
restart: () => void;
}
const [state, setState] = useState(MgbaState.Uninitialised);
const [gameLoaded, setGameLoaded] = useState(false);
export const Mgba = forwardRef<MgbaHandle, MgbaProps>(
({ gameUrl, volume, controls, paused }, ref) => {
const canvas = useRef(null);
const mgbaModule = useRef<Module>({});
useEffect(() => {
if (state !== MgbaState.Initialised) return;
(async () => {
const game = await fetch(gameUrl);
const gameData = await game.arrayBuffer();
const [state, setState] = useState(MgbaState.Uninitialised);
const [gameLoaded, setGameLoaded] = useState(false);
const gamePath = `${MGBA_ROM_DIRECTORY}/${gameUrl}`;
mgbaModule.current.FS.writeFile(gamePath, new Uint8Array(gameData));
mgbaModule.current.loadGame(gamePath);
setGameLoaded(true);
})();
}, [state, gameUrl]);
useEffect(() => {
if (state !== MgbaState.Initialised) return;
(async () => {
const game = await fetch(gameUrl);
const gameData = await game.arrayBuffer();
// init mgba
useEffect(() => {
(async () => {
if (canvas === null) return;
if (state !== MgbaState.Uninitialised) return;
const gamePath = `${MGBA_ROM_DIRECTORY}/${gameUrl}`;
mgbaModule.current.FS.writeFile(gamePath, new Uint8Array(gameData));
mgbaModule.current.loadGame(gamePath);
setGameLoaded(true);
})();
}, [state, gameUrl]);
setState(MgbaState.Initialising);
// init mgba
useEffect(() => {
(async () => {
if (canvas === null) return;
if (state !== MgbaState.Uninitialised) return;
mgbaModule.current = {
canvas: canvas.current,
locateFile: (file: string) => {
if (file === "mgba.wasm") {
return "/vendor/mgba.wasm";
}
return file;
},
setState(MgbaState.Initialising);
mgbaModule.current = {
canvas: canvas.current,
locateFile: (file: string) => {
if (file === "mgba.wasm") {
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) => {
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} />;
};
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 => {
const initialValue = useRef<T>();
const isFirstRun = !initialValue.current;
useLayoutEffect(() => {
if (!initialValue.current) {
try {
const storageValue = localStorage.getItem(appName);
if (storageValue) {
initialValue.current = JSON.parse(storageValue);
} else {
initialValue.current = currentValue;
}
} catch {
initialValue.current = currentValue;
export const useLocalStorage = <T>(
defaultValue: T,
appName: string
): [T, (newValue: T) => void] => {
const [value, setValue] = useState(() => {
try {
const storageValue = localStorage.getItem(appName);
if (storageValue) {
return JSON.parse(storageValue);
} else {
return defaultValue;
}
} catch {
return defaultValue;
}
});
const setStoredValue = useCallback((newValue: T) => {
setValue(newValue);
try {
localStorage.setItem(appName, JSON.stringify(newValue));
} catch {}
}, []);
useEffect(() => {
try {
if (initialValue.current && currentValue) {
localStorage.setItem(appName, JSON.stringify(currentValue));
}
} catch {}
}, [currentValue]);
if (isFirstRun) {
return initialValue.current ?? currentValue;
} else {
return currentValue;
}
return [value, setStoredValue];
};