working on initialize and linking groupdefs (#820)

* move images to initialize

* return nodes from initialize, lookup groupdefs

* fix attributes when copying groupdefs

* position groups with x/y

* more parsers for classic modern skin

* if img doesn't exist just return the node without adding to registry

* dont need nested node

* simplify ids

* simplify splitValues

* new variable with name
This commit is contained in:
jberg 2019-07-31 16:44:07 -07:00 committed by Jordan Eldredge
parent aa97783ca0
commit 91cc5767ef
2 changed files with 222 additions and 134 deletions

View file

@ -33,77 +33,20 @@ const IGNORE_IDS = new Set([
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/simple.wal");
const resp = await fetch(process.env.PUBLIC_URL + "/skins/CornerAmp_Redux.wal");
const blob = await resp.blob();
const zip = null;
const registry = { scripts: [] };
await initialize(registry, blob);
console.log("registry: ", registry);
throw new Error("done");
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.asyncTreeFlatMap(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;
});
const { nodes, registry } = await initialize(zip, skinXml);
// 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];
return [skinXml, nodes, registry];
}
function Layout({
@ -219,11 +162,24 @@ function Button({ id, image, action, x, y, downImage, tooltip, children }) {
}
function ToggleButton(props) {
return <Button {...props} />;
return <Button data-node-type="togglebutton" {...props} />;
}
function GroupDef(props) {
return <div {...props} />;
function Group(props) {
const { id, children, x, y} = props;
const style = {
position: "absolute",
};
if (x !== undefined) {
style.left = Number(x);
}
if (y !== undefined) {
style.top = Number(y);
}
return <div
data-node-type="group"
data-node-id={id}
style={style}>{children}</div>;
}
const NODE_NAME_TO_COMPONENT = {
@ -231,26 +187,27 @@ const NODE_NAME_TO_COMPONENT = {
layer: Layer,
button: Button,
togglebutton: ToggleButton,
groupef: GroupDef,
group: Group,
};
// Given a skin XML node, pick which component to use, and render it.
function XmlNode({ node }) {
const attributes = node.attributes;
const name = node.name;
if (attributes && IGNORE_IDS.has(attributes.id)) {
return null;
}
if (node.name == null) {
if (name == null) {
// This is likely a comment
return null;
}
const Component = NODE_NAME_TO_COMPONENT[node.name];
const Component = NODE_NAME_TO_COMPONENT[name];
const childNodes = node.children || [];
const children = childNodes.map((childNode, i) => (
<XmlNode key={i} node={childNode} />
<XmlNode key={i} parent={node} node={childNode} />
));
if (Component == null) {
console.warn("Unknown node type", node.name);
console.warn("Unknown node type", name);
if (childNodes.length) {
return <>{children}</>;
}
@ -267,16 +224,11 @@ function App() {
if (data == null) {
return <h1>Loading...</h1>;
}
const [skinXml, images] = data;
const [skinXml, nodes, registry] = data;
return (
<SkinContext.Provider value={images}>
<SkinContext.Provider value={registry.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]
}
node={nodes}
/>
</SkinContext.Provider>
);

View file

@ -1,8 +1,27 @@
import * as Utils from "./utils";
import JSZip from "jszip";
function splitValues(str) {
return str.split(",").map(parseFloat);
}
async function loadImage(imgUrl) {
return await new Promise(resolve => {
const img = new Image();
img.addEventListener("load", function() {
resolve(img);
});
img.src = imgUrl;
});
}
let idCount = 0;
function getId() {
return '_' + idCount++;
}
const schema = {
groupdef: [
"scripts",
"layer",
"layoutstatus",
"hideobject",
@ -18,6 +37,39 @@ const schema = {
"wasabi:button",
"text",
"vis",
"grid",
"rect",
"animatedlayer",
"nstatesbutton",
"togglebutton",
"songticker",
"menu",
"status",
"albumart",
"playlistplus",
],
group: [
"button",
"layer",
"text",
"vis",
"group",
"scripts",
"layoutstatus",
"hideobject",
"wasabi:titlebar",
"menu",
"nstatesbutton",
"status",
"script",
"songticker",
"grid",
"animatedlayer",
"togglebutton",
"slider",
"rect",
"eqvis",
"playlistplus",
],
layout: [
"wasabi:standardframe:status",
@ -29,10 +81,16 @@ const schema = {
"status",
"slider",
"group",
"sendparams",
"script",
"grid",
"vis",
"rect",
"component",
],
container: ["groupdef", "layout", "scripts"],
scripts: ["script"],
elements: ["color", "bitmap", "bitmapfont", "truetypefont"],
elements: ["color", "bitmap", "bitmapfont", "truetypefont", "cursor", "elementalias"],
skininfo: [
"version",
"name",
@ -42,7 +100,7 @@ const schema = {
"homepage",
"screenshot",
],
wasabixml: ["skininfo", "scripts", "elements", "groupdef", "container"],
wasabixml: ["skininfo", "scripts", "elements", "groupdef", "container", "gammaset"],
// same as above, wa3 vs wa5
winampabstractionlayer: [
"skininfo",
@ -50,59 +108,131 @@ const schema = {
"elements",
"groupdef",
"container",
"gammaset",
"accelerators",
],
gammaset: ["gammagroup"],
accelerators: ["accelerator"],
};
const noop = (node) => node;
const parsers = {
groupdef(node, parent, registry, zip) {},
skininfo: () => {},
version: () => {},
name: () => {},
comment: () => {},
author: () => {},
email: () => {},
homepage: () => {},
screenshot: () => {},
container: () => {},
scripts: () => {},
gammaset: () => {},
color: () => {},
layer: () => {},
layoutstatus: () => {},
hideobject: () => {},
button: () => {},
group: () => {},
layout: () => {},
sendparams: () => {},
elements: () => {},
bitmap: () => {},
eqvis: () => {},
slider: () => {},
gammagroup: () => {},
truetypefont: () => {},
component: () => {},
text: () => {},
layer: () => {},
button: () => {},
togglebutton: () => {},
status: () => {},
slider: () => {},
bitmapfont: () => {},
vis: () => {},
"wasabi:titlebar": () => {},
"colorthemes:list": () => {},
"wasabi:standardframe:status": () => {},
"wasabi:standardframe:nostatus": () => {},
"wasabi:button": () => {},
groupdef: (node, parent, registry) => {
const attributeId = node.attributes.id;
registry.groupdefs[attributeId] = node;
return node;
},
skininfo: noop,
version: noop,
name: noop,
comment: noop,
author: noop,
email: noop,
homepage: noop,
screenshot: noop,
container: noop,
scripts: noop,
gammaset: (node, parent, registry) => {
const gammaId = node.attributes.id;
if (!registry.gammasets.hasOwnProperty(gammaId)) {
registry.gammasets[gammaId] = {};
}
return node;
},
color: noop,
layer: noop,
layoutstatus: noop,
hideobject: noop,
button: noop,
group: (node, parent, registry) => {
if (!node.children || node.children.length === 0) {
const groupdef = registry.groupdefs[node.attributes.id];
if (groupdef) {
return {
...node,
...groupdef,
attributes: { ...node.attributes, ...groupdef.attributes },
name: "group",
};
}
}
return node;
},
layout: noop,
sendparams: noop,
elements: noop,
bitmap: async (node, parent, registry, zip) => {
let { file, gammagroup, h, id, w, x, y } = node.attributes;
// TODO: Escape file for regex
const img = Utils.getCaseInsensitveFile(zip, file);
if (img === undefined) {
return node;
}
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;
}
registry.images[id.toLowerCase()] = { file, gammagroup, h, w, x, y, imgUrl };
return node;
},
eqvis: noop,
slider: noop,
gammagroup: (node, parent, registry) => {
const gammaId = parent.attributes.id;
const attributeId = node.attributes.id;
const attributeValues = splitValues(node.attributes.value);
registry.gammasets[gammaId][attributeId] = attributeValues;
return node;
},
truetypefont: noop,
component: noop,
text: noop,
layer: noop,
button: noop,
togglebutton: noop,
status: noop,
slider: noop,
bitmapfont: noop,
vis: noop,
"wasabi:titlebar": noop,
"colorthemes:list": noop,
"wasabi:standardframe:status": noop,
"wasabi:standardframe:nostatus": noop,
"wasabi:button": noop,
accelerators: noop,
accelerator: noop,
cursor: noop,
elementalias: noop,
grid: noop,
rect: noop,
animatedlayer: noop,
nstatesbutton: noop,
songticker: noop,
menu: noop,
status: noop,
albumart: noop,
playlistplus: noop,
async script(node, parent, registry, zip) {
const { id, file, param } = node.attributes;
const script = await Utils.readUint8array(zip, file);
registry.scripts.push({ parent, id, param, script });
return { ...node, script, param };
},
};
async function parseChildren(node, parsedParent, registry, zip) {
async function parseChildren(node, registry, zip) {
if (node.type === "comment") {
return;
}
@ -112,14 +242,14 @@ async function parseChildren(node, parsedParent, registry, zip) {
}
const validChildren = new Set(schema[node.name.toLowerCase()]);
await Promise.all(
const resolvedChildren = await Promise.all(
node.children.map(async child => {
if (child.type === "comment") {
return;
}
if (child.type === "text") {
// TODO: Handle text
return;
return { ...child, id: getId() };
}
if (child.name == null) {
console.error(child);
@ -132,7 +262,6 @@ async function parseChildren(node, parsedParent, registry, zip) {
}
if (!validChildren.has(childName)) {
debugger;
throw new Error(`Invalid child of a ${node.name}: ${childName}`);
}
@ -142,21 +271,28 @@ async function parseChildren(node, parsedParent, registry, zip) {
return;
}
const parsedChild = await childParser(child, node, registry, zip);
if (child.children != null) {
await parseChildren(child, parsedChild, registry, zip);
const returnNode = { ...parsedChild, id: getId() };
if (parsedChild.children != null) {
const parsedChildren = await parseChildren(parsedChild, registry, zip);
returnNode.children = parsedChildren.children;
}
return returnNode;
})
);
// remove comments other trimmed nodes
const filteredChildren = resolvedChildren.filter(item => item !== undefined);
return {
...node,
children: filteredChildren
};
}
async function initialize(registry, skinBlob) {
const zip = await JSZip.loadAsync(skinBlob);
const skinXml = await Utils.inlineIncludes(
await Utils.readXml(zip, "skin.xml"),
zip
);
await parseChildren(skinXml.children[0], null, registry, zip);
async function initialize(zip, skinXml) {
const registry = { scripts: [], gammasets: {}, images: {}, groupdefs: {} };
const nodes = await parseChildren(skinXml.children[0], registry, zip);
nodes.id = getId();
return { nodes, registry };
}
export default initialize;