import * as Utils from "./utils"; import MakiObject from "./runtime/MakiObject"; import GuiObject from "./runtime/GuiObject"; import JsWinampAbstractionLayer from "./runtime/JsWinampAbstractionLayer"; import Layout from "./runtime/Layout"; import Layer from "./runtime/Layer"; import Container from "./runtime/Container"; import JsScript from "./runtime/JsScript"; import JsElements from "./runtime/JsElements"; import JsGammaSet from "./runtime/JsGammaSet"; import JsGroupDef from "./runtime/JsGroupDef"; import Group from "./runtime/Group"; import Button from "./runtime/Button"; import ToggleButton from "./runtime/ToggleButton"; import Text from "./runtime/Text"; import Status from "./runtime/Status"; import Slider from "./runtime/Slider"; import Vis from "./runtime/Vis"; import EqVis from "./runtime/EqVis"; import AnimatedLayer from "./runtime/AnimatedLayer"; import WindowHolder from "./runtime/WindowHolder"; async function prepareMakiImage(node, zip, file) { let { h, w } = node.attributes; // TODO: Escape file for regex const img = Utils.getCaseInsensitveFile(zip, file); if (img === undefined) { return {}; } const imgBlob = await img.async("blob"); const imgUrl = await Utils.getUrlFromBlob(imgBlob); if (w === undefined || h === undefined) { const { width, height } = await Utils.getSizeFromUrl(imgUrl); w = width; h = height; } return { h, w, imgUrl, }; } const noop = (node, parent) => new GuiObject(node, parent); const parsers = { groupdef: (node, parent) => new JsGroupDef(node, parent), skininfo: noop, guiobject: noop, version: noop, name: noop, comment: noop, syscmds: noop, author: noop, email: noop, homepage: noop, screenshot: noop, container: (node, parent) => new Container(node, parent), scripts: noop, gammaset: (node, parent) => new JsGammaSet(node, parent), color: noop, layer: (node, parent) => new Layer(node, parent), 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), sendparams: noop, elements: (node, parent) => new JsElements(node, parent), bitmap: noop, eqvis: (node, parent) => new EqVis(node, parent), slider: (node, parent) => new Slider(node, parent), gammagroup: noop, truetypefont: async (node, parent, zip) => { const { file } = node.attributes; const font = Utils.getCaseInsensitveFile(zip, file); if (!font) { console.warn(`Unable to find font file ${file}`); return new MakiObject(node, parent); } const fontBlob = await font.async("blob"); const fontUrl = await Utils.getUrlFromBlob(fontBlob); const fontFamily = `font-${Utils.getId()}-${file.replace(/\./, "_")}`; try { await Utils.loadFont(fontUrl, fontFamily); } catch { console.warn(`Failed to load font ${fontFamily}`); return new MakiObject(node, parent); } return new MakiObject(node, parent, { fontFamily }); }, component: (node, parent) => new WindowHolder(node, parent), text: (node, parent) => new Text(node, parent), togglebutton: (node, parent) => new ToggleButton(node, parent), status: (node, parent) => new Status(node, parent), bitmapfont: noop, vis: (node, parent) => new Vis(node, parent), "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: (node, parent) => new AnimatedLayer(node, parent), nstatesbutton: noop, songticker: noop, menu: noop, albumart: noop, playlistplus: noop, script: (node, parent) => new JsScript(node, parent), }; async function parseChildren(node, children, zip) { if (node.type === "comment") { return; } if (node.name == null) { console.error(node); throw new Error("Unknown node"); } const resolvedChildren = await Promise.all( children.map(async (child) => { if (child.type === "comment") { return; } if (child.type === "text") { // TODO: Handle text return new MakiObject({ ...child }, node, undefined); } if (child.name == null) { console.error(child); throw new Error("Unknown node"); } const childName = child.name.toLowerCase(); if (childName == null) { console.error(node); throw new Error("Unknown node"); } let childParser = parsers[childName]; if (childParser == null) { console.warn(`Missing parser in initialize for ${childName}`); childParser = noop; } const parsedChild = await childParser(child, node, zip); child.maki = parsedChild; if (child.children != null && child.children.length > 0) { await parseChildren(parsedChild, child.children, zip); } return parsedChild; }) ); // remove comments other trimmed nodes const filteredChildren = resolvedChildren.filter( (item) => item !== undefined ); node.js_addChildren(filteredChildren); } async function nodeImageLookup(node, root, zip) { const imageAttributes = Utils.imageAttributesFromNode(node); if (!imageAttributes || imageAttributes.length === 0) { return; } if (!node.attributes.js_assets) { node.attributes.js_assets = {}; } await Promise.all( imageAttributes.map(async (attribute) => { const image = node.attributes[attribute]; if (!image || !Utils.isString(image)) { return; } let img; if (image.endsWith(".png")) { img = await prepareMakiImage(node, zip, image); } else { const elementNode = Utils.findXmlElementById(node, image, root); if (elementNode) { img = await prepareMakiImage( elementNode, zip, elementNode.attributes.file ); const { x, y } = elementNode.attributes; img.x = x !== undefined ? x : 0; img.y = y !== undefined ? y : 0; } else { console.warn("Unable to find image:", image); } } node.attributes.js_assets[attribute.toLowerCase()] = img; }) ); } async function applyImageLookups(root, zip) { await Utils.asyncTreeFlatMap(root, async (node) => { await nodeImageLookup(node, root, zip); return node; }); } async function applyGroupDefs(root) { await Utils.asyncTreeFlatMap(root, async (node) => { switch (node.name) { case "group": { if (!node.children || node.children.length === 0) { const groupdef = node.js_groupdefLookup(node.attributes.id); if (!groupdef) { console.warn( "Unable to find groupdef. Rendering null", node.attributes.id ); return {}; } node.children = groupdef.children; // Do we need to copy the items instead of just changing the parent? node.children.forEach((item) => { item.parent = node; }); node.attributes = { ...node.attributes, ...groupdef.attributes, }; } return {}; } default: { return node; } } }); } async function initialize(zip, skinXml) { const xmlRoot = skinXml.children[0]; await applyImageLookups(xmlRoot, zip); const root = new JsWinampAbstractionLayer(xmlRoot, null, undefined); await parseChildren(root, xmlRoot.children, zip); await applyGroupDefs(root); return root; } export default initialize;