Merge branch 'main' into dev
This commit is contained in:
commit
2cf11a3f36
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
src/server/public/dist
|
src/server/public/dist
|
||||||
|
src/server/shrinko8
|
||||||
.env
|
.env
|
101
src/client/GamePage.tsx
Normal file
101
src/client/GamePage.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { useNavigate, useParams } from "react-router-dom"
|
||||||
|
import { Pico8Console } from "./pico8-client/Pico8Console";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { DbRelease } from "../server/dbal/dbal";
|
||||||
|
import { css } from "@emotion/css";
|
||||||
|
|
||||||
|
type Info = {
|
||||||
|
release: DbRelease | null;
|
||||||
|
versions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GamePage = () => {
|
||||||
|
const {author, slug, version} = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [info, setInfo] = useState<Info | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchInfo = async () => {
|
||||||
|
let url = `/api/release?author=${author}&slug=${slug}`;
|
||||||
|
if (version) {
|
||||||
|
url += `&version=${version.slice(1)}`;
|
||||||
|
}
|
||||||
|
const information = await fetch(url);
|
||||||
|
const json = await information.json();
|
||||||
|
setInfo(json);
|
||||||
|
}
|
||||||
|
fetchInfo();
|
||||||
|
}, [setInfo, author, slug, version]);
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
return (
|
||||||
|
<div className={`
|
||||||
|
min-height: 100vh;
|
||||||
|
`}>
|
||||||
|
LOADING...
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!info.release) {
|
||||||
|
return (
|
||||||
|
<div className={`
|
||||||
|
min-height: 100vh;
|
||||||
|
`}>
|
||||||
|
NOT FOUND
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={css`
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: hsl(230, 10%, 10%);
|
||||||
|
color: white;
|
||||||
|
`}>
|
||||||
|
<div className={css`
|
||||||
|
margin: auto;
|
||||||
|
width: max-content;
|
||||||
|
max-inline-size: 66ch;
|
||||||
|
padding: 1.5em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
`}>
|
||||||
|
<div>
|
||||||
|
<h1>{info.release.manifest.title ?? slug!.split("-").map(word => word[0].toUpperCase()+word.slice(1)).join(" ")}</h1>
|
||||||
|
<h2>by {info.release.author}</h2>
|
||||||
|
</div>
|
||||||
|
<div className={css`
|
||||||
|
width: 512px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
`}>
|
||||||
|
<div className={css`
|
||||||
|
border: 2px solid transparent;
|
||||||
|
&:focus-within {
|
||||||
|
border: 2px solid limegreen;
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
<Pico8Console carts={info.release.carts} />
|
||||||
|
</div>
|
||||||
|
<div className={css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
`}>
|
||||||
|
Version: <select defaultValue={info.release.version} onChange={(ev) => navigate(`/u/${author}/${slug}/v${ev.target.value}`)}>
|
||||||
|
{
|
||||||
|
[...info.versions].reverse().map(v => (
|
||||||
|
<option key={v} value={v}>{v}</option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <div>
|
||||||
|
<p>This is a paragraph about this game. It is a cool game. And a cool website to play it on. It automagically connects from GitHub.</p>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
3
src/client/HomePage.tsx
Normal file
3
src/client/HomePage.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const HomePage = () => {
|
||||||
|
return <div>Welcome to Picobook!</div>
|
||||||
|
}
|
@ -1,15 +1,17 @@
|
|||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { Pico8Console } from "./pico8-client/Pico8Console";
|
import { Pico8Console } from "./pico8-client/Pico8Console";
|
||||||
import testcarts from "./testcarts";
|
import testcarts from "./testcarts";
|
||||||
|
import { Routing } from "./routing";
|
||||||
|
|
||||||
const App = (props: {}) => {
|
const App = (props: {}) => {
|
||||||
return (
|
return (
|
||||||
<div className={css`
|
<Routing/>
|
||||||
min-height: 100vh;
|
// <div className={css`
|
||||||
`}>
|
// min-height: 100vh;
|
||||||
<h1>Picobook</h1>
|
// `}>
|
||||||
<Pico8Console carts={testcarts.carts} />
|
// <h1>Picobook</h1>
|
||||||
</div>
|
// <Pico8Console carts={testcarts.carts} />
|
||||||
|
// </div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,16 +8,27 @@ type Pico8ConsoleImperatives = {
|
|||||||
|
|
||||||
export const Pico8Console = forwardRef((props: { carts: PicoCart[] }, forwardedRef: ForwardedRef<Pico8ConsoleImperatives>) => {
|
export const Pico8Console = forwardRef((props: { carts: PicoCart[] }, forwardedRef: ForwardedRef<Pico8ConsoleImperatives>) => {
|
||||||
const {carts} = props;
|
const {carts} = props;
|
||||||
|
const [playing, setPlaying] = useState(false);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [handle, setHandle] = useState<PicoPlayerHandle | null>(null);
|
const [handle, setHandle] = useState<PicoPlayerHandle | null>(null);
|
||||||
const attachConsole = useCallback(async () => {
|
const attachConsole = useCallback(async () => {
|
||||||
const picoConsole = await makePicoConsole({
|
const picoConsole = await makePicoConsole({
|
||||||
carts,
|
carts,
|
||||||
});
|
});
|
||||||
|
picoConsole.canvas.tabIndex=0;
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
ref.current.appendChild(picoConsole.canvas);
|
ref.current.appendChild(picoConsole.canvas);
|
||||||
|
picoConsole.canvas.focus();
|
||||||
}
|
}
|
||||||
setHandle(picoConsole);
|
setHandle(picoConsole);
|
||||||
|
picoConsole.canvas.addEventListener('keydown',(event) => {
|
||||||
|
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key)) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}, {passive: false});
|
||||||
|
picoConsole.canvas.addEventListener('click', () => {
|
||||||
|
picoConsole.canvas.focus();
|
||||||
|
})
|
||||||
}, [carts]);
|
}, [carts]);
|
||||||
useImperativeHandle(forwardedRef, () => ({
|
useImperativeHandle(forwardedRef, () => ({
|
||||||
getPicoConsoleHandle() {
|
getPicoConsoleHandle() {
|
||||||
@ -25,8 +36,37 @@ export const Pico8Console = forwardRef((props: { carts: PicoCart[] }, forwardedR
|
|||||||
}
|
}
|
||||||
}), [handle]);
|
}), [handle]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (playing) {
|
||||||
attachConsole();
|
attachConsole();
|
||||||
}, [attachConsole]);
|
return () => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [playing, attachConsole]);
|
||||||
|
if (!playing) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={css`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
`}
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {setPlaying(true)}}
|
||||||
|
>
|
||||||
|
Play!
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={css`
|
<div ref={ref} className={css`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -18435,6 +18435,7 @@ use chrome, FireFox or Internet Explorer 11`);
|
|||||||
var require_veryRawRenderCart = __commonJS((exports, module) => {
|
var require_veryRawRenderCart = __commonJS((exports, module) => {
|
||||||
var __dirname = "/home/dylan/repos/picobook/src/client/pico8-client";
|
var __dirname = "/home/dylan/repos/picobook/src/client/pico8-client";
|
||||||
window.P8 = function(Module, cartNames, cartDatas) {
|
window.P8 = function(Module, cartNames, cartDatas) {
|
||||||
|
const codo_textarea_el = Module.codo_textarea || document.getElementById("codo_textarea");
|
||||||
let p8_touch_detected;
|
let p8_touch_detected;
|
||||||
let p8_dropped_cart;
|
let p8_dropped_cart;
|
||||||
let p8_dropped_cart_name;
|
let p8_dropped_cart_name;
|
||||||
@ -19390,7 +19391,7 @@ var require_veryRawRenderCart = __commonJS((exports, module) => {
|
|||||||
}, function() {
|
}, function() {
|
||||||
if (typeof codo_key_buffer === "undefined")
|
if (typeof codo_key_buffer === "undefined")
|
||||||
codo_key_buffer = [];
|
codo_key_buffer = [];
|
||||||
document.addEventListener("keydown", function(e) {
|
Module["canvas"].addEventListener("keydown", function(e) {
|
||||||
var val = -1;
|
var val = -1;
|
||||||
if (e.key.length == 1) {
|
if (e.key.length == 1) {
|
||||||
val = e.key.charCodeAt(0);
|
val = e.key.charCodeAt(0);
|
||||||
@ -19406,7 +19407,7 @@ var require_veryRawRenderCart = __commonJS((exports, module) => {
|
|||||||
if (val == -1) {
|
if (val == -1) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var el2 = document.getElementById("codo_textarea");
|
var el2 = codo_textarea_el;
|
||||||
codo_key_buffer.push(val);
|
codo_key_buffer.push(val);
|
||||||
});
|
});
|
||||||
}, function() {
|
}, function() {
|
||||||
@ -19515,7 +19516,7 @@ var require_veryRawRenderCart = __commonJS((exports, module) => {
|
|||||||
}, function() {
|
}, function() {
|
||||||
if (document.hidden)
|
if (document.hidden)
|
||||||
return 0;
|
return 0;
|
||||||
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea;
|
el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
|
||||||
if (el && el == document.activeElement)
|
if (el && el == document.activeElement)
|
||||||
return 1;
|
return 1;
|
||||||
el = document.activeElement;
|
el = document.activeElement;
|
||||||
@ -19529,13 +19530,13 @@ var require_veryRawRenderCart = __commonJS((exports, module) => {
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}, function() {
|
}, function() {
|
||||||
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea;
|
el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
|
||||||
if (el && el.style.display != "none") {
|
if (el && el.style.display != "none") {
|
||||||
el.focus();
|
el.focus();
|
||||||
el.select();
|
el.select();
|
||||||
}
|
}
|
||||||
}, function() {
|
}, function() {
|
||||||
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea;
|
el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
|
||||||
if (el && el.style.display != "none") {
|
if (el && el.style.display != "none") {
|
||||||
el.select();
|
el.select();
|
||||||
}
|
}
|
||||||
@ -19550,7 +19551,7 @@ var require_veryRawRenderCart = __commonJS((exports, module) => {
|
|||||||
}, function() {
|
}, function() {
|
||||||
Module["canvas"].exitPointerLock();
|
Module["canvas"].exitPointerLock();
|
||||||
}, function() {
|
}, function() {
|
||||||
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea;
|
el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
|
||||||
if (el) {
|
if (el) {
|
||||||
}
|
}
|
||||||
}, function() {
|
}, function() {
|
||||||
@ -19558,21 +19559,21 @@ var require_veryRawRenderCart = __commonJS((exports, module) => {
|
|||||||
}, function($0, $1) {
|
}, function($0, $1) {
|
||||||
_codo_str_out = Module.UTF8ToString($0, $1);
|
_codo_str_out = Module.UTF8ToString($0, $1);
|
||||||
}, function() {
|
}, function() {
|
||||||
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea;
|
el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
|
||||||
if (el) {
|
if (el) {
|
||||||
el.value = _codo_str_out;
|
el.value = _codo_str_out;
|
||||||
return 0;
|
return 0;
|
||||||
} else
|
} else
|
||||||
return 1;
|
return 1;
|
||||||
}, function() {
|
}, function() {
|
||||||
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea;
|
el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
|
||||||
if (el && el.style.display == "none" && (typeof p8_touch_detected === "undefined" || !p8_touch_detected)) {
|
if (el && el.style.display == "none" && (typeof p8_touch_detected === "undefined" || !p8_touch_detected)) {
|
||||||
el.style.display = "";
|
el.style.display = "";
|
||||||
el.focus();
|
el.focus();
|
||||||
el.select();
|
el.select();
|
||||||
}
|
}
|
||||||
}, function() {
|
}, function() {
|
||||||
el = typeof codo_textarea === "undefined" ? document.getElementById("codo_textarea") : codo_textarea;
|
el = typeof codo_textarea === "undefined" ? codo_textarea_el : codo_textarea;
|
||||||
if (el && el.style.display != "none" && el.value != "") {
|
if (el && el.style.display != "none" && el.value != "") {
|
||||||
_codo_text_value = el.value;
|
_codo_text_value = el.value;
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -3,7 +3,7 @@ import "./build/veryRawRenderCart.js";
|
|||||||
|
|
||||||
export type PicoBool = 0 | 1;
|
export type PicoBool = 0 | 1;
|
||||||
|
|
||||||
export type RenderCart = (Module: {canvas: HTMLCanvasElement}, cartNames: string[], cartDatas: number[][], audioContext: AudioContext) => {
|
export type RenderCart = (Module: {canvas: HTMLCanvasElement, codo_textarea?: HTMLTextAreaElement}, cartNames: string[], cartDatas: number[][], audioContext: AudioContext) => {
|
||||||
p8_touch_detected?: PicoBool;
|
p8_touch_detected?: PicoBool;
|
||||||
p8_dropped_cart?: string;
|
p8_dropped_cart?: string;
|
||||||
p8_dropped_cart_name?: string;
|
p8_dropped_cart_name?: string;
|
||||||
|
@ -80,12 +80,18 @@ const getRom = async (cart: PicoCart) => {
|
|||||||
|
|
||||||
export const makePicoConsole = async (props: {
|
export const makePicoConsole = async (props: {
|
||||||
canvas?: HTMLCanvasElement;
|
canvas?: HTMLCanvasElement;
|
||||||
|
codoTextarea?: HTMLTextAreaElement;
|
||||||
audioContext?: AudioContext;
|
audioContext?: AudioContext;
|
||||||
carts: PicoCart[];
|
carts: PicoCart[];
|
||||||
}): Promise<PicoPlayerHandle> => {
|
}): Promise<PicoPlayerHandle> => {
|
||||||
const {carts, canvas = document.createElement("canvas"), audioContext = new AudioContext()} = props;
|
const {carts, canvas = document.createElement("canvas"), codoTextarea = document.createElement("textarea"), audioContext = new AudioContext()} = props;
|
||||||
canvas.style.imageRendering = "pixelated";
|
canvas.style.imageRendering = "pixelated";
|
||||||
const Module = {canvas};
|
codoTextarea.style.display="none";
|
||||||
|
codoTextarea.style.position="fixed";
|
||||||
|
codoTextarea.style.left="-9999px";
|
||||||
|
codoTextarea.style.height="0px";
|
||||||
|
codoTextarea.style.overflow="hidden";
|
||||||
|
const Module = {canvas, keyboardListeningElement: canvas};
|
||||||
const cartsDatas = await Promise.all(carts.map(cart => getRom(cart)));
|
const cartsDatas = await Promise.all(carts.map(cart => getRom(cart)));
|
||||||
const handle = rawRenderCart(Module, carts.map(cart => cart.name), cartsDatas, audioContext);
|
const handle = rawRenderCart(Module, carts.map(cart => cart.name), cartsDatas, audioContext);
|
||||||
handle.pico8_state = {};
|
handle.pico8_state = {};
|
||||||
|
File diff suppressed because one or more lines are too long
37
src/client/routing.tsx
Normal file
37
src/client/routing.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Outlet, RouterProvider, ScrollRestoration, createBrowserRouter, redirect } from "react-router-dom"
|
||||||
|
import { HomePage } from "./HomePage";
|
||||||
|
import { GamePage } from "./GamePage";
|
||||||
|
|
||||||
|
const RouteRoot = () => {
|
||||||
|
return <>
|
||||||
|
{/* <Nav> */}
|
||||||
|
<ScrollRestoration />
|
||||||
|
<Outlet/>
|
||||||
|
{/* </Nav> */}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
element: <RouteRoot/>,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <HomePage/>,
|
||||||
|
// loader: () => {
|
||||||
|
// return redirect("/megachat");
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/u/:author/:slug",
|
||||||
|
element: <GamePage/>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/u/:author/:slug/:version",
|
||||||
|
element: <GamePage/>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const Routing = () => <RouterProvider router={router} />;
|
@ -24,7 +24,13 @@ server.register(fastifyStatic, {
|
|||||||
|
|
||||||
routeList.forEach(firRoute => {
|
routeList.forEach(firRoute => {
|
||||||
server.route(route(firRoute));
|
server.route(route(firRoute));
|
||||||
})
|
});
|
||||||
|
|
||||||
|
server.setNotFoundHandler((req, res) => {
|
||||||
|
if (!req.url.startsWith("/api")) {
|
||||||
|
res.sendFile('index.html');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Run the server!
|
// Run the server!
|
||||||
try {
|
try {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<script src="dist/index.js" type="module"></script>
|
<script src="/dist/index.js" type="module"></script>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--measure: 64ch;
|
--measure: 64ch;
|
||||||
|
@ -1120,7 +1120,22 @@ canvas{
|
|||||||
|
|
||||||
<!-- Add content below the cart here -->
|
<!-- Add content below the cart here -->
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Content here<br/>
|
||||||
|
Content here<br/>
|
||||||
|
Content here<br/>
|
||||||
|
Content here<br/>
|
||||||
|
Content here<br/>
|
||||||
|
Content here<br/>
|
||||||
|
Content here<br/>
|
||||||
|
Content here<br/>
|
||||||
|
Content here<br/>
|
||||||
|
Content here<br/>
|
||||||
|
Content here<br/>
|
||||||
|
Content here<br/>
|
||||||
|
Content here<br/>
|
||||||
|
<input type="text"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div> <!-- body_0 -->
|
</div> <!-- body_0 -->
|
||||||
|
Loading…
x
Reference in New Issue
Block a user