[WIP] Give Maki objects access to Redux state (#882)

* Share uids between maki objects and state tree

* Start to add xml tree
This commit is contained in:
Jordan Eldredge 2019-08-24 21:10:00 -07:00
parent a5cd2439d6
commit 82c6308b16
7 changed files with 120 additions and 59 deletions

View file

@ -35,14 +35,17 @@ export function gotSkinBlob(blob: Blob, store: ModernStore) {
export function gotSkinZip(zip: JSZip, store: ModernStore) {
return async dispatch => {
const xmlTree = await Utils.inlineIncludes(
const rawXmlTree = await Utils.inlineIncludes(
await Utils.readXml(zip, "skin.xml"),
zip
);
const xmlTree = Utils.mapTree(rawXmlTree, node => {
return { ...node, uid: Utils.getId() };
});
dispatch(setXmlTree(xmlTree));
const makiTree = await initialize(zip, xmlTree);
const makiTree = await initialize(zip, xmlTree, store);
// Execute scripts
await Utils.asyncTreeFlatMap(makiTree, node => {
switch (node.name) {

View file

@ -0,0 +1,14 @@
import * as Utils from "./utils";
function findNodeByUid(state, uid) {
return Utils.findInTree(state.xmlTree, node => {
return node.uid === uid;
});
}
export function getTop(uid) {
return state => {
const node = findNodeByUid(state, uid);
return Number(node.attributes.y) || 0;
};
}

View file

@ -19,10 +19,12 @@ import EqVis from "./runtime/EqVis";
import AnimatedLayer from "./runtime/AnimatedLayer";
import Component from "./runtime/Component";
const noop = (node, parent) => new GuiObject(node, parent);
const noop = (node, parent, zip, store) =>
new GuiObject(node, parent, undefined, store);
const parsers = {
groupdef: (node, parent) => new JsGroupDef(node, parent),
groupdef: (node, parent, zip, store) =>
new JsGroupDef(node, parent, undefined, store),
skininfo: noop,
guiobject: noop,
version: noop,
@ -33,19 +35,26 @@ const parsers = {
email: noop,
homepage: noop,
screenshot: noop,
container: (node, parent) => new Container(node, parent),
container: (node, parent, zip, store) =>
new Container(node, parent, undefined, store),
scripts: noop,
gammaset: (node, parent) => new JsGammaSet(node, parent),
gammaset: (node, parent, zip, store) =>
new JsGammaSet(node, parent, undefined, store),
color: noop,
layer: (node, parent) => new Layer(node, parent),
layer: (node, parent, zip, store) =>
new Layer(node, parent, undefined, store),
layoutstatus: noop,
hideobject: noop,
button: (node, parent) => new Button(node, parent),
group: (node, parent) => new Group(node, parent),
layout: (node, parent) => new Layout(node, parent),
button: (node, parent, zip, store) =>
new Button(node, parent, undefined, store),
group: (node, parent, zip, store) =>
new Group(node, parent, undefined, store),
layout: (node, parent, zip, store) =>
new Layout(node, parent, undefined, store),
sendparams: noop,
elements: (node, parent) => new JsElements(node, parent),
bitmap: async (node, parent, zip) => {
elements: (node, parent, zip, store) =>
new JsElements(node, parent, undefined, store),
bitmap: async (node, parent, zip, store) => {
let { h, w, x, y } = node.attributes;
const { file, gammagroup, id } = node.attributes;
// TODO: Escape file for regex
@ -63,27 +72,37 @@ const parsers = {
y = y !== undefined ? y : 0;
}
return new MakiObject(node, parent, {
id,
file,
gammagroup,
h,
w,
x,
y,
imgUrl,
});
return new MakiObject(
node,
parent,
{
id,
file,
gammagroup,
h,
w,
x,
y,
imgUrl,
},
store
);
},
eqvis: (node, parent) => new EqVis(node, parent),
slider: (node, parent) => new Slider(node, parent),
eqvis: (node, parent, zip, store) =>
new EqVis(node, parent, undefined, store),
slider: (node, parent, zip, store) =>
new Slider(node, parent, undefined, store),
gammagroup: noop,
truetypefont: noop,
component: (node, parent) => new Component(node, parent),
text: (node, parent) => new Text(node, parent),
togglebutton: (node, parent) => new ToggleButton(node, parent),
status: (node, parent) => new Status(node, parent),
component: (node, parent, zip, store) =>
new Component(node, parent, undefined, store),
text: (node, parent, zip, store) => new Text(node, parent, undefined, store),
togglebutton: (node, parent, zip, store) =>
new ToggleButton(node, parent, undefined, store),
status: (node, parent, zip, store) =>
new Status(node, parent, undefined, store),
bitmapfont: noop,
vis: (node, parent) => new Vis(node, parent),
vis: (node, parent, zip, store) => new Vis(node, parent, undefined, store),
"wasabi:titlebar": noop,
"colorthemes:list": noop,
"wasabi:standardframe:status": noop,
@ -95,19 +114,20 @@ const parsers = {
elementalias: noop,
grid: noop,
rect: noop,
animatedlayer: (node, parent) => new AnimatedLayer(node, parent),
animatedlayer: (node, parent, zip, store) =>
new AnimatedLayer(node, parent, undefined, store),
nstatesbutton: noop,
songticker: noop,
menu: noop,
albumart: noop,
playlistplus: noop,
async script(node, parent, zip) {
async script(node, parent, zip, store) {
const script = await Utils.readUint8array(zip, node.attributes.file);
return new MakiObject(node, parent, { script });
return new MakiObject(node, parent, { script }, store);
},
};
async function parseChildren(node, children, zip) {
async function parseChildren(node, children, zip, store) {
if (node.type === "comment") {
return;
}
@ -123,7 +143,7 @@ async function parseChildren(node, children, zip) {
}
if (child.type === "text") {
// TODO: Handle text
return new MakiObject({ ...child }, node);
return new MakiObject({ ...child }, node, undefined, store);
}
if (child.name == null) {
console.error(child);
@ -140,9 +160,9 @@ async function parseChildren(node, children, zip) {
console.warn(`Missing parser in initialize for ${childName}`);
childParser = noop;
}
const parsedChild = await childParser(child, node, zip);
const parsedChild = await childParser(child, node, zip, store);
if (child.children != null && child.children.length > 0) {
await parseChildren(parsedChild, child.children, zip);
await parseChildren(parsedChild, child.children, zip, store);
}
return parsedChild;
})
@ -185,10 +205,15 @@ async function applyGroupDefs(root) {
});
}
async function initialize(zip, skinXml) {
async function initialize(zip, skinXml, store) {
const xmlRoot = skinXml.children[0];
const root = new JsWinampAbstractionLayer(skinXml.children[0], null);
await parseChildren(root, xmlRoot.children, zip);
const root = new JsWinampAbstractionLayer(
skinXml.children[0],
null,
undefined,
store
);
await parseChildren(root, xmlRoot.children, zip, store);
await applyGroupDefs(root);
return root;
}

View file

@ -1,15 +1,7 @@
import { XmlTree } from "./types";
import * as Utils from "./utils";
function mapTree(node, cb) {
const children = node.children || [];
return cb({ ...node, children: children.map(child => mapTree(child, cb)) });
}
export default async function initializeStateTree(
xmlTree: XmlTree
): Promise<XmlTree> {
return mapTree(xmlTree, node => {
return { ...node, uid: Utils.getId() };
});
return xmlTree;
}

View file

@ -4,10 +4,11 @@ import {
findParentNodeOfType,
unimplementedWarning,
} from "../utils";
import * as MakiSelectors from "../MakiSelectors";
class GuiObject extends MakiObject {
constructor(node, parent) {
super(node, parent);
constructor(node, parent, annotations, store) {
super(node, parent, annotations, store);
this.visible = true;
}
@ -61,7 +62,14 @@ class GuiObject extends MakiObject {
}
gettop() {
return Number(this.attributes.y) || 0;
const stateTop = MakiSelectors.getTop(this._uid)(this._store.getState());
const makiTop = Number(this.attributes.y) || 0;
if (stateTop !== makiTop) {
console.error(
`Maki state ${makiTop} is out of sync with tree state ${stateTop}`
);
}
return makiTop;
}
getleft() {

View file

@ -1,17 +1,15 @@
import Emitter from "../Emitter";
import {
findElementById,
findGroupDefById,
unimplementedWarning,
} from "../utils";
import * as Utils from "../utils";
class MakiObject {
constructor(node, parent, annotations = {}, store) {
this._store = store;
if (node) {
this._uid = node.uid;
this.attributes = node.attributes || {};
this.name = node.name;
} else {
this._uid = Utils.getId();
// When dynamically creating an object with `new` we have no underlying node
this.attributes = {};
this.name = this.getclassname().toLowerCase();
@ -56,7 +54,7 @@ class MakiObject {
}
js_imageLookup(id) {
const element = findElementById(this, id);
const element = Utils.findElementById(this, id);
if (element) {
return element.js_annotations;
}
@ -65,7 +63,7 @@ class MakiObject {
}
js_groupdefLookup(id) {
const groupdef = findGroupDefById(this, id);
const groupdef = Utils.findGroupDefById(this, id);
if (groupdef) {
return groupdef;
}
@ -91,7 +89,7 @@ class MakiObject {
}
onnotify(command, param, a, b) {
unimplementedWarning("onnotify");
Utils.unimplementedWarning("onnotify");
return 0;
}
}

View file

@ -5,6 +5,12 @@ export function getId() {
return i++;
}
// Depth-first tree map
export function mapTree(node, cb) {
const children = node.children || [];
return cb({ ...node, children: children.map(child => mapTree(child, cb)) });
}
export function isPromise(obj) {
return obj && typeof obj.then === "function";
}
@ -114,6 +120,21 @@ export function unimplementedWarning(name) {
console.warn(`Executing unimplemented MAKI function: ${name}`);
}
// Bredth-first search in a tree
export function findInTree(node, predicate) {
if (predicate(node)) {
return node;
}
const children = node.children || [];
for (const child of children) {
const found = findInTree(child, predicate);
if (found != null) {
return found;
}
}
return null;
}
// Operations on trees
export function findParentNodeOfType(node, type) {
let n = node;