mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-23 18:17:38 +00:00
280 lines
7 KiB
JavaScript
280 lines
7 KiB
JavaScript
import React from "react";
|
|
import JSZip from "jszip";
|
|
import "./App.css";
|
|
import * as Utils from "./utils";
|
|
// const runtime = require("./maki-interpreter/runtime");
|
|
// const System = require("./maki-interpreter/runtime/System");
|
|
// const interpret = require("./maki-interpreter/interpreter");
|
|
|
|
const IGNORE_IDS = new Set([
|
|
// The maki script shows/hides these depending on which corner you are in
|
|
"main2",
|
|
"main3",
|
|
"main4",
|
|
"Title2",
|
|
"Title3",
|
|
"Title4",
|
|
"mask2",
|
|
"mask3",
|
|
"mask4",
|
|
"placeholder", // This looks like something related to an EQ window
|
|
"Scaler",
|
|
"toggle",
|
|
"presets",
|
|
"auto",
|
|
// These need the maki script to get sized
|
|
"volumethumb",
|
|
"seekfull",
|
|
"seek1",
|
|
"Repeat",
|
|
]);
|
|
|
|
const SkinContext = React.createContext(null);
|
|
|
|
async function loadImage (imgUrl) {
|
|
return await new Promise(resolve => {
|
|
const img = new Image();
|
|
img.addEventListener("load", function() {
|
|
resolve(img);
|
|
});
|
|
img.src = imgUrl;
|
|
});
|
|
}
|
|
|
|
async function getSkin() {
|
|
const resp = await fetch(
|
|
process.env.PUBLIC_URL + "/skins/CornerAmp_Redux.wal"
|
|
);
|
|
const blob = await resp.blob();
|
|
const zip = await JSZip.loadAsync(blob);
|
|
const skinXml = await Utils.inlineIncludes(
|
|
await Utils.readXml(zip, "skin.xml"),
|
|
zip
|
|
);
|
|
|
|
// const system = new System();
|
|
|
|
const images = {};
|
|
await Utils.asyncTreeMap(skinXml, async node => {
|
|
// TODO: This is probalby only valid if in an `<elements>` node
|
|
switch (node.name) {
|
|
case "bitmap": {
|
|
let { file, gammagroup, h, id, w, x, y } = node.attributes;
|
|
// TODO: Escape file for regex
|
|
const img = Utils.getCaseInsensitveFile(zip, file);
|
|
const imgBlob = await img.async("blob");
|
|
const imgUrl = URL.createObjectURL(imgBlob);
|
|
if (w === undefined || h === undefined) {
|
|
const image = await loadImage(imgUrl);
|
|
w = image.width;
|
|
h = image.height;
|
|
x = (x !== undefined) ? x : 0;
|
|
y = (y !== undefined) ? y : 0;
|
|
}
|
|
images[id.toLowerCase()] = { file, gammagroup, h, w, x, y, imgUrl };
|
|
break;
|
|
}
|
|
case "truetypefont": {
|
|
//console.log(element);
|
|
break;
|
|
}
|
|
case "script": {
|
|
const { file, param } = node.attributes;
|
|
if (!file.endsWith("standardframe.maki")) {
|
|
break;
|
|
}
|
|
const scriptFile = Utils.getCaseInsensitveFile(zip, file);
|
|
const data = await scriptFile.async("uint8array");
|
|
// interpret({ data, system, runtime, log: true });
|
|
console.log(data);
|
|
break;
|
|
}
|
|
default: {
|
|
// console.error(`Unknown node ${node.name}`);
|
|
}
|
|
}
|
|
return node;
|
|
});
|
|
|
|
// Gross hack returing a tuple here. We're just doing some crazy stuff to get
|
|
// some data returned in the laziest way possible
|
|
return [skinXml, images];
|
|
}
|
|
|
|
function Layout({
|
|
id,
|
|
background,
|
|
desktopalpha,
|
|
drawBackground,
|
|
minimum_h,
|
|
maximum_h,
|
|
minimum_w,
|
|
maximum_w,
|
|
droptarget,
|
|
children,
|
|
}) {
|
|
const data = React.useContext(SkinContext);
|
|
if (background == null) {
|
|
console.warn("Got a Layout without a background. Rendering null", id);
|
|
return null;
|
|
}
|
|
|
|
const image = data[background];
|
|
return (
|
|
<>
|
|
<img
|
|
data-node-type="layout"
|
|
data-node-id={id}
|
|
src={image.imgUrl}
|
|
style={{
|
|
minWidth: Number(minimum_w),
|
|
minHeight: Number(minimum_h),
|
|
maxWidth: Number(maximum_w),
|
|
maxHeight: Number(maximum_h),
|
|
position: "absolute",
|
|
}}
|
|
/>
|
|
{children}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function Layer({ id, image, children, x, y }) {
|
|
const data = React.useContext(SkinContext);
|
|
if (image == null) {
|
|
console.warn("Got an Layer without an image. Rendering null", id);
|
|
return null;
|
|
}
|
|
const img = data[image.toLowerCase()];
|
|
if (img == null) {
|
|
console.warn("Unable to find image to render. Rendering null", image);
|
|
return null;
|
|
}
|
|
const params = {};
|
|
if (x !== undefined) {
|
|
params.left = Number(x);
|
|
}
|
|
if (y !== undefined) {
|
|
params.top = Number(y);
|
|
}
|
|
return (
|
|
<>
|
|
<img
|
|
data-node-type="Layer"
|
|
data-node-id={id}
|
|
src={img.imgUrl}
|
|
style={Object.assign({ position: "absolute" }, params)}
|
|
/>
|
|
{children}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function Button({ id, image, action, x, y, downImage, tooltip, children }) {
|
|
const data = React.useContext(SkinContext);
|
|
const [down, setDown] = React.useState(false);
|
|
const imgId = down && downImage ? downImage : image;
|
|
if (imgId == null) {
|
|
console.warn("Got a Button without a imgId. Rendering null", id);
|
|
return null;
|
|
}
|
|
// TODO: These seem to be switching too fast
|
|
const img = data[imgId.toLowerCase()];
|
|
if (img == null) {
|
|
console.warn("Unable to find image to render. Rendering null", image);
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
data-node-type="button"
|
|
data-node-id={id}
|
|
onMouseDown={e => {
|
|
setDown(true);
|
|
document.addEventListener("mouseup", () => {
|
|
// TODO: This could be unmounted
|
|
setDown(false);
|
|
});
|
|
}}
|
|
title={tooltip}
|
|
style={{
|
|
position: "absolute",
|
|
top: Number(y),
|
|
left: Number(x),
|
|
backgroundPositionX: -Number(img.x),
|
|
backgroundPositionY: -Number(img.y),
|
|
width: Number(img.w),
|
|
height: Number(img.h),
|
|
backgroundImage: `url(${img.imgUrl})`,
|
|
}}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ToggleButton(props) {
|
|
return <Button {...props} />;
|
|
}
|
|
|
|
function GroupDef(props) {
|
|
return <div {...props} />;
|
|
}
|
|
|
|
const NODE_NAME_TO_COMPONENT = {
|
|
layout: Layout,
|
|
layer: Layer,
|
|
button: Button,
|
|
togglebutton: ToggleButton,
|
|
groupef: GroupDef,
|
|
};
|
|
|
|
// Given a skin XML node, pick which component to use, and render it.
|
|
function XmlNode({ node }) {
|
|
const attributes = node.attributes;
|
|
if (attributes && IGNORE_IDS.has(attributes.id)) {
|
|
return null;
|
|
}
|
|
if (node.name == null) {
|
|
// This is likely a comment
|
|
return null;
|
|
}
|
|
const Component = NODE_NAME_TO_COMPONENT[node.name];
|
|
const childNodes = node.children || [];
|
|
const children = childNodes.map((childNode, i) => (
|
|
<XmlNode key={i} node={childNode} />
|
|
));
|
|
if (Component == null) {
|
|
console.warn("Unknown node type", node.name);
|
|
if (childNodes.length) {
|
|
return <>{children}</>;
|
|
}
|
|
return null;
|
|
}
|
|
return <Component {...attributes}>{children}</Component>;
|
|
}
|
|
|
|
function App() {
|
|
const [data, setData] = React.useState(null);
|
|
React.useEffect(() => {
|
|
getSkin().then(setData);
|
|
}, []);
|
|
if (data == null) {
|
|
return <h1>Loading...</h1>;
|
|
}
|
|
const [skinXml, images] = data;
|
|
return (
|
|
<SkinContext.Provider value={images}>
|
|
<XmlNode
|
|
node={
|
|
// TODO: This is not quite right. Really we should only be rendering the
|
|
// portion of the XML that is actually view code.
|
|
// For now we just render the whole thing and ignore whatever we don't recognize
|
|
skinXml.children[0]
|
|
}
|
|
/>
|
|
</SkinContext.Provider>
|
|
);
|
|
}
|
|
|
|
export default App;
|