Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| daed4245fd | |||
| afea1ce949 | |||
| 6ad0ffe1c5 | |||
| e51df30a08 | |||
| 03e13075da | |||
| e76e4eb34c | |||
| cb14976221 | |||
| e6fede22f6 | |||
| 876404d8f5 | |||
| 07595c31ef | |||
| db2007e4b0 | |||
| a343f74821 | |||
| ac5f4d2c1e |
@@ -1 +1,2 @@
|
||||
@firebox:registry = "https://nodepack.playbox.link"
|
||||
@firebox:registry = "https://nodepack.playbox.link"
|
||||
@dylanpizzo:registry = "https://dylanpizzo.dev"
|
||||
+6
-6
@@ -1,15 +1,15 @@
|
||||
# Installs Node image
|
||||
FROM node:18-alpine as base
|
||||
FROM node:20-alpine as base
|
||||
|
||||
# sets the working directory for any RUN, CMD, COPY command
|
||||
WORKDIR /app
|
||||
|
||||
# Install Python and pip
|
||||
RUN apk add --update python3 py3-pip
|
||||
RUN python3 -m ensurepip
|
||||
RUN pip3 install --no-cache --upgrade pip setuptools
|
||||
# # Install Python and pip
|
||||
# RUN apk add --update python3 py3-pip
|
||||
# RUN python3 -m ensurepip
|
||||
# RUN pip3 install --no-cache --upgrade pip setuptools
|
||||
|
||||
COPY ./data ./data
|
||||
# COPY ./data ./data
|
||||
|
||||
# Copies stuff to cache for install
|
||||
COPY ./package.json ./package-lock.json tsconfig.json ./
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
# Firstack
|
||||
# Picobook
|
||||
|
||||
Firstack is a template repo for a tech stack. This stack includes
|
||||
|
||||
- [react](https://react.dev/)
|
||||
- [emotion](https://emotion.sh/)
|
||||
- [fastify](https://fastify.dev/)
|
||||
- [postgres](https://www.postgresql.org/)
|
||||
- [typescript](https://www.typescriptlang.org/)
|
||||
A website for hosting pico8 projects.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
Binary file not shown.
+14
-31
@@ -1,33 +1,16 @@
|
||||
version: '3.9'
|
||||
version: "3.9"
|
||||
services:
|
||||
app:
|
||||
container_name: picobook-app
|
||||
image: node
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: base
|
||||
env_file:
|
||||
.env
|
||||
ports:
|
||||
- ${PORT}:${PORT}
|
||||
depends_on:
|
||||
- db
|
||||
profiles: ["prod"]
|
||||
app:
|
||||
container_name: picobook-app
|
||||
image: node
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: base
|
||||
env_file: .env
|
||||
ports:
|
||||
- ${PORT}:${PORT}
|
||||
profiles: ["prod"]
|
||||
|
||||
db:
|
||||
container_name: picobook-postgres
|
||||
image: postgres
|
||||
env_file:
|
||||
.env
|
||||
ports:
|
||||
- '5432:${DB_PORT}'
|
||||
volumes:
|
||||
- data:/data/db
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME}
|
||||
profiles: ["dev", "prod"]
|
||||
|
||||
volumes:
|
||||
data: {}
|
||||
volumes:
|
||||
data: {}
|
||||
|
||||
Generated
+9268
-8217
File diff suppressed because it is too large
Load Diff
+60
-60
@@ -1,62 +1,62 @@
|
||||
{
|
||||
"name": "firstack",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Firstack is a template repo for a tech stack.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"makeuser": "npm run withenv ./scripts/makeuser.ts",
|
||||
"dev-docker": "docker compose --profile dev up -d",
|
||||
"dev-server": "echo \"starting server\" && npm run withenv ./src/server/index.ts",
|
||||
"dev-watch-client": "npm run withenv ./scripts/watch.ts",
|
||||
"dev-migrate": "source ./.env && pg-migrations apply --directory ./src/database/migrations",
|
||||
"prod-migrate": "pg-migrations apply --directory ./src/database/migrations",
|
||||
"prod-build-client": "npm run withenv ./scripts/build.ts",
|
||||
"prod-docker": "docker compose --profile prod up -d",
|
||||
"prod-start": "echo \"building frontend\" && npm run prod-build-client && echo \"${DATABASE_URL}\" && echo \"running migrations\" && npm run prod-migrate && echo \"starting server\" && npm run withenv ./src/server/index.ts",
|
||||
"withenv": "tsx ./scripts/run-with-env.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build-p8client": "bun build src/client/pico8-client/veryRawRenderCart.js --outdir src/client/pico8-client/build",
|
||||
"add-pico": "npm run withenv ./scripts/do-release.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.playbox.link/dylan/firstack.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@databases/pg": "^5.4.1",
|
||||
"@fastify/cookie": "^9.0.4",
|
||||
"@fastify/secure-session": "^7.1.0",
|
||||
"@fastify/static": "^6.10.2",
|
||||
"@fastify/websocket": "^10.0.1",
|
||||
"@firebox/components": "^0.1.5",
|
||||
"@firebox/tsutil": "^0.1.2",
|
||||
"@sinclair/typebox": "^0.31.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"execa": "^8.0.1",
|
||||
"fastify": "^4.26.2",
|
||||
"isomorphic-git": "^1.25.6",
|
||||
"react-pico-8": "^4.1.0",
|
||||
"react-router-dom": "^6.18.0",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@databases/pg-migrations": "^5.0.2",
|
||||
"@emotion/css": "^11.11.2",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/react": "^18.2.21",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@types/ws": "^8.5.10",
|
||||
"esbuild": "^0.19.2",
|
||||
"nodemon": "^3.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.10.1",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
"name": "picobook",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Picobook.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"makeuser": "npm run withenv ./scripts/makeuser.ts",
|
||||
"dev-docker": "docker compose --profile dev up -d",
|
||||
"dev-server": "echo \"starting server\" && npm run withenv ./src/server/index.ts",
|
||||
"dev-watch-client": "npm run withenv ./scripts/watch.ts",
|
||||
"dev-migrate": "source ./.env && pg-migrations apply --directory ./src/database/migrations",
|
||||
"prod-migrate": "pg-migrations apply --directory ./src/database/migrations",
|
||||
"prod-build-client": "npm run withenv ./scripts/build.ts",
|
||||
"prod-docker": "docker compose --profile prod up -d",
|
||||
"prod-start": "echo \"building frontend\" && npm run prod-build-client && echo \"starting server\" && npm run withenv ./src/server/index.ts",
|
||||
"withenv": "tsx ./scripts/run-with-env.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"add-pico": "npm run withenv ./scripts/do-release.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.playbox.link/dylan/firstack.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@athingperday/react-pico-player": "^0.1.1",
|
||||
"@databases/pg": "^5.4.1",
|
||||
"@fastify/cookie": "^9.0.4",
|
||||
"@fastify/secure-session": "^7.1.0",
|
||||
"@fastify/static": "^7.0.4",
|
||||
"@fastify/websocket": "^10.0.1",
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@sinclair/typebox": "^0.31.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"execa": "^8.0.1",
|
||||
"fastify": "^4.26.2",
|
||||
"isomorphic-git": "^1.25.6",
|
||||
"octokit": "^5.0.5",
|
||||
"react-pico-8": "^4.1.0",
|
||||
"react-router-dom": "^6.18.0",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@databases/pg-migrations": "^5.0.2",
|
||||
"@emotion/css": "^11.11.2",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/react": "^18.2.21",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@types/ws": "^8.5.10",
|
||||
"esbuild": "^0.19.2",
|
||||
"nodemon": "^3.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.10.1",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
+28
-36
@@ -1,15 +1,15 @@
|
||||
import { Link, useParams } from "react-router-dom"
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { DbRelease } from "../server/dbal/dbal";
|
||||
import { css } from "@emotion/css";
|
||||
// import { type Pico8Player } from "@athingperday/react-pico-player";
|
||||
|
||||
type Info = {
|
||||
author: string | null;
|
||||
games: {slug: string; releases: DbRelease[]}[];
|
||||
}
|
||||
author: string | null;
|
||||
games: string[];
|
||||
};
|
||||
|
||||
export const AuthorPage = () => {
|
||||
const {author} = useParams();
|
||||
const { author } = useParams();
|
||||
const [info, setInfo] = useState<Info | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -17,46 +17,38 @@ export const AuthorPage = () => {
|
||||
let url = `/api/author?author=${author}`;
|
||||
const information = await fetch(url);
|
||||
const json = await information.json();
|
||||
console.log('json', json);
|
||||
console.log("json", json);
|
||||
setInfo(json);
|
||||
}
|
||||
};
|
||||
fetchInfo();
|
||||
}, [setInfo, author]);
|
||||
|
||||
if (!info) {
|
||||
return (
|
||||
<div>
|
||||
LOADING...
|
||||
</div>
|
||||
)
|
||||
return <div>LOADING...</div>;
|
||||
}
|
||||
|
||||
if (!info.author) {
|
||||
return (
|
||||
<div>
|
||||
NOT FOUND
|
||||
</div>
|
||||
)
|
||||
return <div>NOT FOUND</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css`
|
||||
margin: auto;
|
||||
width: max-content;
|
||||
max-inline-size: 66ch;
|
||||
padding: 1.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
`}>
|
||||
<div
|
||||
className={css`
|
||||
margin: auto;
|
||||
width: max-content;
|
||||
max-inline-size: 66ch;
|
||||
padding: 1.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
`}
|
||||
>
|
||||
<h1>{author}</h1>
|
||||
{
|
||||
info.games.map(game => (
|
||||
<Link key={game.slug} to={`/u/${author}/${game.slug}`}>
|
||||
<h3>{game.releases[0].manifest.title ?? game.slug}</h3>
|
||||
</Link>
|
||||
))
|
||||
}
|
||||
{info.games.map((game) => (
|
||||
<Link key={game} to={`/u/${author}/${game}`}>
|
||||
<h3>{game}</h3>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
+117
-100
@@ -1,128 +1,145 @@
|
||||
import { Link, useParams, useSearchParams } from "react-router-dom"
|
||||
import { Pico8Console, Pico8ConsoleImperatives } from "./pico8-client/Pico8Console";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { DbRelease } from "../server/dbal/dbal";
|
||||
import { Link, useParams, useSearchParams } from "react-router-dom";
|
||||
import { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import { css } from "@emotion/css";
|
||||
import { useWebsocket } from "./hooks/useWebsocket";
|
||||
import { Pico8Player } from "@athingperday/react-pico-player";
|
||||
|
||||
type Info = {
|
||||
release: DbRelease | null;
|
||||
versions: string[];
|
||||
}
|
||||
type PicoPlayerHandle =
|
||||
NonNullable<
|
||||
Parameters<typeof Pico8Player>["0"]["consoleRef"]
|
||||
> extends MutableRefObject<infer T>
|
||||
? NonNullable<T>
|
||||
: never;
|
||||
|
||||
type Game = {
|
||||
carts: Parameters<typeof Pico8Player>["0"]["carts"];
|
||||
};
|
||||
|
||||
export const GamePage = () => {
|
||||
const {author, slug} = useParams();
|
||||
const { author, slug } = useParams();
|
||||
// const [text, setText] = useState("");
|
||||
const [prevGpio, setPrevGpio] = useState<number[] | null>(null);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const room = searchParams.get('room');
|
||||
const picoRef = useRef<Pico8ConsoleImperatives>(null);
|
||||
const room = searchParams.get("room");
|
||||
const picoRef = useRef<PicoPlayerHandle>(null);
|
||||
const socket = useWebsocket({
|
||||
url: `/api/ws/room?room=${room}`,
|
||||
// url: "wss://echo.websocket.org",
|
||||
onMessage({message}) {
|
||||
// const msg = message as any;
|
||||
// if (msg.type === "gpio") {
|
||||
// if (picoRef.current) {
|
||||
// const handle = picoRef.current.getPicoConsoleHandle();
|
||||
// if (handle) {
|
||||
// console.log("updating pico gpio");
|
||||
// (handle.gpio as any).dontSend = true;
|
||||
// handle.gpio.length = 0;
|
||||
// handle.gpio.push(...msg.gpio);
|
||||
// (handle.gpio as any).dontSend = false;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
console.log('message', message);
|
||||
}
|
||||
})
|
||||
// const version = searchParams.get('v');
|
||||
const [v, setVersion] = useState<string | null>(null);
|
||||
const [info, setInfo] = useState<Info | null>(null);
|
||||
|
||||
const version = v ?? info?.release?.version ?? info?.versions[0];
|
||||
onMessage({ message }) {
|
||||
// console.log("message", message);
|
||||
const msg = message as any;
|
||||
if (msg.getGpio) {
|
||||
if (picoRef.current) {
|
||||
const handle = picoRef.current;
|
||||
if (handle) {
|
||||
socket.sendMessage({ gpio: handle.gpio });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg.gpio) {
|
||||
if (picoRef.current) {
|
||||
const handle = picoRef.current;
|
||||
if (handle) {
|
||||
// console.log("updating pico gpio");
|
||||
handle.gpio.length = 0;
|
||||
handle.gpio.push(...msg.gpio);
|
||||
setPrevGpio([...handle.gpio]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
const [game, setGame] = useState<Game | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchInfo = async () => {
|
||||
let url = `/api/release?author=${author}&slug=${slug}`;
|
||||
if (version) {
|
||||
url += `&version=${version}`;
|
||||
}
|
||||
let url = `/api/game?author=${author}&name=${slug ?? ""}`;
|
||||
const information = await fetch(url);
|
||||
const json = await information.json();
|
||||
setInfo(json);
|
||||
}
|
||||
console.log(json);
|
||||
setGame(json);
|
||||
};
|
||||
fetchInfo();
|
||||
}, [setInfo, author, slug, version]);
|
||||
}, [setGame, slug]);
|
||||
|
||||
if (!info) {
|
||||
return (
|
||||
<div>
|
||||
LOADING...
|
||||
</div>
|
||||
)
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const handle = picoRef.current;
|
||||
if (!handle) {
|
||||
return;
|
||||
}
|
||||
if (JSON.stringify(handle.gpio) !== JSON.stringify(prevGpio)) {
|
||||
if (prevGpio) {
|
||||
setPrevGpio([...handle.gpio]);
|
||||
socket.sendMessage({ gpio: handle.gpio });
|
||||
} else {
|
||||
socket.sendMessage({ getGpio: true });
|
||||
setPrevGpio([...handle.gpio]);
|
||||
}
|
||||
}
|
||||
}, 1000 / 60);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
});
|
||||
|
||||
if (!game) {
|
||||
return <div>LOADING...</div>;
|
||||
}
|
||||
|
||||
if (!info.release) {
|
||||
return (
|
||||
<div>
|
||||
NOT FOUND
|
||||
</div>
|
||||
)
|
||||
if (!game.carts) {
|
||||
return <div>NOT FOUND</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<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 <Link to={`/u/${info.release.author}`}>{info.release.author}</Link></h2>
|
||||
</div>
|
||||
<div className={css`
|
||||
width: 512px;
|
||||
max-width: 100%;
|
||||
<div
|
||||
className={css`
|
||||
margin: auto;
|
||||
`}>
|
||||
<div className={css`
|
||||
border: 2px solid transparent;
|
||||
&:focus-within {
|
||||
border: 2px solid limegreen;
|
||||
}
|
||||
`}>
|
||||
<Pico8Console
|
||||
ref={picoRef}
|
||||
carts={info.release.carts}
|
||||
// onGpioChange={(gpio: number[]) => {
|
||||
// console.log("sending gpio");
|
||||
// socket.sendMessage({
|
||||
// type: "gpio",
|
||||
// gpio,
|
||||
// });
|
||||
// }}
|
||||
/>
|
||||
</div>
|
||||
<div className={css`
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
`}>
|
||||
Version: <select defaultValue={info.release.version} onChange={(ev) => setVersion(ev.target.value)}>
|
||||
{
|
||||
[...info.versions].reverse().map(v => (
|
||||
<option key={v} value={v}>{v}</option>
|
||||
))
|
||||
width: max-content;
|
||||
max-inline-size: 66ch;
|
||||
padding: 1.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
<h1>{slug}</h1>
|
||||
<h2>
|
||||
by <Link to={`/u/${author}`}>{author}</Link>
|
||||
</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;
|
||||
}
|
||||
</select>
|
||||
`}
|
||||
>
|
||||
<Pico8Player consoleRef={picoRef} carts={game.carts} />
|
||||
</div>
|
||||
</div>
|
||||
{/* <div>
|
||||
<input onChange={(x) => setText(x.target.value)} />
|
||||
<button
|
||||
onClick={() => {
|
||||
socket.sendMessage({ text });
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
+14
-2
@@ -1,3 +1,15 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { authors } from "../data/authors";
|
||||
|
||||
export const HomePage = () => {
|
||||
return <div>Welcome to Picobook!</div>
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h2>Welcome to Picobook!</h2>
|
||||
{authors.map((author) => (
|
||||
<Link key={author.username} to={`/u/${author.username}`}>
|
||||
<h3>{author.username}</h3>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { Pico8Console } from "./pico8-client/Pico8Console";
|
||||
import testcarts from "./testcarts";
|
||||
// import { css } from "@emotion/css";
|
||||
// import { Pico8Console } from "./pico8-client/Pico8Console";
|
||||
// import testcarts from "./testcarts";
|
||||
import { Routing } from "./routing";
|
||||
|
||||
const App = (props: {}) => {
|
||||
return (
|
||||
<Routing/>
|
||||
<Routing />
|
||||
// <div className={css`
|
||||
// min-height: 100vh;
|
||||
// `}>
|
||||
|
||||
@@ -1,46 +1,52 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
export const useWebsocket = (props: {url: string; onMessage: (stuff: {socket: WebSocket; message: unknown}) => void}) => {
|
||||
const {url, onMessage} = props;
|
||||
const onMessageRef = useRef(onMessage);
|
||||
const ws = useRef<WebSocket | null>(null);
|
||||
onMessageRef.current = onMessage;
|
||||
export const useWebsocket = (props: {
|
||||
url: string;
|
||||
onMessage: (stuff: { socket: WebSocket; message: unknown }) => void;
|
||||
}) => {
|
||||
const { url, onMessage } = props;
|
||||
const onMessageRef = useRef(onMessage);
|
||||
const ws = useRef<WebSocket | null>(null);
|
||||
onMessageRef.current = onMessage;
|
||||
|
||||
useEffect(() => {
|
||||
const webSocket = new WebSocket(url);
|
||||
useEffect(() => {
|
||||
const webSocket = new WebSocket(url);
|
||||
|
||||
webSocket.addEventListener("open", () => {
|
||||
console.log("WebSocket is open now.");
|
||||
});
|
||||
webSocket.addEventListener("open", () => {
|
||||
console.log("WebSocket is open now.");
|
||||
});
|
||||
|
||||
webSocket.addEventListener("message", (event: any) => {
|
||||
onMessageRef.current({socket: webSocket, message: JSON.parse(event.data)});
|
||||
});
|
||||
webSocket.addEventListener("message", (event: any) => {
|
||||
onMessageRef.current({
|
||||
socket: webSocket,
|
||||
message: JSON.parse(event.data),
|
||||
});
|
||||
});
|
||||
|
||||
webSocket.addEventListener("error", (event) => {
|
||||
console.log("WebSocket error: ", event);
|
||||
});
|
||||
console.log("WebSocket error: ", event);
|
||||
});
|
||||
|
||||
webSocket.addEventListener("close", () => {
|
||||
console.log("WebSocket is closed now.");
|
||||
ws.current = null;
|
||||
});
|
||||
webSocket.addEventListener("close", () => {
|
||||
console.log("WebSocket is closed now.");
|
||||
ws.current = null;
|
||||
});
|
||||
|
||||
ws.current = webSocket;
|
||||
ws.current = webSocket;
|
||||
|
||||
return () => {
|
||||
webSocket.close();
|
||||
};
|
||||
return () => {
|
||||
webSocket.close();
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
}, [url]);
|
||||
const sendMessage = useCallback((message: unknown) => {
|
||||
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||
ws.current.send(JSON.stringify(message));
|
||||
// console.log("sending", message);
|
||||
} else {
|
||||
console.error("WebSocket is not open. Message not sent.");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const sendMessage = useCallback((message: unknown) => {
|
||||
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||
ws.current.send(JSON.stringify(message));
|
||||
} else {
|
||||
console.error("WebSocket is not open. Message not sent.");
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {sendMessage};
|
||||
};
|
||||
return { sendMessage };
|
||||
};
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { PicoCart, PicoPlayerHandle, makePicoConsole } from "./renderCart";
|
||||
import { ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||
|
||||
export type Pico8ConsoleImperatives = {
|
||||
getPicoConsoleHandle(): PicoPlayerHandle | null;
|
||||
}
|
||||
|
||||
export type Pico8ConsoleProps = {
|
||||
carts: PicoCart[],
|
||||
// onGpioChange?(gpio: number[]): void,
|
||||
}
|
||||
|
||||
// const noop = () => {};
|
||||
|
||||
export const Pico8Console = forwardRef((props: Pico8ConsoleProps, forwardedRef: ForwardedRef<Pico8ConsoleImperatives>) => {
|
||||
const {
|
||||
carts,
|
||||
// onGpioChange = noop
|
||||
} = props;
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [handle, setHandle] = useState<PicoPlayerHandle | null>(null);
|
||||
const attachConsole = useCallback(async () => {
|
||||
const picoConsole = await makePicoConsole({
|
||||
carts,
|
||||
});
|
||||
picoConsole.canvas.tabIndex=0;
|
||||
if (ref.current) {
|
||||
ref.current.appendChild(picoConsole.canvas);
|
||||
|
||||
// Set the width and height because pico8 adds them as properties on chrome-based browsers
|
||||
picoConsole.canvas.style.width = "";
|
||||
picoConsole.canvas.style.height = "";
|
||||
|
||||
picoConsole.canvas.focus();
|
||||
}
|
||||
setHandle(picoConsole);
|
||||
// picoConsole.gpio.subscribe(onGpioChange);
|
||||
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]);
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
getPicoConsoleHandle() {
|
||||
return handle;
|
||||
}
|
||||
}), [handle]);
|
||||
useEffect(() => {
|
||||
if (playing) {
|
||||
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 (
|
||||
<div ref={ref} className={css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`}></div>
|
||||
);
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
export {makePicoConsole} from "./renderCart";
|
||||
export {pngToRom} from "./pngToRom";
|
||||
@@ -1,57 +0,0 @@
|
||||
// TODO: something is broken for the new mygame.p8.png
|
||||
const imageDataToRom = (imageData: ImageData) => {
|
||||
const width = imageData.width;
|
||||
const height = imageData.height;
|
||||
const data = []; // For raw cart data
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const index = (y * width + x) * 4;
|
||||
const r = imageData.data[index];
|
||||
const g = imageData.data[index + 1];
|
||||
const b = imageData.data[index + 2];
|
||||
const a = imageData.data[index + 3];
|
||||
|
||||
// Extracting and encoding the data from pixel components
|
||||
const byte = ((b & 3) << 0) | ((g & 3) << 2) | ((r & 3) << 4) | ((a & 3) << 6);
|
||||
data.push(byte);
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, `data` contains the raw bytes extracted from the image
|
||||
// Here, you would decode this data into a format representing the cart
|
||||
// This could involve uncompressing code, reading sprite data, etc.
|
||||
// The specifics depend on your cart format and how data was originally encoded
|
||||
|
||||
// Returning raw data for demonstration; you'll need to adapt this part
|
||||
return data.slice(0, 32768);
|
||||
// return data.slice(0, 32768);
|
||||
}
|
||||
|
||||
const pngGetImageData = (src: string) => {
|
||||
return new Promise<ImageData>((resolve, reject) => {
|
||||
try {
|
||||
const img = document.createElement('img');
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
img.onload = () => {
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||
resolve(imageData);
|
||||
};
|
||||
img.onerror = () => {
|
||||
reject(Error("BAD IMAGE"));
|
||||
}
|
||||
img.src = src;
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const pngToRom = async (src: string) => {
|
||||
return imageDataToRom(await pngGetImageData(src));
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// @ts-ignore
|
||||
import "./build/veryRawRenderCart.js";
|
||||
|
||||
export type PicoBool = 0 | 1;
|
||||
|
||||
export type RenderCart = (Module: {canvas: HTMLCanvasElement, codo_textarea?: HTMLTextAreaElement}, cartNames: string[], cartDatas: number[][], audioContext: AudioContext) => {
|
||||
p8_touch_detected?: PicoBool;
|
||||
p8_dropped_cart?: string;
|
||||
p8_dropped_cart_name?: string;
|
||||
pico8_state?: Partial<{
|
||||
frame_number: number;
|
||||
has_focus: PicoBool;
|
||||
is_paused: PicoBool;
|
||||
request_pointer_lock: PicoBool;
|
||||
require_page_navigate_confirmation: PicoBool;
|
||||
show_dpad: PicoBool;
|
||||
shutdown_requested: PicoBool;
|
||||
sound_volume: number;
|
||||
}>;
|
||||
pico8_buttons?: [number, number, number, number, number, number, number, number];
|
||||
pico8_gamepads?: {count: number};
|
||||
pico8_gpio?: number[]; // should be 128 length
|
||||
pico8_audio_context?: AudioContext;
|
||||
pico8_mouse?: [number, number, number];
|
||||
codo_command?: number;
|
||||
}
|
||||
|
||||
const typedRenderCart = (window as any).P8 as RenderCart;
|
||||
|
||||
export {typedRenderCart as renderCart}
|
||||
@@ -1,202 +0,0 @@
|
||||
import { assertNever } from "@firebox/tsutil";
|
||||
import { pngToRom } from "./pngToRom";
|
||||
import { RenderCart, renderCart as rawRenderCart } from "./rawRenderCart";
|
||||
|
||||
export type PicoCart = {
|
||||
name: string;
|
||||
src: string;
|
||||
} | {
|
||||
name: string;
|
||||
rom: number[];
|
||||
}
|
||||
|
||||
type PlayerButtons = {
|
||||
left: boolean;
|
||||
right: boolean;
|
||||
up: boolean;
|
||||
down: boolean;
|
||||
o: boolean;
|
||||
x: boolean;
|
||||
menu: boolean;
|
||||
}
|
||||
|
||||
export type PicoPlayerHandle = {
|
||||
raw: ReturnType<RenderCart>;
|
||||
rawModule: unknown;
|
||||
// external things
|
||||
readonly canvas: HTMLCanvasElement;
|
||||
|
||||
// i/o
|
||||
setButtons: (buttons: PlayerButtons[]) => void;
|
||||
setMouse: (mouse: {
|
||||
x: number;
|
||||
y: number;
|
||||
leftClick: boolean;
|
||||
rightClick: boolean;
|
||||
}) => void;
|
||||
setGamepadCount: (count: number) => void;
|
||||
gpio: (
|
||||
number[]
|
||||
// & {subscribe: (f: (gpio: number[]) => void) => void}
|
||||
); // read + write (should be 256-tuple)
|
||||
|
||||
// state
|
||||
readonly state: {
|
||||
frameNumber: number;
|
||||
isPaused: boolean;
|
||||
hasFocus: boolean;
|
||||
requestPointerLock: boolean;
|
||||
requirePageNavigateConfirmation: boolean;
|
||||
showDpad: boolean;
|
||||
shutdownRequested: boolean;
|
||||
soundVolume: number;
|
||||
};
|
||||
|
||||
// misc?
|
||||
setTouchDetected: (touchDetected: boolean) => void;
|
||||
dropCart: (cart: PicoCart) => void;
|
||||
|
||||
// Module
|
||||
toggleSound: () => void;
|
||||
toggleControlMenu: () => void;
|
||||
togglePaused: () => void;
|
||||
// TODO: rename these two better (what do they do??)
|
||||
modDragOver: () => void;
|
||||
modDragStop: () => void;
|
||||
}
|
||||
|
||||
const bitfield = (...args: boolean[]): number => {
|
||||
if (!args.length) {
|
||||
return 0;
|
||||
}
|
||||
return (args[0]?1:0)+2*bitfield(...args.slice(1));
|
||||
}
|
||||
|
||||
const getRom = async (cart: PicoCart) => {
|
||||
if ("src" in cart) {
|
||||
return await pngToRom(cart.src);
|
||||
} else if ("rom" in cart) {
|
||||
return cart.rom;
|
||||
}
|
||||
assertNever(cart);
|
||||
}
|
||||
|
||||
export const makePicoConsole = async (props: {
|
||||
canvas?: HTMLCanvasElement;
|
||||
codoTextarea?: HTMLTextAreaElement;
|
||||
audioContext?: AudioContext;
|
||||
carts: PicoCart[];
|
||||
}): Promise<PicoPlayerHandle> => {
|
||||
const {carts, canvas = document.createElement("canvas"), codoTextarea = document.createElement("textarea"), audioContext = new AudioContext()} = props;
|
||||
canvas.style.imageRendering = "pixelated";
|
||||
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 handle = rawRenderCart(Module, carts.map(cart => cart.name), cartsDatas, audioContext);
|
||||
handle.pico8_state = {};
|
||||
handle.pico8_buttons = [0,0,0,0,0,0,0,0];
|
||||
handle.pico8_mouse = [0,0,0];
|
||||
handle.pico8_gpio = [
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
];
|
||||
// let gpioChanged = (gpio: number[]) => {};
|
||||
// const gpioInner = [
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
// ];
|
||||
// handle.pico8_gpio = new Proxy(gpioInner, {
|
||||
// get(target, prop) {
|
||||
// return target[prop as any];
|
||||
// },
|
||||
// set(target, prop, newValue) {
|
||||
// const t = target as any;
|
||||
// if (t.setting) {
|
||||
// return false;
|
||||
// }
|
||||
// const prev = [...target];
|
||||
// target[prop as any] = newValue;
|
||||
// const next = [...target];
|
||||
// if (!t.dontSend && prev.some((p, i) => p !== next[i])) {
|
||||
// gpioChanged(target);
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
// });
|
||||
// (handle as any).pico8_gpio.subscribe = (f: (gpio: number[]) => void) => {
|
||||
// gpioChanged = f;
|
||||
// }
|
||||
handle.pico8_gamepads = {count: 0};
|
||||
return {
|
||||
raw: handle,
|
||||
rawModule: Module,
|
||||
canvas,
|
||||
state: {
|
||||
frameNumber: handle.pico8_state.frame_number!,
|
||||
isPaused: !!handle.pico8_state.is_paused!,
|
||||
hasFocus: !!handle.pico8_state.has_focus!,
|
||||
requestPointerLock: !!handle.pico8_state.request_pointer_lock!,
|
||||
requirePageNavigateConfirmation: !!handle.pico8_state.require_page_navigate_confirmation!,
|
||||
showDpad: !!handle.pico8_state.show_dpad!,
|
||||
shutdownRequested: !!handle.pico8_state.shutdown_requested!,
|
||||
soundVolume: handle.pico8_state.sound_volume!,
|
||||
},
|
||||
gpio: handle.pico8_gpio as PicoPlayerHandle["gpio"],
|
||||
setMouse({x, y, leftClick, rightClick}) {
|
||||
handle.pico8_mouse = [x, y, bitfield(leftClick, rightClick)];
|
||||
},
|
||||
setButtons(buttons) {
|
||||
// TODO: pad this properly here instead of casting
|
||||
handle.pico8_buttons = buttons.map(({left, right, up, down, o, x, menu}) => bitfield(left, right, up, down, o, x, menu)) as any;
|
||||
},
|
||||
setGamepadCount(count) {
|
||||
handle.pico8_gamepads = {count};
|
||||
},
|
||||
setTouchDetected(touchDetected) {
|
||||
handle.p8_touch_detected = touchDetected ? 1 : 0;
|
||||
},
|
||||
dropCart(cart) {
|
||||
handle.p8_dropped_cart_name = cart.name;
|
||||
// TODO: make sure this is a dataURL first, and if not, load it and then pass it in
|
||||
// handle.p8_dropped_cart = cart.src;
|
||||
// handle.codo_command = 9;
|
||||
},
|
||||
modDragOver: (Module as any).pico8DragOver,
|
||||
modDragStop: (Module as any).pico8DragStop,
|
||||
togglePaused: (Module as any).pico8TogglePaused,
|
||||
toggleSound: (Module as any).pico8ToggleSound,
|
||||
toggleControlMenu: (Module as any).pico8ToggleControlMenu,
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,16 @@
|
||||
export const authors = [
|
||||
{
|
||||
username: "dylanpizzo",
|
||||
repo: "pico-carts",
|
||||
auth: {
|
||||
pat: "f4a1ab7c30ecb6b68da44f5b3f231ab6+719a6bf4edff6d809d1eb6d3ba3c9eb5:a12f955e441c684193053075690f2aba161894c26ad62d6ef8e5fb71b1969cbe755333e1dc7fc90e339ccba2fae27bd82e2ebd1bb233956e09c1e0bf1b951d69a279b4731ca26f8315514524ba93c2228b02456081624db71c14fd4ba0",
|
||||
},
|
||||
},
|
||||
{
|
||||
username: "loupizzo",
|
||||
repo: "pico-carts",
|
||||
auth: {
|
||||
pat: "4827f847fdc737e5480748b4d5269bae+be1b031d650bfad15bbfef146d736c5e:3ac3c214ae4236bf89a317ba6b731b781e6e8dfa22c304adfe1ba6328cc497bc24a97f3389ce1a439c6956f2dfea900f4fba559ebe8ecb91b573bc897656c08d7f60372b69f3ee7310ae70eb75ee7bff9ce35e7c3c1b088e86e3e458a7",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,28 +0,0 @@
|
||||
import createConnectionPool, {ConnectionPool, ConnectionPoolConfig, sql} from '@databases/pg';
|
||||
|
||||
export {sql};
|
||||
|
||||
const portString = process.env["DB_PORT"];
|
||||
const portNumber = portString ? parseInt(portString) : undefined;
|
||||
|
||||
const clientConfig: ConnectionPoolConfig = {
|
||||
host: process.env["DB_HOST"],
|
||||
user: process.env["DB_USER"],
|
||||
database: process.env["DB_NAME"],
|
||||
password: process.env["DB_PASSWORD"],
|
||||
port: portNumber,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const db: ConnectionPool = createConnectionPool({
|
||||
connectionString: false,
|
||||
...clientConfig
|
||||
});
|
||||
|
||||
process.once('SIGTERM', () => {
|
||||
db.dispose().catch((ex) => {
|
||||
console.error(ex);
|
||||
});
|
||||
});
|
||||
|
||||
export {db};
|
||||
@@ -1,5 +0,0 @@
|
||||
CREATE TABLE users (
|
||||
id text,
|
||||
name text,
|
||||
password text
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
CREATE TABLE repos (
|
||||
id text,
|
||||
repo_fullname text, -- e.g. "username/reponame"
|
||||
repo_hosttype text, -- "github", "gitea", "gitlab", ...
|
||||
user_id text
|
||||
)
|
||||
@@ -1,7 +0,0 @@
|
||||
CREATE TABLE releases (
|
||||
id text,
|
||||
repo_id text,
|
||||
release_number integer,
|
||||
cart_png_base64 text,
|
||||
created_at time
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
DROP TABLE repos;
|
||||
|
||||
DROP TABLE releases;
|
||||
|
||||
CREATE TABLE releases (
|
||||
id text,
|
||||
repo text,
|
||||
author text,
|
||||
slug text,
|
||||
version text,
|
||||
carts json,
|
||||
manifest json,
|
||||
created_at time
|
||||
);
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE releases DROP COLUMN created_at;
|
||||
ALTER TABLE releases ADD created_at timestamp;
|
||||
+13
-6
@@ -1,11 +1,18 @@
|
||||
import type { RouteList } from "./routelist.ts"
|
||||
// import type { RouteList } from "./routelist.ts";
|
||||
|
||||
type RouteUrl = RouteList[number]["url"];
|
||||
// type RouteUrl = RouteList[number]["url"];
|
||||
|
||||
type HttpMethod = RouteList[number]["method"];
|
||||
// type HttpMethod = RouteList[number]["method"];
|
||||
|
||||
type Route<M extends HttpMethod, U extends RouteUrl> = Extract<RouteList[number], {url: U, method: M}>;
|
||||
// type Route<M extends HttpMethod, U extends RouteUrl> = Extract<
|
||||
// RouteList[number],
|
||||
// { url: U; method: M }
|
||||
// >;
|
||||
|
||||
export type RoutePayload<M extends HttpMethod, U extends RouteUrl> = Parameters<Route<M, U>["handler"]>[0]["payload"];
|
||||
// export type RoutePayload<M extends HttpMethod, U extends RouteUrl> = Parameters<
|
||||
// Route<M, U>["handler"]
|
||||
// >[0]["payload"];
|
||||
|
||||
export type RouteResponse<M extends HttpMethod, U extends RouteUrl> = Awaited<ReturnType<Route<M, U>["handler"]>>;
|
||||
// export type RouteResponse<M extends HttpMethod, U extends RouteUrl> = Awaited<
|
||||
// ReturnType<Route<M, U>["handler"]>
|
||||
// >;
|
||||
|
||||
+40
-10
@@ -1,29 +1,59 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { FirRouteInput, FirRouteOptions } from "../util/routewrap.ts";
|
||||
import { getAuthorGames, getReleases } from "../dbal/dbal.ts";
|
||||
import { authors } from "../../data/authors.ts";
|
||||
import { Octokit } from "octokit";
|
||||
import { decrypt } from "../util/crypt.ts";
|
||||
|
||||
const method = "GET";
|
||||
const url = "/api/author";
|
||||
|
||||
const payloadT = Type.Any();
|
||||
|
||||
const handler = async ({payload}: FirRouteInput<typeof payloadT>) => {
|
||||
const {author} = payload;
|
||||
const handler = async ({ payload }: FirRouteInput<typeof payloadT>) => {
|
||||
const { author } = payload;
|
||||
|
||||
if (typeof author !== "string") {
|
||||
const authorData = authors.find((x) => x.username === author);
|
||||
|
||||
if (!authorData) {
|
||||
return {
|
||||
author: null,
|
||||
releases: [],
|
||||
games: [],
|
||||
};
|
||||
}
|
||||
console.log("author", author);
|
||||
console.log("author", authorData);
|
||||
|
||||
const pat = decrypt({
|
||||
password: process.env.ENCRYPTION_PASSWORD!,
|
||||
cyphertext: authorData.auth.pat,
|
||||
});
|
||||
|
||||
const octokit = new Octokit({
|
||||
auth: pat,
|
||||
});
|
||||
|
||||
const gameStuff = await octokit.rest.repos.getContent({
|
||||
owner: authorData.username,
|
||||
repo: authorData.repo,
|
||||
path: ".picobook",
|
||||
});
|
||||
|
||||
// TODO: get the games.
|
||||
const games: string[] = [];
|
||||
|
||||
if (Array.isArray(gameStuff.data)) {
|
||||
games.push(
|
||||
...gameStuff.data
|
||||
.map((x) => x.name)
|
||||
.filter((x) => x.endsWith(".p8.png"))
|
||||
.map((x) => x.slice(0, -".p8.png".length)),
|
||||
);
|
||||
console.log("hi");
|
||||
}
|
||||
|
||||
const games = await getAuthorGames({author});
|
||||
|
||||
return {
|
||||
author,
|
||||
games,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -31,4 +61,4 @@ export default {
|
||||
url,
|
||||
payloadT,
|
||||
handler,
|
||||
} as const satisfies FirRouteOptions<typeof payloadT>;
|
||||
} as const satisfies FirRouteOptions<typeof payloadT>;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { FirRouteInput, FirRouteOptions } from "../util/routewrap.ts";
|
||||
import { authors } from "../../data/authors.ts";
|
||||
import { Octokit } from "octokit";
|
||||
import { decrypt } from "../util/crypt.ts";
|
||||
|
||||
const method = "GET";
|
||||
const url = "/api/game";
|
||||
|
||||
const payloadT = Type.Any();
|
||||
|
||||
const handler = async ({ payload }: FirRouteInput<typeof payloadT>) => {
|
||||
const { author, name } = payload;
|
||||
|
||||
const authorData = authors.find((x) => x.username === author);
|
||||
|
||||
if (!authorData) {
|
||||
return null;
|
||||
}
|
||||
console.log("author", authorData);
|
||||
|
||||
const pat = decrypt({
|
||||
password: process.env.ENCRYPTION_PASSWORD!,
|
||||
cyphertext: authorData.auth.pat,
|
||||
});
|
||||
|
||||
const octokit = new Octokit({
|
||||
auth: pat,
|
||||
});
|
||||
|
||||
const gameData = (
|
||||
await octokit.rest.repos.getContent({
|
||||
owner: authorData.username,
|
||||
repo: authorData.repo,
|
||||
path: `.picobook/${name}.p8.png`,
|
||||
})
|
||||
).data;
|
||||
|
||||
console.log(gameData);
|
||||
|
||||
if (Array.isArray(gameData)) {
|
||||
return null;
|
||||
}
|
||||
if (gameData.type !== "file") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
carts: [{ name, src: `data:image/png;base64,${gameData.content}` }],
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
method,
|
||||
url,
|
||||
payloadT,
|
||||
handler,
|
||||
} as const satisfies FirRouteOptions<typeof payloadT>;
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { FirRouteInput, FirRouteOptions } from "../util/routewrap.ts";
|
||||
import { getRelease, getReleases } from "../dbal/dbal.ts";
|
||||
|
||||
const method = "GET";
|
||||
const url = "/api/release";
|
||||
|
||||
const payloadT = Type.Any();
|
||||
|
||||
const handler = async ({payload}: FirRouteInput<typeof payloadT>) => {
|
||||
const {author, slug, version} = payload;
|
||||
|
||||
if (typeof author !== "string") {
|
||||
return {
|
||||
release: null,
|
||||
versions: [],
|
||||
};
|
||||
}
|
||||
if (typeof slug !== "string") {
|
||||
return {
|
||||
release: null,
|
||||
versions: [],
|
||||
};
|
||||
}
|
||||
|
||||
const release = await getRelease({author, slug, version});
|
||||
const releases = await getReleases({author, slug});
|
||||
|
||||
const versions = releases.map(r => r.version);
|
||||
|
||||
return {
|
||||
release,
|
||||
versions,
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
method,
|
||||
url,
|
||||
payloadT,
|
||||
handler,
|
||||
} as const satisfies FirRouteOptions<typeof payloadT>;
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { FirRouteInput, FirRouteOptions } from "../util/routewrap";
|
||||
import {git} from "../util/git.ts";
|
||||
import { randomUUID } from "crypto";
|
||||
import path from "path";
|
||||
import {fileURLToPath} from 'url';
|
||||
import { getCarts } from "../util/carts.ts";
|
||||
import { getRelease, insertRelease } from "../dbal/dbal.ts";
|
||||
import { ManifestType } from "../types.ts";
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const reposPath = path.resolve(__dirname, "..", "..", "..", "repos");
|
||||
|
||||
const method = "POST";
|
||||
const url = "/api/release";
|
||||
|
||||
const payloadT = Type.Any();
|
||||
|
||||
const handler = async ({payload}: FirRouteInput<typeof payloadT>) => {
|
||||
const {manifest, token} = payload;
|
||||
|
||||
if (!ManifestType.Check(manifest)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const release = await getRelease({author: manifest.author, slug: manifest.id, version: manifest.version});
|
||||
if (release) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uuid = randomUUID();
|
||||
const repoPath = path.join(reposPath, uuid);
|
||||
|
||||
await git.clone({
|
||||
from: manifest.repo,
|
||||
to: repoPath,
|
||||
auth: token,
|
||||
});
|
||||
|
||||
const carts = await getCarts(repoPath, manifest.carts);
|
||||
|
||||
await insertRelease({
|
||||
manifest,
|
||||
carts,
|
||||
});
|
||||
|
||||
console.log({
|
||||
manifest,
|
||||
carts,
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default {
|
||||
method,
|
||||
url,
|
||||
payloadT,
|
||||
handler,
|
||||
} as const satisfies FirRouteOptions<typeof payloadT>;
|
||||
@@ -1,122 +0,0 @@
|
||||
// Database Access Layer stuff goes here
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { db, sql } from "../../database/db";
|
||||
import { PicobookManifest } from '../types';
|
||||
|
||||
export type DbRelease = {
|
||||
id: string;
|
||||
slug: string;
|
||||
repo: string;
|
||||
version: string;
|
||||
carts: {name: string; rom: number[]}[];
|
||||
author: string;
|
||||
manifest: PicobookManifest;
|
||||
}
|
||||
|
||||
export type DbReleaseInternal = {
|
||||
id: string;
|
||||
slug: string;
|
||||
repo: string;
|
||||
version: string;
|
||||
carts: {carts: {name: string; rom: number[]}[]};
|
||||
author: string;
|
||||
manifest: PicobookManifest;
|
||||
}
|
||||
|
||||
const compareVersions = (a: string, b: string) => {
|
||||
const [a1, a2] = a.split(".").map(x => Number(x));
|
||||
const [b1, b2] = b.split(".").map(x => Number(x));
|
||||
if (a1 !== b1) {
|
||||
return a1 - b1;
|
||||
} else {
|
||||
return a2 - b2;
|
||||
}
|
||||
}
|
||||
|
||||
const compareByVersion = (a: DbRelease, b: DbRelease) => compareVersions(a.version, b.version);
|
||||
|
||||
export const getReleases = async (where: {
|
||||
author: string;
|
||||
slug?: string;
|
||||
version?: string;
|
||||
}): Promise<DbRelease[]> => {
|
||||
const {author, slug, version} = where;
|
||||
let rows: DbReleaseInternal[];
|
||||
if (!slug) {
|
||||
rows = await db.query(sql`
|
||||
SELECT * from releases
|
||||
WHERE
|
||||
author = ${author}
|
||||
`);
|
||||
} else if (!version) {
|
||||
rows = await db.query(sql`
|
||||
SELECT * from releases
|
||||
WHERE
|
||||
slug = ${slug} AND
|
||||
author = ${author}
|
||||
`);
|
||||
} else {
|
||||
rows = await db.query(sql`
|
||||
SELECT * from releases
|
||||
WHERE
|
||||
slug = ${slug} AND
|
||||
author = ${author} AND
|
||||
version = ${version}
|
||||
`);
|
||||
}
|
||||
return rows.map(row => ({...row, carts: row.carts.carts}));
|
||||
}
|
||||
|
||||
export const getRelease = async (where: {
|
||||
author: string;
|
||||
slug: string;
|
||||
version?: string;
|
||||
}) => {
|
||||
const {version} = where;
|
||||
const releases = await getReleases(where);
|
||||
if (version) {
|
||||
if (releases.length === 1) {
|
||||
return releases[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
if (releases.length < 1) {
|
||||
return null;
|
||||
}
|
||||
releases.sort(compareByVersion);
|
||||
return releases[releases.length-1];
|
||||
}
|
||||
}
|
||||
|
||||
export const getAuthorGames = async (where: {
|
||||
author: string;
|
||||
}) => {
|
||||
const releases = await getReleases(where);
|
||||
const games = releases.reduce((accum, curr) => {
|
||||
const found = accum.find(r => r.slug === curr.slug);
|
||||
if (found) {
|
||||
found.releases.push(curr);
|
||||
} else {
|
||||
accum.push({slug: curr.slug, releases: [curr]});
|
||||
}
|
||||
return accum;
|
||||
}, [] as {slug: string; releases: DbRelease[]}[]);
|
||||
games.forEach(game => {
|
||||
game.releases.sort(compareByVersion).reverse();
|
||||
});
|
||||
return games;
|
||||
}
|
||||
|
||||
export const insertRelease = async (props: {manifest: PicobookManifest, carts: {name: string; rom: number[]}[]}) => {
|
||||
const {manifest, carts} = props;
|
||||
const {id: slug, author, repo, version} = manifest;
|
||||
const id = uuidv4();
|
||||
const now = new Date();
|
||||
await db.query(sql`
|
||||
INSERT INTO releases (id, slug, repo, version, author, carts, manifest, created_at)
|
||||
VALUES (${id}, ${slug}, ${repo}, ${version}, ${author}, ${{carts}}, ${manifest}, ${now})
|
||||
`);
|
||||
return id;
|
||||
}
|
||||
+15
-21
@@ -1,37 +1,28 @@
|
||||
// Import the framework and instantiate it
|
||||
import Fastify from 'fastify'
|
||||
import fastifyStatic from '@fastify/static'
|
||||
import {fastifyWebsocket} from '@fastify/websocket';
|
||||
import Fastify from "fastify";
|
||||
import fastifyStatic from "@fastify/static";
|
||||
import { fastifyWebsocket } from "@fastify/websocket";
|
||||
import { routeList } from "./routelist.ts";
|
||||
import { attachRoute } from "./util/routewrap.ts";
|
||||
import { git } from './util/git.ts';
|
||||
import path from "path";
|
||||
import {fileURLToPath} from 'url';
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
await git.clone({
|
||||
from: "https://github.com/thisismypassport/shrinko8",
|
||||
to: path.resolve(__dirname, "shrinko8"),
|
||||
});
|
||||
|
||||
const server = Fastify({
|
||||
logger: true
|
||||
logger: true,
|
||||
});
|
||||
|
||||
server.register(fastifyWebsocket);
|
||||
|
||||
server.register(fastifyStatic, {
|
||||
root: new URL('public', import.meta.url).toString().slice("file://".length),
|
||||
prefix: '/',
|
||||
root: new URL("public", import.meta.url).toString().slice("file://".length),
|
||||
prefix: "/",
|
||||
});
|
||||
|
||||
routeList.forEach(firRoute => {
|
||||
routeList.forEach((firRoute) => {
|
||||
attachRoute(server, firRoute);
|
||||
});
|
||||
|
||||
server.setNotFoundHandler((req, res) => {
|
||||
if (!req.url.startsWith("/api")) {
|
||||
res.sendFile('index.html');
|
||||
res.sendFile("index.html");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -40,8 +31,11 @@ try {
|
||||
// Note: host needs to be 0.0.0.0 rather than omitted or localhost, otherwise
|
||||
// it always returns an empty reply when used inside docker...
|
||||
// See: https://github.com/fastify/fastify/issues/935
|
||||
await server.listen({ port: parseInt(process.env["PORT"] ?? "3000"), host: "0.0.0.0" })
|
||||
await server.listen({
|
||||
port: parseInt(process.env["PORT"] ?? "3000"),
|
||||
host: "0.0.0.0",
|
||||
});
|
||||
} catch (err) {
|
||||
server.log.error(err)
|
||||
process.exit(1)
|
||||
}
|
||||
server.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,14 @@
|
||||
footer {
|
||||
max-inline-size: none;
|
||||
}
|
||||
|
||||
a,
|
||||
a:hover,
|
||||
a:focus,
|
||||
a:active,
|
||||
a:visited {
|
||||
color: lime;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
File diff suppressed because one or more lines are too long
+1276
-1047
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,57 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>PICO-8 Cartridge</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<style>
|
||||
.cart-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cart-container > canvas {
|
||||
width: 512px;
|
||||
max-width: calc(100% - 2px);
|
||||
border: 1px solid white;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="padding:0px; margin:0px; background-color:#222; color:#ccc">
|
||||
<div id="container" class="cart-container"></div>
|
||||
<button id="start-button">Click</button>
|
||||
<script type="module">
|
||||
import {makePicoConsole, pngToRom} from "./dist/index.js";
|
||||
import mygame from "./mygamefull.js";
|
||||
// const arrayToHex = (a) => {
|
||||
// const h = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];
|
||||
// return a.map(n => h[Math.floor(n/16)]+h[n%16]).join("");
|
||||
// };
|
||||
const hexToArray = (hex) => {
|
||||
const h = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];
|
||||
const a = [];
|
||||
for (let i = 0; i < hex.length; i+=2) {
|
||||
a.push(16*h.indexOf(hex[i])+h.indexOf(hex[i+1]));
|
||||
}
|
||||
return a;
|
||||
};
|
||||
mygame.carts[0] = ({name: mygame.carts[0].name, rom: hexToArray(mygame.carts[0].hex)});
|
||||
// const mainRom = mygame.carts[0].rom;
|
||||
// const pngRom = await pngToRom("mygame.p8.png");
|
||||
// pngRom.forEach((v,i) => {
|
||||
// if (v !== mainRom[i]) {
|
||||
// console.log(i, Math.floor(i/160), i%160, v, mainRom[i]);
|
||||
// }
|
||||
// })
|
||||
// console.log(arrayToHex(await pngToRom("secondcart.p8.png")));
|
||||
async function start() {
|
||||
const console1 = await makePicoConsole({carts: mygame.carts});
|
||||
console.log(console1);
|
||||
document.getElementById("container").appendChild(console1.canvas);
|
||||
}
|
||||
|
||||
document.getElementById("start-button").addEventListener("click", start);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+3
-11
@@ -1,17 +1,9 @@
|
||||
import echo from "./api/echo.ts";
|
||||
import getAuthor from "./api/getAuthor.ts";
|
||||
import getRelease from "./api/getRelease.ts";
|
||||
import release from "./api/release.ts";
|
||||
import getGame from "./api/getGame.ts";
|
||||
import room from "./api/room.ts";
|
||||
import webhook from "./api/webhook.ts";
|
||||
|
||||
export const routeList = [
|
||||
echo,
|
||||
webhook,
|
||||
release,
|
||||
getRelease,
|
||||
getAuthor,
|
||||
room,
|
||||
];
|
||||
export const routeList = [echo, webhook, getAuthor, room, getGame];
|
||||
|
||||
export type RouteList = typeof routeList;
|
||||
export type RouteList = typeof routeList;
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { shrinko8 } from "./shrinko8";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const outputDirPath = path.resolve(__dirname, "..", "..", "..", "output");
|
||||
|
||||
const getRom = async (inputFile: string) => {
|
||||
const uuid = randomUUID();
|
||||
const dir = path.join(outputDirPath, uuid);
|
||||
fs.mkdirSync(dir, {recursive: true});
|
||||
const outputFile = path.join(dir, "output.js");
|
||||
|
||||
await shrinko8({
|
||||
inputFile,
|
||||
outputFile,
|
||||
});
|
||||
|
||||
const js = await fs.promises.readFile(outputFile, "utf8");
|
||||
|
||||
await fs.promises.rm(dir, {recursive: true, force: true});
|
||||
|
||||
const match = js.match(/\b_cartdat\s*=\s*(\[.*?\])/s);
|
||||
if (!match) {
|
||||
console.log("BEGIN js contents --------------------------------------------");
|
||||
console.log(js);
|
||||
console.log("END js contents ----------------------------------------------");
|
||||
throw Error("Could not properly parse js file to find _cartdat");
|
||||
}
|
||||
|
||||
return JSON.parse(match[1]) as number[]
|
||||
}
|
||||
|
||||
const getCart = async (baseDir: string, inputFile: string) => {
|
||||
return {
|
||||
name: inputFile,
|
||||
rom: await getRom(path.join(baseDir, inputFile)),
|
||||
}
|
||||
}
|
||||
|
||||
export const getCarts = async (baseDir: string, inputFiles: string[]) => {
|
||||
return await Promise.all(inputFiles.map(inputFile => getCart(baseDir, inputFile)));
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
// Key derivation from password
|
||||
const algorithm = "aes-256-gcm";
|
||||
// TODO: make salt vary by use case;
|
||||
const salt = "saltysalt";
|
||||
|
||||
export const encrypt = (props: { password: string; text: string }) => {
|
||||
const { password, text } = props;
|
||||
const key = new Uint8Array(crypto.scryptSync(password, salt, 32)); // 32 bytes key
|
||||
const iv = new Uint8Array(crypto.randomBytes(16)); // Initialization Vector
|
||||
// Encryption
|
||||
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
||||
let encrypted = cipher.update(text, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
const authTag = cipher.getAuthTag().toString("hex");
|
||||
const fullEncrypted =
|
||||
authTag + "+" + Buffer.from(iv).toString("hex") + ":" + encrypted; // Store IV with encrypted data
|
||||
// console.log({ iv, authTag });
|
||||
return fullEncrypted;
|
||||
};
|
||||
|
||||
export const decrypt = (props: { password: string; cyphertext: string }) => {
|
||||
const { password, cyphertext } = props;
|
||||
const key = new Uint8Array(crypto.scryptSync(password, salt, 32)); // 32 bytes key
|
||||
// Decryption
|
||||
const [pre, encryptedHex] = cyphertext.split(":");
|
||||
const [authTag, ivHex] = pre.split("+");
|
||||
const ivFromStorage = new Uint8Array(Buffer.from(ivHex, "hex"));
|
||||
// console.log({ ivFromStorage, authTag });
|
||||
const decipher = crypto.createDecipheriv(algorithm, key, ivFromStorage);
|
||||
decipher.setAuthTag(new Uint8Array(Buffer.from(authTag, "hex")));
|
||||
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
return decrypted;
|
||||
};
|
||||
|
||||
// console.log(process.env.ENCRYPTION_PASSWORD);
|
||||
|
||||
// const x = encrypt({
|
||||
// password: process.env.ENCRYPTION_PASSWORD!,
|
||||
// text: "",
|
||||
// });
|
||||
|
||||
// const y = decrypt({
|
||||
// password: process.env.ENCRYPTION_PASSWORD!,
|
||||
// cyphertext: "",
|
||||
// });
|
||||
|
||||
// console.log(x);
|
||||
// console.log(y);
|
||||
@@ -1,27 +0,0 @@
|
||||
import fs from "fs";
|
||||
import isogit from "isomorphic-git";
|
||||
import http from "isomorphic-git/http/node";
|
||||
|
||||
const clone = async (options: {
|
||||
from: string;
|
||||
to: string;
|
||||
auth?: string;
|
||||
}) => {
|
||||
fs.mkdirSync(options.to, {recursive: true});
|
||||
await isogit.clone({
|
||||
fs,
|
||||
http,
|
||||
onAuth() {
|
||||
return {
|
||||
username: 'x-access-token',
|
||||
password: options.auth,
|
||||
}
|
||||
},
|
||||
dir: options.to,
|
||||
url: options.from,
|
||||
});
|
||||
}
|
||||
|
||||
export const git = {
|
||||
clone,
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {fileURLToPath} from 'url';
|
||||
import {execFile} from "child_process";
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
const picoDirPath = path.resolve(__dirname, "..", "..", "..", "pico8");
|
||||
const picoBinPath = path.resolve(picoDirPath, "pico8_dyn");
|
||||
|
||||
const cmd = (cmd: string, args: string[], options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(cmd, args, options, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject({error, stderr});
|
||||
} else {
|
||||
resolve({stdout});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const execPico = async (args: string[]) => {
|
||||
return await cmd(picoBinPath, args);
|
||||
}
|
||||
|
||||
export const pico8 = {
|
||||
async export(filesIn: string[], fileOut: string) {
|
||||
try {
|
||||
// console.log((await cmd("ls", ["-la", "/app/pico8"]) as any).stdout)
|
||||
return await execPico([...filesIn, "-export", fileOut]);
|
||||
} catch (err) {
|
||||
console.log("CAUGHT ERROR", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const result = await pico8.export(["/home/dylan/.lexaloffle/pico-8/carts/my-pico-project/mygame.p8","/home/dylan/.lexaloffle/pico-8/carts/my-pico-project/secondcart.p8"], "/home/dylan/repos/picobook/sample2.js");
|
||||
// console.log(result);
|
||||
@@ -1,19 +0,0 @@
|
||||
import { execa } from "execa";
|
||||
import path from "path";
|
||||
import {fileURLToPath} from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
const shrinko8DirPath = path.resolve(__dirname, "../shrinko8");
|
||||
const shrinko8Path = path.resolve(shrinko8DirPath, "shrinko8.py");
|
||||
const pico8DatPath = path.resolve(__dirname, "../../../data/pico8.dat");
|
||||
|
||||
export const shrinko8 = async (props: {
|
||||
inputFile: string;
|
||||
outputFile: string;
|
||||
options?: string[];
|
||||
}) => {
|
||||
const {inputFile, outputFile, options = []} = props;
|
||||
return await execa("python3", [shrinko8Path, inputFile, outputFile, "--pico8-dat", pico8DatPath, ...options])
|
||||
}
|
||||
|
||||
+5
-5
@@ -3,16 +3,16 @@
|
||||
"target": "es2017",
|
||||
"module": "es2022",
|
||||
"lib": ["DOM"],
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"strict": true
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true,
|
||||
"experimentalSpecifierResolution": "node",
|
||||
}
|
||||
}
|
||||
"experimentalSpecifierResolution": "node"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user