mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 08:11:33 +11:00
Make useLocalStorage actually work
This commit is contained in:
parent
472d641cfc
commit
683afbf133
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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} />;
|
|
||||||
};
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue