diff --git a/packages/webamp-modern-2/assets/WinampModern566.wal b/packages/webamp-modern-2/assets/WinampModern566.wal new file mode 100644 index 00000000..87599e22 Binary files /dev/null and b/packages/webamp-modern-2/assets/WinampModern566.wal differ diff --git a/packages/webamp-modern-2/snowpack.config.js b/packages/webamp-modern-2/snowpack.config.js index a1cc060c..dd222416 100644 --- a/packages/webamp-modern-2/snowpack.config.js +++ b/packages/webamp-modern-2/snowpack.config.js @@ -8,6 +8,9 @@ module.exports = { src: "/", assets: "/assets", }, + exclude: [ + '*.tmp' + ], plugins: [ /* ... */ ], diff --git a/packages/webamp-modern-2/src/UIRoot.ts b/packages/webamp-modern-2/src/UIRoot.ts index 98410518..b2925234 100644 --- a/packages/webamp-modern-2/src/UIRoot.ts +++ b/packages/webamp-modern-2/src/UIRoot.ts @@ -1,7 +1,14 @@ import Bitmap from "./skin/Bitmap"; +import JSZip, { JSZipObject } from "jszip"; import { XmlElement } from "@rgrove/parse-xml"; import TrueTypeFont from "./skin/TrueTypeFont"; -import { assert, assume, findLast } from "./utils"; +import { + assert, + assume, + findLast, + getCaseInsensitiveFile, + removeAllChildNodes, +} from "./utils"; import BitmapFont from "./skin/BitmapFont"; import Color from "./skin/Color"; import GammaGroup from "./skin/GammaGroup"; @@ -9,9 +16,14 @@ import Container from "./skin/makiClasses/Container"; import Vm from "./skin/VM"; import BaseObject from "./skin/makiClasses/BaseObject"; import AUDIO_PLAYER, { AudioPlayer } from "./skin/AudioPlayer"; +import SystemObject from "./skin/makiClasses/SystemObject"; +import ComponentBucket from "./skin/makiClasses/ComponentBucket"; +import GroupXFade from "./skin/makiClasses/GroupXFade"; +import SkinParser from "./skin/parse"; export class UIRoot { _div: HTMLDivElement = document.createElement("div"); + _parser: SkinParser; // Just a temporary place to stash things _bitmaps: Bitmap[] = []; _fonts: (TrueTypeFont | BitmapFont)[] = []; @@ -22,14 +34,25 @@ export class UIRoot { _dummyGammaGroup: GammaGroup = null; _activeGammaSetName: string = ""; _xuiElements: XmlElement[] = []; - _activeGammaSet: GammaGroup[] | null = null; + _activeGammaSet: GammaGroup[] = []; _containers: Container[] = []; + _systemObjects: SystemObject[] = []; + _buckets: { [wndType: string]: ComponentBucket } = {}; + _bucketEntries: { [wndType: string]: XmlElement[] } = {}; + _xFades: GroupXFade[] = []; // A list of all objects created for this skin. _objects: BaseObject[] = []; vm: Vm = new Vm(); audio: AudioPlayer = AUDIO_PLAYER; + getFileAsString: (filePath: string) => Promise; + getFileAsBytes: (filePath: string) => Promise; + getFileAsBlob: (filePath: string) => Promise; + + constructor() { + this._parser = new SkinParser(this); + } reset() { this.dispose(); @@ -39,9 +62,14 @@ export class UIRoot { this._groupDefs = []; this._gammaSets = new Map(); this._xuiElements = []; - this._activeGammaSet = null; + this._activeGammaSet = []; this._containers = []; + this._systemObjects = []; this._gammaNames = {}; + this._buckets = {} + this._bucketEntries = {} + this._xFades = [] + removeAllChildNodes(this._div); // A list of all objects created for this skin. this._objects = []; @@ -67,7 +95,7 @@ export class UIRoot { (bitmap) => bitmap._id.toLowerCase() === lowercaseId ); - assert(found != null, `Could not find bitmap with id ${id}.`); + assume(found != null, `Could not find bitmap with id ${id}.`); return found; } @@ -86,7 +114,7 @@ export class UIRoot { (color) => color._id.toLowerCase() === lowercaseId ); - assert(found != null, `Could not find color with id ${id}.`); + assume(found != null, `Could not find color with id ${id}.`); return found; } @@ -102,6 +130,29 @@ export class UIRoot { return found ?? null; } + addComponentBucket(windowType: string, bucket: ComponentBucket) { + this._buckets[windowType] = bucket; + } + getComponentBucket(windowType: string): ComponentBucket { + return this._buckets[windowType]; + } + addBucketEntry(windowType: string, entry: XmlElement) { + if (!this._bucketEntries[windowType]) { + this._bucketEntries[windowType] = []; + } + this._bucketEntries[windowType].push(entry); + } + getBucketEntries(windowType: string): XmlElement[] { + return this._bucketEntries[windowType] || [] + } + + addXFade(xfade: GroupXFade){ + this._xFades.push(xfade) + } + getXFades(): GroupXFade[] { + return this._xFades; + } + addGroupDef(groupDef: XmlElement) { this._groupDefs.push(groupDef); if (groupDef.attributes.xuitag) { @@ -110,6 +161,7 @@ export class UIRoot { } getGroupDef(id: string): XmlElement | null { + if (!id) return null; const lowercaseId = id.toLowerCase(); const found = findLast( this._groupDefs, @@ -138,22 +190,27 @@ export class UIRoot { this._gammaSets.set(lower, gammaSet); } - enableGammaSet(id: string) { - const found = this._gammaSets.get(id.toLowerCase()); - assume( - found != null, - `Could not find gammaset for id "${id}" from set of ${Array.from( - this._gammaSets.keys() - ).join(", ")}` - ); - this._activeGammaSetName = id; - this._activeGammaSet = found; + enableGammaSet(id: string | null) { + if (id) { + const found = this._gammaSets.get(id.toLowerCase()); + assume( + found != null, + `Could not find gammaset for id "${id}" from set of ${Array.from( + this._gammaSets.keys() + ).join(", ")}` + ); + this._activeGammaSetName = id; + this._activeGammaSet = found; + } this._setCssVars(); } enableDefaultGammaSet() { - this._activeGammaSet = Array.from(this._gammaSets.values())[0] ?? null; - this._setCssVars(); + // TODO: restore the latest gammaSet picked by user for this skin + const gammaSetNames = Array.from(this._gammaSets.keys()); + const firstName = gammaSetNames[0]; + const antiBoring = gammaSetNames[1]; + this.enableGammaSet(antiBoring || firstName || null); } _getGammaGroup(id: string): GammaGroup | null { @@ -164,7 +221,7 @@ export class UIRoot { const found = findLast(this._activeGammaSet, (gammaGroup) => { return gammaGroup.getId().toLowerCase() === lower; }); - return found ?? null; + return found ?? this._getGammaGroupDummy(); } _getGammaGroupDummy() { @@ -181,7 +238,10 @@ export class UIRoot { _setCssVars() { const cssRules = []; - for (const bitmap of this._bitmaps) { + const bitmapFonts: BitmapFont[] = this._fonts.filter( + (font) => font instanceof BitmapFont && !font.useExternalBitmap() + ) as BitmapFont[]; + for (const bitmap of [...this._bitmaps, ...bitmapFonts]) { const img = bitmap.getImg(); if (!img) { console.warn(`Bitmap/font ${bitmap.getId()} has no img!`); @@ -198,6 +258,12 @@ export class UIRoot { ); cssRules.push(` ${bitmap.getCSSVar()}: url(${url});`); } + for (const color of this._colors) { + const groupId = color.getGammaGroup(); + const gammaGroup = this._getGammaGroup(groupId); + const url = gammaGroup.transformColor(color.getValue()); + cssRules.push(` ${color.getCSSVar()}: ${url};`); + } cssRules.unshift(":root{"); cssRules.push("}"); const cssEl = document.getElementById("bitmap-css"); @@ -237,6 +303,9 @@ export class UIRoot { case "toggle": this.toggleContainer(param); break; + case "close": + this.closeContainer(); + break; default: assume(false, `Unknown global action: ${action}`); } @@ -248,6 +317,17 @@ export class UIRoot { container.toggle(); } + closeContainer() { + const btn = document.activeElement; + const containerEl = btn.closest("container"); + const container_id = containerEl.getAttribute("id").toLowerCase(); + for (const container of this._containers) { + if (container._id.toLowerCase() == container_id) { + container.close(); + } + } + } + draw() { this._div.setAttribute("id", "ui-root"); this._div.style.imageRendering = "pixelated"; @@ -263,6 +343,87 @@ export class UIRoot { obj.dispose(); } } + + //? Zip things ======================== + /* because maki need to load a groupdef outside init() */ + _zip: JSZip; + + setZip(zip: JSZip) { + this._zip = zip; + if (zip != null) { + this.getFileAsString = this.getFileAsStringZip; + this.getFileAsBytes = this.getFileAsBytesZip; + this.getFileAsBlob = this.getFileAsBlobZip; + } else { + this.getFileAsString = this.getFileAsStringPath; + this.getFileAsBytes = this.getFileAsBytesPath; + this.getFileAsBlob = this.getFileAsBlobPath; + } + } + + //? Path things ======================== + /* needed to avoid direct fetch to root path */ + _skinPath: string; + + setSkinDir(skinPath: string) { + // required to end with slash/ + this._skinPath = skinPath; + } + + async getFileAsStringZip(filePath: string): Promise { + if (!filePath) return null; + const zipObj = getCaseInsensitiveFile(this._zip, filePath); + if (!zipObj) return null; + return await zipObj.async("string"); + } + + async getFileAsBytesZip(filePath: string): Promise { + if (!filePath) return null; + const zipObj = getCaseInsensitiveFile(this._zip, filePath); + if (!zipObj) return null; + return await zipObj.async("arraybuffer"); + } + + async getFileAsBlobZip(filePath: string): Promise { + if (!filePath) return null; + const zipObj = getCaseInsensitiveFile(this._zip, filePath); + if (!zipObj) return null; + return await zipObj.async("blob"); + } + + async getFileAsStringPath(filePath: string): Promise { + const response = await fetch(this._skinPath + filePath); + return await response.text(); + } + + async getFileAsBytesPath(filePath: string): Promise { + const response = await fetch(this._skinPath + filePath); + return await response.arrayBuffer(); + } + + async getFileAsBlobPath(filePath: string): Promise { + const response = await fetch(this._skinPath + filePath); + return await response.blob(); + } + + getFileIsExist(filePath: string): boolean { + const zipObj = getCaseInsensitiveFile(this._zip, filePath); + return !!zipObj; + } + + //? System things ======================== + /* because maki need to be run if not inside any Group @init() */ + addSystemObject(systemObj: SystemObject) { + this._systemObjects.push(systemObj); + } + init() { + for (const systemObject of this._systemObjects) { + systemObject.init(); + } + } + getId() { + return "UIROOT"; + } } // Global Singleton for now diff --git a/packages/webamp-modern-2/src/clip_path.html b/packages/webamp-modern-2/src/clip_path.html index 633bf8cd..c0924df4 100644 --- a/packages/webamp-modern-2/src/clip_path.html +++ b/packages/webamp-modern-2/src/clip_path.html @@ -20,9 +20,10 @@ click me! +-> - + - + + diff --git a/packages/webamp-modern-2/src/clip_path.ts b/packages/webamp-modern-2/src/clip_path.ts index 8bf61b9c..6d1ab918 100644 --- a/packages/webamp-modern-2/src/clip_path.ts +++ b/packages/webamp-modern-2/src/clip_path.ts @@ -10,9 +10,10 @@ document.getElementById("img1").onclick = (event) => { function main() { const oriImg = document.getElementById("img1"); + const preferedWidth = parseInt(oriImg.getAttribute("data-width")); + const preferedHeight = parseInt(oriImg.getAttribute("data-height")); const img2 = new Image(); img2.onload = (ev) => { - // const canvas = document.createElement('canvas'); const canvas = document.getElementById("canvas") as HTMLCanvasElement; canvas.width = img2.width; canvas.height = img2.height; @@ -21,12 +22,23 @@ function main() { ctx.drawImage(img2, 0, 0); const edge = new Edges(); - edge.parseCanvasTransparency(canvas); - document.getElementById("top").textContent = edge.gettop().replace(/px/gi, "").replace(/\,\s/gi, "\n"); - document.getElementById("right").textContent = edge.getright().replace(/px/gi, "").replace(/\,\s/gi, "\n"); - document.getElementById("bottom").textContent = edge.getbottom().replace(/px/gi, "").replace(/\,\s/gi, "\n"); - document.getElementById("left").textContent = edge.getleft().replace(/px/gi, "").replace(/\,\s/gi, "\n"); - // document.getElementById('app').style.clipPath = `polygon(${edge.top}, ${edge.bottom})`; + edge.parseCanvasTransparency(canvas, preferedWidth, preferedHeight); + document.getElementById("top").textContent = edge + .gettop() + .replace(/px/gi, "") + .replace(/\,\s/gi, "\n"); + document.getElementById("right").textContent = edge + .getright() + .replace(/px/gi, "") + .replace(/\,\s/gi, "\n"); + document.getElementById("bottom").textContent = edge + .getbottom() + .replace(/px/gi, "") + .replace(/\,\s/gi, "\n"); + document.getElementById("left").textContent = edge + .getleft() + .replace(/px/gi, "") + .replace(/\,\s/gi, "\n"); document.getElementById("app").style.clipPath = edge.getPolygon(); }; img2.setAttribute("src", oriImg.getAttribute("src")); diff --git a/packages/webamp-modern-2/src/index.html b/packages/webamp-modern-2/src/index.html index 9f270196..b62de132 100644 --- a/packages/webamp-modern-2/src/index.html +++ b/packages/webamp-modern-2/src/index.html @@ -21,11 +21,27 @@ select:focus { outline: none; } + select option { + padding-left: 5px; + width: 300%; + } + select option[selected] { + font-weight: bold; + } + select::before { + padding-left: 5px; + content: var(--colheader, none); + display: block; + position: sticky; + top: var(--colheadertop, 0); + left: 0; + background-color: black; + color: silver; + } .webamp--img { background-image: var(--background-image); background-position: top 0 left 0; } - .webamp--img:active { background-image: var(--down-background-image, var(--background-image)); } @@ -70,6 +86,30 @@ slider { overflow: hidden; } + slider > div { + display: none; + } + slider::after { + content: ""; + position: absolute; + left: var(--thumb-left); + top: var(--thumb-top); + width: var(--thumb-width); + height: var(--thumb-height); + background-image: var(--thumb-background-image); + } + slider:hover:after { + background-image: var( + --thumb-hover-background-image, + var(--thumb-background-image) + ); + } + slider:active:after { + background-image: var( + --thumb-down-background-image, + var(--thumb-background-image) + ); + } text { overflow: hidden; box-sizing: border-box; @@ -125,6 +165,7 @@ text, wasabiframe, wasabititlebar, + componentbucket, colorthemeslist { position: absolute; left: 0; @@ -146,12 +187,33 @@ grid middle { flex-grow: 1; } - /* grid left, gridprogress left {left: 0;} - grid right, gridprogress right {right: 0;} */ - /* body > div select>option { - position: unset; - display: block; - } */ + componentbucket { + overflow: hidden; + } + componentbucket > wrapper { + display: flex; + position: absolute; + left: 0; + top: 0; + height: 100%; + width: auto; + transition: top 0.5s, left 0.5s; + } + componentbucket.vertical > wrapper { + flex-direction: column; + height: auto; + width: 100%; + } + componentbucket > wrapper > group { + position: relative; + } + group.x-fade > * { + transition: opacity var(--fade-in-speed, 0.5); + } + group.x-fade > .fading-out { + transition: opacity var(--fade-out-speed, 0.25); + } + .wasabi-button { border: 3px solid transparent; --border-image: var(--bitmap-studio-button); @@ -184,6 +246,8 @@ border: 1px solid blue; background-color: rgba(74, 74, 251, 0.205); z-index: 1000; + box-sizing: border-box; + transition: width 0.1s, height 0.1s, left 0.1s, top 0.1s; } diff --git a/packages/webamp-modern-2/src/index.ts b/packages/webamp-modern-2/src/index.ts index 48b600b4..722d78f7 100644 --- a/packages/webamp-modern-2/src/index.ts +++ b/packages/webamp-modern-2/src/index.ts @@ -1,17 +1,9 @@ import JSZip from "jszip"; // This module is imported early here in order to avoid a circular dependency. -import { classResolver } from "./skin/resolver"; -import SkinParser from "./skin/parse"; import UI_ROOT from "./UIRoot"; -import { getUrlQuery, removeAllChildNodes } from "./utils"; +import { getUrlQuery } from "./utils"; import { addDropHandler } from "./dropTarget"; -function hack() { - // Without this Snowpack will try to treeshake out resolver causing a circular - // dependency. - classResolver("A funny joke about why this is needed."); -} - addDropHandler(loadSkin); const STATUS = document.getElementById("status"); @@ -20,9 +12,12 @@ function setStatus(status: string) { STATUS.innerText = status; } +// const DEFAULT_SKIN = "assets/MMD3.wal" +const DEFAULT_SKIN = "assets/WinampModern566.wal" + async function main() { setStatus("Downloading skin..."); - const skinPath = getUrlQuery(window.location, "skin") || "assets/MMD3.wal"; + const skinPath = getUrlQuery(window.location, "skin") || DEFAULT_SKIN; const response = await fetch(skinPath); const data = await response.blob(); await loadSkin(data); @@ -34,12 +29,12 @@ async function loadSkin(skinData: Blob) { setStatus("Loading .wal archive..."); const zip = await JSZip.loadAsync(skinData); + UI_ROOT.setZip(zip); setStatus("Parsing XML and initializing images..."); - const parser = new SkinParser(zip, UI_ROOT); // This is always the same as the global singleton. - const uiRoot = await parser.parse(); + const uiRoot = await UI_ROOT._parser.parse(); const start = performance.now(); uiRoot.enableDefaultGammaSet(); @@ -48,6 +43,7 @@ async function loadSkin(skinData: Blob) { setStatus("Rendering skin for the first time..."); uiRoot.draw(); + uiRoot.init(); setStatus("Initializing Maki..."); for (const container of uiRoot.getContainers()) { diff --git a/packages/webamp-modern-2/src/maki/interpreter.ts b/packages/webamp-modern-2/src/maki/interpreter.ts index b8bc89a3..a41d3367 100644 --- a/packages/webamp-modern-2/src/maki/interpreter.ts +++ b/packages/webamp-modern-2/src/maki/interpreter.ts @@ -2,6 +2,7 @@ import { V, Variable } from "./v"; import { assert, assume } from "../utils"; import { ParsedMaki, Command, Method } from "./parser"; import { getClass, getMethod } from "./objects"; +import GuiObj from "../skin/makiClasses/GuiObj"; // import { classResolver } from "../skin/resolver"; function validateMaki(program: ParsedMaki) { @@ -311,6 +312,22 @@ class Interpreter { methodArgs.push(a.value); } const obj = this.stack.pop(); + + // It is temporary patch until we can bind a + // singleton object to maki world. + // It is because maki think each class name below as a const. + if ( + !obj.value && + klass.name && + [ + "winampconfig", + "winampconfiggroup", + "configclass", + "config", + ].includes(klass.name.toLowerCase()) + ) { + obj.value = new klass(); + } assert( (obj.type === "OBJECT" && typeof obj.value) === "object" && obj.value != null, @@ -323,8 +340,11 @@ class Interpreter { value = obj.value[methodName](...methodArgs); } catch (err) { console.warn( - `error call: ${klass.name}.${methodName}(...${JSON.stringify(methodArgs)})`, - `err: ${err.message} obj: ${JSON.stringify(obj)}` + `error call: ${klass.name}.${methodName}(...${JSON.stringify( + methodArgs + )})`, + `err: ${err.message} obj:`, + obj ); value = null; } @@ -603,14 +623,12 @@ class Interpreter { switch (a.type) { case "STRING": case "OBJECT": - case "BOOLEAN": case "NULL": throw new Error("Tried to add non-numbers."); } switch (b.type) { case "STRING": case "OBJECT": - case "BOOLEAN": case "NULL": throw new Error("Tried to add non-numbers."); } diff --git a/packages/webamp-modern-2/src/maki/objectData/winampconfig.json b/packages/webamp-modern-2/src/maki/objectData/winampconfig.json new file mode 100644 index 00000000..0ec8fc5e --- /dev/null +++ b/packages/webamp-modern-2/src/maki/objectData/winampconfig.json @@ -0,0 +1,88 @@ +{ + "B2AD3F2B31ED4e31BC6DE9951CD555BB": { + "name": "WinampConfig", + "parent": "Object", + "functions": [ + { + "result": "WinampConfigGroup", + "name": "getGroup", + "parameters": [ + [ + "String", + "config_group_guid" + ] + ] + } + ] + }, + "FC17844EC72B4518A068A8F930A5BA80": { + "name": "WinampConfigGroup", + "parent": "Object", + "functions": [ + { + "result": "Boolean", + "name": "getBool", + "parameters": [ + [ + "String", + "itemname" + ] + ] + }, + { + "result": "", + "name": "setBool", + "parameters": [ + [ + "String", + "itemname" + ], + [ + "Boolean", + "itemvalue" + ] + ] + }, + { + "result": "Int", + "name": "getInt", + "parameters": [ + [ + "String", + "itemname" + ] + ] + }, + { + "result": "", + "name": "setInt", + "parameters": [ + [ + "String", + "itemname" + ] + ] + }, + { + "result": "String", + "name": "getString", + "parameters": [ + [ + "String", + "itemname" + ] + ] + }, + { + "result": "", + "name": "setString", + "parameters": [ + [ + "String", + "itemname" + ] + ] + } + ] + } +} \ No newline at end of file diff --git a/packages/webamp-modern-2/src/maki/objects.ts b/packages/webamp-modern-2/src/maki/objects.ts index 56e60b45..b1444e5e 100644 --- a/packages/webamp-modern-2/src/maki/objects.ts +++ b/packages/webamp-modern-2/src/maki/objects.ts @@ -1,6 +1,7 @@ import stdPatched from "./objectData/stdPatched"; import pldir from "./objectData/pldir.json"; import config from "./objectData/config.json"; +import winampconfig from "./objectData/winampconfig.json"; import { DataType } from "./v"; import { assert } from "../utils"; @@ -21,6 +22,7 @@ const objects: { [key: string]: ObjectDefinition } = { ...stdPatched, ...pldir, ...config, + ...winampconfig, }; export function getClass(id: string): ObjectDefinition { diff --git a/packages/webamp-modern-2/src/progress.ts b/packages/webamp-modern-2/src/progress.ts index e8bbf303..19372e23 100644 --- a/packages/webamp-modern-2/src/progress.ts +++ b/packages/webamp-modern-2/src/progress.ts @@ -2,8 +2,6 @@ import { classResolver } from "./skin/resolver"; import { normalizedObjects, getFormattedId } from "./maki/objects"; import BaseObject from "./skin/makiClasses/BaseObject"; -// import { addDropHandler } from "./dropTarget"; -// import JSZip from "jszip"; function hack() { // Without this Snowpack will try to treeshake out resolver causing a circular diff --git a/packages/webamp-modern-2/src/skin/AudioPlayer.ts b/packages/webamp-modern-2/src/skin/AudioPlayer.ts index c7185361..a259e685 100644 --- a/packages/webamp-modern-2/src/skin/AudioPlayer.ts +++ b/packages/webamp-modern-2/src/skin/AudioPlayer.ts @@ -2,6 +2,10 @@ import { clamp, Emitter } from "../utils"; const BANDS = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000]; +export const AUDIO_PAUSED = "paused"; +export const AUDIO_STOPPED = "stopped"; +export const AUDIO_PLAYING = "playing"; + export class AudioPlayer { _input: HTMLInputElement = document.createElement("input"); _audio: HTMLAudioElement = document.createElement("audio"); @@ -12,6 +16,12 @@ export class AudioPlayer { _eqValues: { [kind: string]: number } = {}; _eqNodes: { [kind: string]: number } = {}; _eqEmitter: Emitter = new Emitter(); + _isStop: boolean = true; //becaue we can't audio.stop() currently + _trackInfo: {}; + _albumArtUrl: string = null; + //events aka addEventListener() + _eventListener: Emitter = new Emitter(); + constructor() { this._context = this._context = new (window.AudioContext || window.webkitAudioContext)(); @@ -85,20 +95,48 @@ export class AudioPlayer { this._audio.src = URL.createObjectURL(file); this.play(); }; + + //temporary: in the end of playing mp3, lets stop. + //TODO: in future, when ended: play next mp3 + this._audio.addEventListener("ended", () => this.stop()); } + + // shortcut of this.Emitter + on(event: string, callback: Function) { + this._eventListener.on(event, callback); + } + trigger(event: string, ...args: any[]) { + this._eventListener.trigger(event, ...args); + } + off(event: string, callback: Function) { + this._eventListener.off(event, callback); + } + // 0-1 getVolume(): number { return this._audio.volume; } play() { + this._isStop = false; this._audio.play(); + this.trigger("play"); + this.trigger("statchanged"); } stop() { + this._isStop = true; // needed to make threestate + if (this._audio.paused) { + this._audio.play(); + } // for trigger the event change this._audio.pause(); this._audio.currentTime = 0; + this.trigger("stop"); + this.trigger("statchanged"); } pause() { + this._isStop = false; // needed to make threestate this._audio.pause(); + this.trigger("pause"); + this.trigger("statchanged"); } eject() { @@ -131,10 +169,25 @@ export class AudioPlayer { return this._audio.currentTime / this._audio.duration; } + getState(): string { + if (this._isStop) { + // To distinct from pause + return AUDIO_STOPPED; + } + const audio = this._audio; + if (!audio.ended && !audio.paused) { + return AUDIO_PLAYING; + } else if (audio.ended) { + return AUDIO_STOPPED; + } else if (audio.paused) { + return AUDIO_PAUSED; + } + } + getEq(kind: string): number { switch (kind) { case "preamp": - return this.__preamp.gain.value; + return (this.__preamp.gain.value + 12) / 24; case "1": case "2": case "3": @@ -222,6 +275,17 @@ export class AudioPlayer { return dispose; } + onVolumeChanged(cb: () => void): () => void { + const handler = () => cb(); + this._audio.addEventListener("volumechange", handler); + const dispose = () => { + this._audio.removeEventListener("volumechange", handler); + }; + return dispose; + } + + + // Current track length in seconds getLength(): number { return this._audio.duration; diff --git a/packages/webamp-modern-2/src/skin/Bitmap.ts b/packages/webamp-modern-2/src/skin/Bitmap.ts index 1cd408b0..a13a7781 100644 --- a/packages/webamp-modern-2/src/skin/Bitmap.ts +++ b/packages/webamp-modern-2/src/skin/Bitmap.ts @@ -8,8 +8,8 @@ export default class Bitmap { _url: string; _img: HTMLImageElement; _canvas: HTMLCanvasElement; - _x: number; - _y: number; + _x: number = 0; + _y: number = 0; _width: number; _height: number; _file: string; @@ -53,7 +53,11 @@ export default class Bitmap { } getId() { - return this._id; + return this._id || ""; + } + + getFile() { + return this._file; } getWidth() { @@ -136,21 +140,11 @@ export default class Bitmap { if (this._canvas == null) { assert(this._img != null, "Expected bitmap image to be loaded"); this._canvas = document.createElement("canvas"); - this._canvas.width = this.getWidth(); - this._canvas.height = this.getHeight(); + this._canvas.width = this.getWidth() || this._img.width; + this._canvas.height = this.getHeight() || this._img.height; const ctx = this._canvas.getContext("2d"); // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage - ctx.drawImage( - this._img, - this._x, - this._y, - this.getWidth(), - this.getHeight(), - 0, - 0, - this.getWidth(), - this.getHeight() - ); + ctx.drawImage(this._img, -this._x, -this._y); } return this._canvas; } diff --git a/packages/webamp-modern-2/src/skin/BitmapFont.ts b/packages/webamp-modern-2/src/skin/BitmapFont.ts index ab60bed0..d2453730 100644 --- a/packages/webamp-modern-2/src/skin/BitmapFont.ts +++ b/packages/webamp-modern-2/src/skin/BitmapFont.ts @@ -1,8 +1,10 @@ +import UI_ROOT from "../UIRoot"; import { num, px } from "../utils"; +import Bitmap from "./Bitmap"; import ImageManager from "./ImageManager"; const CHARS = - "abcdefghijklmnopqrstuvwxyz\"@ \n0123456789\u2026.:()-'!_+/[]^&%,=$#\nâöä?*"; + "abcdefghijklmnopqrstuvwxyz\"@ \n0123456789\u2026.:()-'!_+\\/[]^&%,=$#\nâöä?*"; const CHAR_MAP = {}; for (const [line, chars] of CHARS.split("\n").entries()) { @@ -12,30 +14,21 @@ for (const [line, chars] of CHARS.split("\n").entries()) { } // http://wiki.winamp.com/wiki/XML_Elements#.3Cbitmapfont.2F.3E -export default class BitmapFont { - _file: string; - _id: string; +export default class BitmapFont extends Bitmap { _charWidth: number; _charHeight: number; _horizontalSpacing: number; _verticalSpacing: number; - _url: string; + _externalBitmap: boolean = false; //? true == _file = another.bitmap.id + _bitmap: Bitmap = null; // the real external bitmap - setXmlAttributes(attributes: { [attrName: string]: string }) { - for (const [key, value] of Object.entries(attributes)) { - this.setXmlAttr(key, value); + setXmlAttr(_key: string, value: string): boolean { + if (super.setXmlAttr(_key, value)) { + return true; } - } - setXmlAttr(_key: string, value: string) { const key = _key.toLowerCase(); switch (key) { - case "id": - this._id = value; - break; - case "file": - this._file = value; - break; case "charwidth": this._charWidth = num(value); break; @@ -54,8 +47,17 @@ export default class BitmapFont { return true; } - getId() { - return this._id; + _setAsBackground(div: HTMLElement, prefix: string) { + if (this._externalBitmap) { + if (!this._bitmap) { + this._bitmap = UI_ROOT.getBitmap(this._file); + } + if (this._bitmap != null) { + this._bitmap._setAsBackground(div, prefix); + } + } else { + super._setAsBackground(div, prefix); + } } // TODO: This could likely be made more efficient. @@ -63,26 +65,19 @@ export default class BitmapFont { // the background position just once. renderLetter(char: string): HTMLSpanElement { const span = document.createElement("span"); - span.style.display = "inline-block"; - span.style.width = px(this._charWidth); - span.style.height = px(this._charHeight); - span.style.backgroundImage = `url(${this._url})`; - span.style.verticalAlign = "top"; const [x, y] = CHAR_MAP[char.toLocaleLowerCase()] ?? CHAR_MAP[" "]; - span.style.backgroundPositionX = px(-(this._charWidth * x)); - span.style.backgroundPositionY = px(-(this._charHeight * y)); - span.style.paddingTop = px((this._verticalSpacing ?? 0) / 2); - span.style.paddingBottom = px((this._verticalSpacing ?? 0) / 2); - span.style.paddingLeft = px((this._horizontalSpacing ?? 0) / 2); - span.style.paddingRight = px((this._horizontalSpacing ?? 0) / 2); span.innerText = char; // Keep things accessible - span.style.textIndent = "-9999px"; // But hide the characters + span.style.setProperty("--x", px(-(this._charWidth * x))); + span.style.setProperty("--y", px(-(this._charHeight * y))); return span; } - // Ensure we've loaded the font into our asset loader. - async ensureFontLoaded(imageManager: ImageManager) { - const imgUrl = await imageManager.getUrl(this._file); - this._url = imgUrl; + useExternalBitmap(): boolean { + return this._externalBitmap; } + + setExternalBitmap(isExternal: boolean) { + this._externalBitmap = isExternal; + } + } diff --git a/packages/webamp-modern-2/src/skin/Clippath.ts b/packages/webamp-modern-2/src/skin/Clippath.ts index a2f20230..de0a7a3a 100644 --- a/packages/webamp-modern-2/src/skin/Clippath.ts +++ b/packages/webamp-modern-2/src/skin/Clippath.ts @@ -4,9 +4,9 @@ export class Edges { _bottom: string[] = []; _left: string[] = []; - parseCanvasTransparency(canvas: HTMLCanvasElement) { - const w = canvas.width; - const h = canvas.height; + parseCanvasTransparency(canvas: HTMLCanvasElement, preferedWidth:number, preferedHeight:number) { + const w = preferedWidth || canvas.width; + const h = preferedHeight || canvas.height; const ctx = canvas.getContext("2d"); const data = ctx.getImageData(0, 0, w, h).data; let points: string[] = []; diff --git a/packages/webamp-modern-2/src/skin/Color.ts b/packages/webamp-modern-2/src/skin/Color.ts index 9f2c6e91..a2fdc3a6 100644 --- a/packages/webamp-modern-2/src/skin/Color.ts +++ b/packages/webamp-modern-2/src/skin/Color.ts @@ -2,6 +2,7 @@ import ImageManager from "./ImageManager"; export default class Color { _id: string; + _cssVar: string; _value: string; _gammagroup: string; @@ -16,6 +17,7 @@ export default class Color { switch (key) { case "id": this._id = value; + this._cssVar = `--color-${this.getId().replace(/[^a-zA-Z0-9]/g, "-")}`; break; case "value": this._value = value; @@ -33,6 +35,18 @@ export default class Color { return this._id; } + getGammaGroup(): string { + return this._gammagroup; + } + + getValue(): string { + return this._value; + } + + getCSSVar(): string { + return this._cssVar; + } + getRgb() { return `rgb(${this._value})`; } diff --git a/packages/webamp-modern-2/src/skin/ColorThemesList.ts b/packages/webamp-modern-2/src/skin/ColorThemesList.ts index d6b2e338..51214cb5 100644 --- a/packages/webamp-modern-2/src/skin/ColorThemesList.ts +++ b/packages/webamp-modern-2/src/skin/ColorThemesList.ts @@ -1,52 +1,110 @@ import GuiObj from "./makiClasses/GuiObj"; import UI_ROOT from "../UIRoot"; -import { removeAllChildNodes } from "../utils"; +import { removeAllChildNodes, toBool } from "../utils"; export default class ColorThemesList extends GuiObj { _select: HTMLSelectElement = document.createElement("select"); + _nohscroll: boolean = false; + _nocolheader: boolean = false; constructor() { super(); this._div.appendChild(this._select); + this._registerEvents(); } setXmlAttr(key: string, value: string): boolean { if (super.setXmlAttr(key, value)) { return true; } - switch (key) { + const _key = key.toLowerCase(); + switch (_key) { + // see skin: D-Reliction + case "nocolheader": + this._nocolheader = toBool(value); + break; + case "nohscroll": + this._nohscroll = toBool(value); + break; default: return false; } return true; } + _registerEvents() { + this._select.addEventListener("dblclick", () => { + this.handleAction("colorthemes_switch"); + }); + } + _renderGammaSets() { removeAllChildNodes(this._select); this._select.setAttribute("multiple", "1"); this._select.style.position = "absolute"; this._select.style.width = "100%"; this._select.style.height = "100%"; + for (const key of UI_ROOT._gammaSets.keys()) { const option = document.createElement("option"); option.value = key; option.innerText = UI_ROOT._gammaNames[key]; this._select.appendChild(option); } + + // Overflow + this._select.style.overflowY = "scroll"; + if (this._nohscroll) { + this._select.style.overflowX = "hidden"; + } else { + this._select.style.overflowX = "scroll"; + } + + // column header + if (this._nocolheader) { + this._select.style.setProperty("--colheader", null); + } else { + this._select.style.setProperty("--colheader", "'Theme'"); + } + this._select.value = UI_ROOT._activeGammaSetName; + this._renderBoldSelection(); + } + + _renderBoldSelection() { + Array.from(this._select.options).forEach((option_element) => { + if (option_element.value === UI_ROOT._activeGammaSetName) { + option_element.setAttribute("selected", "selected"); + } else { + option_element.removeAttribute("selected"); + } + }); } handleAction( action: string, - param: string | null, - actionTarget: string | null + param: string | null = null, + actionTarget: string | null = null ) { switch (action) { case "colorthemes_switch": const selected = this._select.value; if (selected != null) { UI_ROOT.enableGammaSet(selected); + this._renderBoldSelection(); } return true; + case "colorthemes_previous": + if(this._select.selectedIndex > 0) { + this._select.selectedIndex--; + return this.handleAction('colorthemes_switch') + } + break + case "colorthemes_next": + if(this._select.selectedIndex < this._select.options.length) { + this._select.selectedIndex++; + return this.handleAction('colorthemes_switch') + } + break } return false; } diff --git a/packages/webamp-modern-2/src/skin/GammaGroup.ts b/packages/webamp-modern-2/src/skin/GammaGroup.ts index 67c3f517..cac61a80 100644 --- a/packages/webamp-modern-2/src/skin/GammaGroup.ts +++ b/packages/webamp-modern-2/src/skin/GammaGroup.ts @@ -89,4 +89,25 @@ export default class GammaGroup { ctx.putImageData(imageData, 0, 0); return canvas.toDataURL(); } + + transformColor(color: string): string { + const [r, g, b] = this._value.split(",").map((v) => { + return Number(v) / 4096 + 1.0; + }); + let [red, green, blue] = color.split(",").map((v) => parseInt(v)); + if (this._gray != 0) { + if (this._gray == 2) red = (red + green + blue) / 3; + if (this._gray == 1) red = Math.max(red, green, blue); + green = red; + blue = red; + } + const mult = this._boost == 2 ? 4 : 1; + const brightness = this._boost == 1 ? 128 : this._boost == 2 ? 32 : 0; + + red = clamp((red + brightness) * mult * r, 0, 255); // red + green = clamp((green + brightness) * mult * g, 0, 255); // green + blue = clamp((blue + brightness) * mult * b, 0, 255); // blue + + return `rgb(${red}, ${green}, ${blue})`; + } } diff --git a/packages/webamp-modern-2/src/skin/ImageManager.ts b/packages/webamp-modern-2/src/skin/ImageManager.ts index 3d904ef7..7d3f1ea7 100644 --- a/packages/webamp-modern-2/src/skin/ImageManager.ts +++ b/packages/webamp-modern-2/src/skin/ImageManager.ts @@ -1,5 +1,6 @@ -import JSZip from "jszip"; +import UI_ROOT from "../UIRoot"; import { getCaseInsensitiveFile } from "../utils"; +import Bitmap from "./Bitmap"; // https://png-pixel.com/ const DEFAULT_IMAGE_URL = @@ -8,24 +9,51 @@ const DEFAULT_IMAGE_URL = export default class ImageManager { _urlCache: Map = new Map(); _imgCache: Map = new Map(); - _cssVarCache: Map = new Map(); - constructor(private _zip: JSZip) {} + _pathofBitmap = {}; //? file : true|false|null + _bitmaps: { [key: string]: Bitmap } = {}; //? Bitmap:file + _bitmapAlias = {}; //? file|id : true|false|null //for BitmapFont async getUrl(filePath: string): Promise { if (!this._urlCache.has(filePath)) { - const zipFile = getCaseInsensitiveFile(this._zip, filePath); - if (zipFile == null) { + const imgBlob = await UI_ROOT.getFileAsBlob(filePath); + if (imgBlob == null) { return null; } - const imgBlob = await zipFile.async("blob"); const imgUrl = await getUrlFromBlob(imgBlob); - // const img = await this.getImage(imgUrl); - // const transformedUrl = transformImage(img); this._urlCache.set(filePath, imgUrl); } return this._urlCache.get(filePath); } + addBitmap(bitmap: Bitmap) { + const id = bitmap.getId(); + const filePath = bitmap.getFile(); + this._pathofBitmap[filePath] = false; + this._bitmaps[id] = bitmap; + } + + // Ensure we've loaded the image into our image loader. + async loadUniquePaths() { + for (const filePath of Object.keys(this._pathofBitmap)) { + await this.getImage(filePath); + } + } + async ensureBitmapsLoaded() { + return Promise.all( + Object.values(this._bitmaps).map(async (bitmap) => { + await this.setBimapImg(bitmap); + if (bitmap._img && bitmap._width == null && bitmap._height == null) { + bitmap.setXmlAttr("w", String(bitmap._img.width)); + bitmap.setXmlAttr("h", String(bitmap._img.height)); + } + }) + ); + } + + async setBimapImg(bitmap: Bitmap) { + bitmap._img = await this.getImage(bitmap.getFile()); + } + async getImage(filePath: string): Promise { if (!this._imgCache.has(filePath)) { // TODO: We could cache this diff --git a/packages/webamp-modern-2/src/skin/PrivateConfig.ts b/packages/webamp-modern-2/src/skin/PrivateConfig.ts index 57bbfa9b..6ecb9fd1 100644 --- a/packages/webamp-modern-2/src/skin/PrivateConfig.ts +++ b/packages/webamp-modern-2/src/skin/PrivateConfig.ts @@ -1,19 +1,26 @@ // TODO: Persist this to local state? class PrivateConfig { - _sections: Map> = new Map(); + _sections: Map> = new Map(); _getSection(section: string) { if (!this._sections.has(section)) { this._sections.set(section, new Map()); } return this._sections.get(section); } - getPrivateInt(section: string, item: string, defvalue: number) { - return this._getSection(section).get(item) ?? defvalue; + getPrivateInt(section: string, item: string, defvalue: number):number { + const value:string = this._getSection(section).get(item); + return value? parseInt(value) : defvalue ; } setPrivateInt(section: string, item: string, value: number) { - return this._getSection(section).set(item, value); + return this._getSection(section).set(item, value.toString()); } + + getPrivateString(section: string, item: string, defvalue: string): string { + return this._getSection(section).get(item) ?? defvalue; + } + + } const PRIVATE_CONFIG = new PrivateConfig(); diff --git a/packages/webamp-modern-2/src/skin/TrueTypeFont.ts b/packages/webamp-modern-2/src/skin/TrueTypeFont.ts index 4a5838eb..63dcccbe 100644 --- a/packages/webamp-modern-2/src/skin/TrueTypeFont.ts +++ b/packages/webamp-modern-2/src/skin/TrueTypeFont.ts @@ -5,6 +5,7 @@ export default class TrueTypeFont { _file: string; _id: string; _fontFace: FontFace; + _inlineFamily: string; setXmlAttributes(attributes: { [attrName: string]: string }) { for (const [key, value] of Object.entries(attributes)) { @@ -13,10 +14,12 @@ export default class TrueTypeFont { } setXmlAttr(key: string, value: string) { - switch (key) { + switch (key.toLowerCase()) { case "id": this._id = value; break; + case "family": + this._inlineFamily = value; case "file": this._file = value; default: @@ -26,11 +29,11 @@ export default class TrueTypeFont { } getId() { - return this._id; + return this._id || ""; } getFontFamily() { - return this._fontFace.family; + return this._inlineFamily || this._fontFace.family; } dispose() { diff --git a/packages/webamp-modern-2/src/skin/VM.ts b/packages/webamp-modern-2/src/skin/VM.ts index 073ddbf9..896f73ec 100644 --- a/packages/webamp-modern-2/src/skin/VM.ts +++ b/packages/webamp-modern-2/src/skin/VM.ts @@ -9,7 +9,8 @@ export default class Vm { _scripts: ParsedMaki[] = []; // This could easily become performance sensitive. We could make this more // performant by normalizing some of these things when scripts are added. - dispatch(object: BaseObject, event: string, args: Variable[] = []) { + dispatch(object: BaseObject, event: string, args: Variable[] = []): number { + let ran = 0; for (const [scriptId, script] of this._scripts.entries()) { for (const binding of script.bindings) { if ( @@ -18,9 +19,11 @@ export default class Vm { ) { const reversedArgs = [...args].reverse(); this.interpret(scriptId, binding.commandOffset, reversedArgs); + ran++; } } } + return ran } addScript(maki: ParsedMaki): number { diff --git a/packages/webamp-modern-2/src/skin/makiClasses/AlbumArt.ts b/packages/webamp-modern-2/src/skin/makiClasses/AlbumArt.ts new file mode 100644 index 00000000..b369a5cc --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/AlbumArt.ts @@ -0,0 +1,43 @@ +import AUDIO_PLAYER from "../AudioPlayer"; +import Layer from "./Layer"; + +// http://wiki.winamp.com/wiki/XML_GUI_Objects#.3CAlbumArt.2F.3E +export default class AlbumArt extends Layer { + static GUID = "6dcb05e448c28ac4f04993b14af50e91"; + + constructor() { + super(); + this._width = 0; + this._height = 0; + this._relatw = "1"; + this._relath = "1"; + } + + draw() { + super.draw(); + this.refresh(); + } + + refresh() { + const albumArtUrl = AUDIO_PLAYER._albumArtUrl; + if (albumArtUrl != null) { + this._div.style.pointerEvents = "all"; + this._div.style.backgroundImage = `url(${albumArtUrl})`; + this._div.style.backgroundSize = "cover"; + } else { + this._div.style.removeProperty("background-image"); + } + } + + isloading(): number { + return 1; + } + + onAlbumArtLoaded(success: boolean) { + return true; //TODO + } + + isinvalid(): boolean { + return false; + } +} diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Button.ts b/packages/webamp-modern-2/src/skin/makiClasses/Button.ts index 055392de..3776f2ee 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Button.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Button.ts @@ -18,12 +18,13 @@ export default class Button extends GuiObj { super(); // TODO: Cleanup! this._div.addEventListener("mousedown", this._handleMouseDown.bind(this)); - this._div.addEventListener("click", (e) => { + this._div.addEventListener("click", (e: MouseEvent) => { if (this._action) { this.dispatchAction(this._action, this._param, this._actionTarget); } - // TODO: Only left button - this.onLeftClick(); + if (e.button == 0) { + this.leftclick(); + } }); } @@ -56,6 +57,7 @@ export default class Button extends GuiObj { this._param = value; break; case "action_target": + case "cbtarget": this._actionTarget = value; break; default: @@ -71,7 +73,7 @@ export default class Button extends GuiObj { } if (this._image != null) { const bitmap = UI_ROOT.getBitmap(this._image); - return bitmap.getHeight(); + if (bitmap) return bitmap.getHeight(); } return super.getheight(); } @@ -83,15 +85,15 @@ export default class Button extends GuiObj { } if (this._image != null) { const bitmap = UI_ROOT.getBitmap(this._image); - return bitmap.getWidth(); + if (bitmap) return bitmap.getWidth(); } return super.getwidth(); } getactivated(): boolean { - return this._active; + return this._active ? true : false; } - setactivated(_onoff: boolean) { + setactivated(_onoff: boolean | number) { const onoff = Boolean(_onoff); if (onoff !== this._active) { @@ -107,12 +109,41 @@ export default class Button extends GuiObj { leftclick() { this.onLeftClick(); + if (this._action && this._actionTarget) { + const guiObj = this.findobject(this._actionTarget); + if (guiObj) { + guiObj.sendaction( + this._action, + this._param, + 0, + 0, + this._div.offsetLeft, + this._div.offsetTop, + this + ); + } + } } onLeftClick() { UI_ROOT.vm.dispatch(this, "onleftclick", []); } + handleAction( + action: string, + param: string | null = null, + actionTarget: string | null = null + ): boolean { + if (actionTarget) { + const guiObj = this.findobject(actionTarget); + if (guiObj) { + guiObj.handleAction(action, param); + return true; + } + } + return false; + } + _renderBackground() { if (this._image != null) { const bitmap = UI_ROOT.getBitmap(this._image); @@ -156,6 +187,13 @@ export default class Button extends GuiObj { this._renderBackground(); } + hide() { + if (document.activeElement == this._div) { + this.getparentlayout()._parent._div.focus(); + } + super.hide(); + } + /* extern Button.onActivate(int activated); extern Button.onLeftClick(); diff --git a/packages/webamp-modern-2/src/skin/makiClasses/ComponentBucket.ts b/packages/webamp-modern-2/src/skin/makiClasses/ComponentBucket.ts new file mode 100644 index 00000000..fa8fe8f6 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/ComponentBucket.ts @@ -0,0 +1,124 @@ +import GuiObj from "./GuiObj"; +import UI_ROOT from "../../UIRoot"; +import Group from "./Group"; +import { px, toBool } from "../../utils"; +import Button from "./Button"; + +// http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Ccomponentbucket.2F.3E +export default class ComponentBucket extends Group { + static GUID = "97aa3e4d4fa8f4d0f20a7b818349452a"; + _wndType: string; + _vertical: boolean = false; // default horizontal + _wrapper: HTMLElement; + _page: number = 0; + + constructor() { + super(); + this._wrapper = document.createElement("wrapper"); + } + + setXmlAttr(_key: string, value: string): boolean { + const key = _key.toLowerCase(); + if (super.setXmlAttr(key, value)) { + return true; + } + + switch (key.toLowerCase()) { + case "wndtype": + this._wndType = value.toLowerCase(); + break; + case "vertical": + this._vertical = toBool(value); + break; + default: + return false; + } + return true; + } + + getWindowType(): string { + return this._wndType; + } + + getmaxheight(): number { + return this._maximumHeight; + } + getmaxwidth(): number { + return this._maximumWidth; + } + + setscroll(x: number): number { + return 10; //TODO setscroll to ._div + } + + getscroll(): number { + return 10; //TODO: Check by ._div.scroll + } + + getnumchildren(): number { + return this._children.length; + } + + enumchildren(n: number): GuiObj { + return this._children[n]; + } + + handleAction( + action: string, + param: string | null = null, + actionTarget: string | null = null + ) { + switch (action.toLowerCase()) { + case "cb_prev": + case "cb_prevpage": + this._scrollPage(1); + return true; + case "cb_next": + case "cb_nextpage": + this._scrollPage(-1); + return true; + } + return false; + } + + init() { + this.resolveButtonsAction(); + super.init(); + } + resolveButtonsAction() { + for (const obj of this.getparent()._children) { + if ( + obj instanceof Button && + (obj._actionTarget == null || obj._actionTarget == "bucket") && + (obj._action && obj._action.startsWith('cb_')) + ) { + obj._actionTarget = this.getId() + } + } + } + + _scrollPage(step: 1 | -1) { + if (!this._children.length) return; + const oneChild = this._children[0]; + const oneStep = this._vertical ? oneChild.getheight() : oneChild.getwidth(); + const anchor = this._vertical ? "top" : "left"; + const currentStep = this._vertical? this._wrapper.offsetTop : this._wrapper.offsetLeft; + //TODO: Clamp to not over top nor over left (showing empty space bug) + this._wrapper.style.setProperty(anchor, px(currentStep + oneStep * step)); + } + + appendChildrenDiv() { + // ComponentBucket wraps its children in a div to be scrollable + this._div.appendChild(this._wrapper); + this._appendChildrenToDiv(this._wrapper); + } + + draw() { + super.draw(); + if (this._vertical) { + this._div.classList.add("vertical"); + } else { + this._div.classList.remove("vertical"); + } + } +} diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Config.ts b/packages/webamp-modern-2/src/skin/makiClasses/Config.ts index 287c493e..c86875b5 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Config.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Config.ts @@ -1,10 +1,24 @@ import XmlObj from "../XmlObj"; import ConfigItem from "./ConfigItem"; -export default class Config extends XmlObj { +const _items : {[key:string]: ConfigItem} = {}; + +export default class ConfigClass { static GUID = "593dba224976d07771f452b90b405536"; newitem(itemName: string, itemGuid: string): ConfigItem { - return new ConfigItem(); + const cfg = new ConfigItem(); + cfg._name = itemName; + _items[itemGuid] = cfg; + return cfg; + } + + getitem(itemGuid: string): ConfigItem { + let cfg = _items[itemGuid]; + if(!cfg){ + cfg = new ConfigItem(); + _items[itemGuid] = cfg; + } + return cfg; } } diff --git a/packages/webamp-modern-2/src/skin/makiClasses/ConfigAttribute.ts b/packages/webamp-modern-2/src/skin/makiClasses/ConfigAttribute.ts new file mode 100644 index 00000000..e1261f50 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/ConfigAttribute.ts @@ -0,0 +1,26 @@ +// import XmlObj from "../XmlObj"; + +export default class ConfigAttribute { + static GUID = "24dec2834a36b76e249ecc8c736c6bc4"; + _name : string; + _default: string; + _value: string; + + constructor(name:string, defaultValue: string) { +// constructor() { + // super(); + this._name = name; + this._default = defaultValue; + // this._value = '' + } + getdata():string{ + // return ''; + return this._value || this._default || ''; + } + setdata(value:string){ + this._value = value; + } + ondatachanged(){ + // this._value = value; + } +} diff --git a/packages/webamp-modern-2/src/skin/makiClasses/ConfigItem.ts b/packages/webamp-modern-2/src/skin/makiClasses/ConfigItem.ts index 8e10a605..d4a7fe54 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/ConfigItem.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/ConfigItem.ts @@ -1,3 +1,23 @@ import XmlObj from "../XmlObj"; +import ConfigAttribute from "./ConfigAttribute"; -export default class ConfigItem extends XmlObj {} + +export default class ConfigItem { + static GUID = "d40302824d873aab32128d87d5fcad6f"; + _name : string; + _items : {[key:string]: ConfigAttribute} = {}; + +// constructor(name:string) { + // constructor() { + // super(); + // this._name = name; + // this._items = {}; + // } + + newattribute(name: string, defaultValue: string): ConfigAttribute { + const cfg = new ConfigAttribute(name, defaultValue); + // const cfg = new ConfigAttribute(); + // this._items[name] = cfg; + return cfg; + } +} diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Container.ts b/packages/webamp-modern-2/src/skin/makiClasses/Container.ts index d3b26d3c..b141d58a 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Container.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Container.ts @@ -2,6 +2,7 @@ import UI_ROOT from "../../UIRoot"; import { assert, num, px, removeAllChildNodes, toBool } from "../../utils"; import Layout from "./Layout"; import XmlObj from "../XmlObj"; +import Group from "./Group"; // > A container is a top level object and it basically represents a window. // > Nothing holds a container. It is an object that holds multiple related @@ -15,6 +16,7 @@ export default class Container extends XmlObj { _activeLayout: Layout | null = null; _visible: boolean = true; _id: string; + _name: string; _x: number = 0; _y: number = 0; _componentGuid: string; // eg. "guid:{1234-...-0ABC}" @@ -30,6 +32,9 @@ export default class Container extends XmlObj { return true; } switch (key) { + case "name": + this._name = value; + break; case "id": this._id = value.toLowerCase(); break; @@ -158,14 +163,28 @@ export default class Container extends XmlObj { throw new Error(`Could not find a container with the id; "${layoutId}"`); } + /** + * @ret Layout + */ + getCurLayout(): Layout { + return this._activeLayout; + } + + + addLayout(layout: Layout) { - layout.setParentContainer(this); + layout.setParent(this as unknown as Group); this._layouts.push(layout); if (this._activeLayout == null) { this._activeLayout = layout; } } + // parser need it. + addChild(layout: Layout) { + this.addLayout(layout) + } + _clearCurrentLayout() { removeAllChildNodes(this._div); } @@ -212,6 +231,8 @@ export default class Container extends XmlObj { draw() { this._div.setAttribute("id", this.getId()); + this._div.setAttribute("tabindex", "1"); + this._renderDimensions(); this._renderLayout(); } } diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Grid.ts b/packages/webamp-modern-2/src/skin/makiClasses/Grid.ts new file mode 100644 index 00000000..f54aa477 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/Grid.ts @@ -0,0 +1,98 @@ +import GuiObj from "./GuiObj"; +import UI_ROOT from "../../UIRoot"; +import { px } from "../../utils"; + +// http://wiki.winamp.com/wiki/XML_GUI_Objects +export default class Grid extends GuiObj { +// static GUID = "5ab9fa1545579a7d5765c8aba97cc6a6"; + _image: string; // link to Bitmap._id + _left : HTMLElement; + _middle: HTMLElement; + _right : HTMLElement; + + constructor(){ + super(); + this._left = document.createElement('left'); + this._middle = document.createElement('middle'); + this._right = document.createElement('right'); + this._div.appendChild(this._left) + this._div.appendChild(this._middle) + this._div.appendChild(this._right) + } + + setXmlAttr(key: string, value: string): boolean { + if (super.setXmlAttr(key, value)) { + return true; + } + switch (key) { + case "middle": + // this._image = value; + // this._renderBackground(); + this._setBitmap(this._middle, value); + break; + case "left": + this._setBitmap(this._left, value); + break; + case "right": + this._setBitmap(this._right, value); + break; + default: + return false; + } + return true; + } + + // This shadows `getheight()` on GuiObj + getheight(): number { + if (this._height) { + return this._height; + } + if (this._image != null) { + const bitmap = UI_ROOT.getBitmap(this._image); + if(bitmap) return bitmap.getHeight(); + } + return super.getheight(); + } + + // This shadows `getwidth()` on GuiObj + getwidth(): number { + if (this._width) { + return this._width; + } + if (this._image != null) { + const bitmap = UI_ROOT.getBitmap(this._image); + if(bitmap) return bitmap.getWidth(); + } + return super.getwidth(); + } + + _renderBackground() { + const bitmap = this._image != null ? UI_ROOT.getBitmap(this._image) : null; + this.setBackgroundImage(bitmap); + } + + _setBitmap(element: HTMLElement, bitmap_id: string) { + const bitmap = UI_ROOT.getBitmap(bitmap_id); + // this.setBackgroundImage(bitmap); + if(bitmap){ + bitmap.setAsBackground(element); + element.style.width = px(bitmap.getWidth()); + } + } + + draw() { + super.draw(); + // this._div.setAttribute("data-obj-name", "Layer"); + // this._div.style.pointerEvents = this._sysregion==-2 || this._ghost? 'none' : 'auto'; + this._div.style.pointerEvents = 'none'; + // this._div.style.overflow = "hidden"; + this._div.style.removeProperty("display"); + this._div.classList.add("webamp--img"); + this._renderBackground(); + } + //setRegionFromMap(regionMap:Map, threshold:number) + + isinvalid():boolean { + return false; + } +} diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Group.ts b/packages/webamp-modern-2/src/skin/makiClasses/Group.ts index e4500eaf..68b66262 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Group.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Group.ts @@ -3,10 +3,12 @@ import UI_ROOT from "../../UIRoot"; import GuiObj from "./GuiObj"; import SystemObject from "./SystemObject"; import Movable from "./Movable"; +import Layout from "./Layout"; // http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Cgroup.2F.3E export default class Group extends Movable { static GUID = "45be95e5419120725fbb5c93fd17f1f9"; + _inited: boolean = false; _parent: Group; _instanceId: string; _background: string; @@ -14,7 +16,9 @@ export default class Group extends Movable { _drawBackground: boolean = true; _isLayout: boolean = false; _systemObjects: SystemObject[] = []; - _children: GuiObj[] = []; + _actualWidth: number; // for _invalidatesize, after draw + _actualHeight: number; + _regionCanvas: HTMLCanvasElement; setXmlAttr(_key: string, value: string): boolean { const key = _key.toLowerCase(); @@ -40,6 +44,11 @@ export default class Group extends Movable { } init() { + if (this._inited) return; + this._inited = true; + + super.init(); + for (const systemObject of this._systemObjects) { systemObject.init(); } @@ -62,22 +71,6 @@ export default class Group extends Movable { this._children.push(child); } - findobject(objectId: string): GuiObj | null { - const lower = objectId.toLowerCase(); - for (const obj of this._children) { - if (obj.getId() === lower) { - return obj; - } - if (obj instanceof Group) { - const found = obj.findobject(objectId); - if (found != null) { - return found; - } - } - } - return null; - } - /* Required for Maki */ getobject(objectId: string): GuiObj { const lower = objectId.toLowerCase(); @@ -92,7 +85,15 @@ export default class Group extends Movable { ); } - getparentlayout(): Group { + enumobject(index: number): GuiObj { + return this._children[index]; + } + + getnumobjects(): number { + return this._children.length; + } + + getparentlayout(): Layout { let obj: Group = this; while (obj._parent) { if (obj._isLayout) { @@ -103,7 +104,11 @@ export default class Group extends Movable { if (!obj) { console.warn("getParentLayout", this.getId(), "failed!"); } - return obj; + return obj as Layout; + } + + isLayout(): boolean { + return this._isLayout; } // This shadows `getheight()` on GuiObj @@ -113,17 +118,23 @@ export default class Group extends Movable { const bitmap = UI_ROOT.getBitmap(this._background); if (bitmap) return bitmap.getHeight(); } - return h; + return h ?? 0; } // This shadows `getwidth()` on GuiObj getwidth(): number { + if (this._autowidthsource) { + const widthSource = this.findobject(this._autowidthsource); + if (widthSource) { + return widthSource.getautowidth(); + } + } const w = super.getwidth(); if (w == null && this._background != null) { const bitmap = UI_ROOT.getBitmap(this._background); if (bitmap) return bitmap.getWidth(); } - return w; + return w ?? 0; } _renderBackground() { @@ -135,23 +146,140 @@ export default class Group extends Movable { } } + async doResize() { + UI_ROOT.vm.dispatch(this, "onresize", [ + { type: "INT", value: 0 }, + { type: "INT", value: 0 }, + { type: "INT", value: this.getwidth() }, + { type: "INT", value: this.getheight() }, + ]); + } + + /** + * it is needed because render region is expensive. + * Hence, we recalculate regions only if needed */ + async _invalidateSize() { + const actualBox = this._div.getBoundingClientRect(); + if ( + actualBox.width != this._actualWidth || + actualBox.height != this._actualHeight + ) { + this._actualWidth = actualBox.width; + this._actualHeight = actualBox.height; + this.doResize(); + this.applyRegions(); + } + for (const child of this._children) { + if (child instanceof Group) child._invalidateSize(); + } + } + + // SYSREGION THINGS ============================== + applyRegions() { + this._regionCanvas = null; + let hasRegions = false; + for (const child of this._children) { + // child.draw(); + if (child._sysregion == -1 || child._sysregion == -2) { + this.putAsRegion(child); + hasRegions = true; + } + } + if (hasRegions) { + this.setRegion(); + } + this._regionCanvas = null; + } + + putAsRegion(child: GuiObj) { + if ( + this._regionCanvas == null || + this._regionCanvas.width == 0 || + this._regionCanvas.height == 0 + ) { + const canvas = (this._regionCanvas = document.createElement("canvas")); + const bound = this._div.getBoundingClientRect(); + canvas.width = bound.width; + canvas.height = bound.height; + // console.log('createRegionCanvas:', bound.width, bound.height) + const ctx = canvas.getContext("2d"); + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, bound.width, bound.height); + } + if (this._regionCanvas.width == 0 || this._regionCanvas.height == 0) { + return; + } + + const ctx2 = this._regionCanvas.getContext("2d"); + const r = child._div.getBoundingClientRect(); + const bitmap = child._backgroundBitmap; + const img = child._backgroundBitmap.getImg(); + ctx2.drawImage( + img, + bitmap._x, + bitmap._y, + r.width, + r.height, + + child._div.offsetLeft, + child._div.offsetTop, + r.width, + r.height + ); + } + + setRegion() { + if (this._regionCanvas.width == 0 || this._regionCanvas.height == 0) { + return; + } + + const ctx2 = this._regionCanvas.getContext("2d"); + + const imageData = ctx2.getImageData( + 0, + 0, + this._regionCanvas.width, + this._regionCanvas.height + ); + const data = imageData.data; + for (var i = 0; i < data.length; i += 4) { + data[i + 3] = data[i + 0]; + } + ctx2.putImageData(imageData, 0, 0); + + this._regionCanvas.toBlob((blob) => { + const url = URL.createObjectURL(blob); + this._div.style.setProperty("mask-image", `url(${url})`); + this._div.style.setProperty("-webkit-mask-image", `url(${url})`); + }); + } + + appendChildrenDiv() { + // ComponentBucket may has different way to append children + this._appendChildrenToDiv(this._div); + } + _appendChildrenToDiv(containerDiv: HTMLElement) { + for (const child of this._children) { + child.draw(); + containerDiv.appendChild(child.getDiv()); + } + } + draw() { super.draw(); this._div.classList.add("webamp--img"); // It seems Groups are not responsive to click events. if (this._movable || this._resizable) { - // this._div.style.removeProperty('pointer-events'); this._div.style.pointerEvents = "auto"; } else { this._div.style.pointerEvents = "none"; } //TODO: allow move/resize if has ._image this._div.style.pointerEvents = "none"; - // this._div.style.overflow = "hidden"; this._renderBackground(); - for (const child of this._children) { - child.draw(); - this._div.appendChild(child.getDiv()); + this.appendChildrenDiv(); + if (this._autowidthsource) { + this._div.classList.add("autowidthsource"); } } } diff --git a/packages/webamp-modern-2/src/skin/makiClasses/GroupXFade.ts b/packages/webamp-modern-2/src/skin/makiClasses/GroupXFade.ts new file mode 100644 index 00000000..16685bc6 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/GroupXFade.ts @@ -0,0 +1,106 @@ +import Group from "./Group"; +import { findLast, num, px, removeAllChildNodes, toBool } from "../../utils"; +import UI_ROOT from "../../UIRoot"; +import { XmlElement } from "@rgrove/parse-xml"; +import GuiObj from "./GuiObj"; + +export default class GroupXFade extends Group { + _speed: number = null; + _activeChild: GuiObj = null; + + getElTag(): string { + return "group"; + } + + setXmlAttr(_key: string, value: string): boolean { + const key = _key.toLowerCase(); + if (super.setXmlAttr(key, value)) { + return true; + } + + switch (key) { + case "speed": + this._speed = num(value); + break; + case "groupid": + // this._speed = num(value); + console.log("xFade new groupid", value); + this._switchTo(value.toLowerCase()); + break; + default: + return false; + } + return true; + } + + handleAction( + action: string, + param: string | null = null, + actionTarget: string | null = null + ) { + // if(action.toLowerCase().startsWith('switchto;')){ + // UI_ROOT.vm.dispatch(this, 'onaction', [ + + // ]) + // } + switch (action.toLowerCase()) { + case "groupid": + // this._switchTo(action.toLowerCase()); + return true; + case "switchto": + // switchto seem as controlled by maki, + // which in turn call setXmlParam("groupid", grp) + // so we do nothing here + break; + } + return false; + } + + init() { + super.init(); + } + + async _switchTo(group_id: string) { + // hide current page + if (this._activeChild) this._fadeOut(this._activeChild); + + let child = findLast(this._children, (c) => c.getId() == group_id); + if (child == null) { + const dummyNode = new XmlElement("dummy", { + id: group_id, + w: "0", + h: "0", + relatw: "1", + relath: "1", + alpha: "0", + }); + child = await UI_ROOT._parser.group(dummyNode, this); + child.draw(); + child.init(); + this._div.appendChild(child.getDiv()); + } + this._activeChild = child; + this._fadeIn(child); + } + + // hide slowly + async _fadeOut(child: GuiObj) { + child._div.classList.add("fading-out"); + child.setalpha(0); + setTimeout(() => { + child._div.classList.remove("fading-out"); + }, this._speed); + } + //show slowly + async _fadeIn(child: GuiObj) { + child.setalpha(255); + // + } + + draw() { + super.draw(); + this._div.classList.add("x-fade"); + this._div.style.setProperty("--fade-in-speed", `${this._speed}s`); + this._div.style.setProperty("--fade-out-speed", `${this._speed / 2}s`); + } +} diff --git a/packages/webamp-modern-2/src/skin/makiClasses/GuiObj.ts b/packages/webamp-modern-2/src/skin/makiClasses/GuiObj.ts index a1042634..360e0e4d 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/GuiObj.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/GuiObj.ts @@ -1,8 +1,17 @@ import UI_ROOT from "../../UIRoot"; -import { assert, num, toBool, px, assume, relative } from "../../utils"; +import { + assert, + num, + toBool, + px, + assume, + relative, + findLast, +} from "../../utils"; import Bitmap from "../Bitmap"; import Group from "./Group"; import XmlObj from "../XmlObj"; +import Layout from "./Layout"; let BRING_LEAST: number = -1; let BRING_MOST_TOP: number = 1; @@ -11,9 +20,11 @@ let BRING_MOST_TOP: number = 1; export default class GuiObj extends XmlObj { static GUID = "4ee3e1994becc636bc78cd97b028869c"; _parent: Group; + _children: GuiObj[] = []; _id: string; - _width: number; - _height: number; + _name: string; + _width: number = 0; + _height: number = 0; _x: number = 0; _y: number = 0; _minimumHeight: number = 0; @@ -24,14 +35,12 @@ export default class GuiObj extends XmlObj { _relaty: string; _relatw: string; _relath: string; - // _resize: string; + _autowidthsource: string; _droptarget: string; _visible: boolean = true; _alpha: number = 255; _ghost: boolean = false; _sysregion: number = 0; - // _movable: boolean = false; - // _resizable: number = 0; _tooltip: string = ""; _targetX: number | null = null; _targetY: number | null = null; @@ -42,8 +51,8 @@ export default class GuiObj extends XmlObj { _goingToTarget: boolean = false; _div: HTMLElement; _backgroundBitmap: Bitmap | null = null; - // _resizingEventsRegisterd: boolean = false; - // _movingEventsRegisterd: boolean = false; + + _metaCommands: XmlElement[] = []; constructor() { super(); @@ -67,6 +76,13 @@ export default class GuiObj extends XmlObj { case "id": this._id = value.toLowerCase(); break; + case "name": + this._name = value; + break; + + case "autowidthsource": + this._autowidthsource = value.toLowerCase(); + break; case "w": case "default_w": this._width = num(value); @@ -114,6 +130,12 @@ export default class GuiObj extends XmlObj { case "droptarget": this._droptarget = value; break; + case "dblclickaction": + const [action, param, actionTarget] = value.split(";"); + this._div.addEventListener("dblclick", (e) => { + this.dispatchAction(action, param, actionTarget); + }); + break; case "ghost": this._ghost = toBool(value); break; @@ -121,6 +143,11 @@ export default class GuiObj extends XmlObj { this._visible = toBool(value); this._renderVisibility(); break; + case "activealpha": + case "inactivealpha": + this._div.setAttribute(key, value); // set directly to html attribute + break; + case "tooltip": this._tooltip = value; break; @@ -136,38 +163,53 @@ export default class GuiObj extends XmlObj { return true; } - init() { - this._div.addEventListener("mousedown", (e) => { - e.stopPropagation(); - /* - if (this._backgroundBitmap != null) { - const { clientX, clientY } = e; - const { x, y } = this._div.getBoundingClientRect(); - const canvasX = clientX - x; - const canvasY = clientY - y; - const canvas = this._backgroundBitmap.getCanvas(); - const ctx = canvas.getContext("2d"); + setxmlparam(key: string, value: string) { + this.setXmlAttr(key, value); + } - const opacity = ctx.getImageData(canvasX, canvasY, 1, 1).data[3]; - if (opacity === 0) { - this._div.style.pointerEvents = "none"; - const newTarget = document.elementFromPoint(clientX, clientY); - this._div.style.pointerEvents = "auto"; - var newEvent = new MouseEvent("click", { - clientX, - clientY, - bubbles: true, - }); - newTarget.dispatchEvent(newEvent); - return; + setSize(newWidth: number, newHeight: number) {} + + init() { + //process and + for (const node of this._metaCommands) { + const cmd = node.name.toLowerCase(); + const el = node.attributes.group + ? this.findobject(node.attributes.group) + : this; + const targets_ids = node.attributes.target.split(";"); + for (const target_id of targets_ids) { + // individual target + const gui = el.findobjectF( + target_id, + `<${cmd}(${target_id})=notfound. @${this.getId()}` + ); + if (gui == null) { + continue; + } + if (cmd == "sendparams") { + for (let attribute in node.attributes) { + if (gui && attribute != "target") { + gui.setxmlparam(attribute, node.attributes[attribute]); + } + } + } else if (cmd == "hideobject" && target_id != "close") { + gui.hide(); } } - */ - this.onLeftButtonDown(e.clientX, e.clientY); + } - const mouseUpHandler = (e) => { - // e.stopPropagation(); - this.onLeftButtonUp(e.clientX, e.clientY); + this._div.addEventListener("mousedown", (e) => { + e.stopPropagation(); + this.onLeftButtonDown( + e.offsetX + this.getleft(), + e.offsetY + this.gettop() + ); + + const mouseUpHandler = (e: MouseEvent) => { + this.onLeftButtonUp( + e.offsetX + this.getleft(), + e.offsetY + this.gettop() + ); this._div.removeEventListener("mouseup", mouseUpHandler); }; this._div.addEventListener("mouseup", mouseUpHandler); @@ -186,7 +228,7 @@ export default class GuiObj extends XmlObj { } getId(): string { - return this._id; + return this._id || ''; } /** @@ -204,6 +246,9 @@ export default class GuiObj extends XmlObj { this._visible = false; this._renderVisibility(); } + isvisible(): boolean { + return this._visible; + } /** * Get the Y position, in the screen, of the @@ -271,6 +316,101 @@ export default class GuiObj extends XmlObj { this._renderDimensions(); } + getxmlparam(param: string): string { + const _ = this["_" + param]; + return _ != null ? _.toString() : null; + } + getguiw(): number { + return this._width; + } + getguih(): number { + return this._height; + } + getguix(): number { + return this._x; + } + getguiy(): number { + return this._y; + } + getguirelatw(): number { + return this._relatw == "1" ? 1 : 0; + } + getguirelath(): number { + return this._relath == "1" ? 1 : 0; + } + getguirelatx(): number { + return this._relatx == "1" ? 1 : 0; + } + getguirelaty(): number { + return this._relaty == "1" ? 1 : 0; + } + getautowidth(): number { + const child = !this._autowidthsource + ? this + : findLast( + this._children, + (c) => c._id.toLowerCase() == this._autowidthsource + ); + if (child) { + return child._div.getBoundingClientRect().width; + } + return 1; + } + getautoheight(): number { + return this._div.getBoundingClientRect().height; + } + + findobject(id: string): GuiObj { + if (id.toLowerCase() == this.getId().toLowerCase()) return this; + + //? Phase 1: find in this children + let ret = this._findobject(id); + + //? Phase 2: find in this layout's children + if (!ret /* && this._parent */) { + const layout = this.getparentlayout(); + if (layout) { + ret = layout._findobject(id); + } + } + if (!ret && id != "sysmenu") { + console.warn(`findObject(${id}) failed, @${this.getId()}`); + } + return ret; + } + + /* internal findObject with custom error msg */ + findobjectF(id: string, msg: string): GuiObj { + const ret = this._findobject(id); + if (!ret && id != "sysmenu") { + console.warn(msg); + } + return ret; + } + + _findobject(id: string): GuiObj { + // too complex to consol.log here + const lower = id.toLowerCase(); + // find in direct children first + for (const obj of this._children) { + if ((obj.getId() || "").toLowerCase() === lower) { + return obj; + } + } + // find in grand child + for (const obj of this._children) { + const found = obj._findobject(id); + if (found != null) { + return found; + } + } + return null; + } + + isActive(): boolean { + return this._div.matches(":focus"); + } + /** * Hookable. Event happens when the left mouse * button was previously down and is now up. @@ -476,6 +616,13 @@ export default class GuiObj extends XmlObj { window.requestAnimationFrame(update); } + /** + * isGoingToTarget() + */ + isgoingtotarget() { + return this._goingToTarget; + } + /** * Experimental/unused */ @@ -523,10 +670,6 @@ export default class GuiObj extends XmlObj { assume(false, "Unimplemented"); } - /** - * isGoingToTarget() - */ - // [WHERE IS THIS?] // modifies the x/y targets so that they compensate for gained width/height. useful to make drawers that open up without jittering @@ -561,7 +704,27 @@ export default class GuiObj extends XmlObj { return this._alpha; } - getparentlayout(): Group { + clienttoscreenx(x: number): number { + return x; + } + + clienttoscreeny(y: number): number { + return y; + } + + screentoclientx(x: number): number { + return x; + } + + screentoclienty(y: number): number { + return y; + } + + getparent(): Group { + return this._parent; + } + + getparentlayout(): Layout { if (this._parent) { return this._parent.getparentlayout(); } @@ -577,11 +740,22 @@ export default class GuiObj extends XmlObj { this._div.style.zIndex = String(BRING_LEAST); } + setenabled(onoff:boolean|number){ + //TODO: + } + handleAction( action: string, - param: string | null, - actionTarget: string | null + param: string | null = null, + actionTarget: string | null = null ): boolean { + if (actionTarget) { + const guiObj = this.findobject(actionTarget); + if (guiObj) { + guiObj.handleAction(action, param); + return true; + } + } return false; } @@ -597,6 +771,26 @@ export default class GuiObj extends XmlObj { } } + sendaction( + action: string, + param: string, + x: number, + y: number, + p1: number, + p2: number, + source: GuiObj, + ): number { + return UI_ROOT.vm.dispatch(this, "onaction", [ + { type: "STRING", value: action }, + { type: "STRING", value: param }, + { type: "INT", value: x }, + { type: "INT", value: y }, + { type: "INT", value: p1 }, + { type: "INT", value: p2 }, + { type: "OBJECT", value: source }, + ]); + } + _renderAlpha() { if (this._alpha != 255) { this._div.style.opacity = `${this._alpha / 255}`; @@ -643,6 +837,15 @@ export default class GuiObj extends XmlObj { this._renderHeight(); } + doResize() { + UI_ROOT.vm.dispatch(this, "onresize", [ + { type: "INT", value: 0 }, + { type: "INT", value: 0 }, + { type: "INT", value: this.getwidth() }, + { type: "INT", value: this.getheight() }, + ]); + } + setBackgroundImage(bitmap: Bitmap | null) { this._backgroundBitmap = bitmap; if (bitmap != null) { @@ -680,7 +883,7 @@ export default class GuiObj extends XmlObj { if (this._tooltip) { this._div.setAttribute("title", this._tooltip); } - if (this._ghost) { + if (this._ghost || this._sysregion == -2) { this._div.style.pointerEvents = "none"; } else { this._div.style.pointerEvents = "auto"; diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Layer.ts b/packages/webamp-modern-2/src/skin/makiClasses/Layer.ts index 88503d8d..159007d7 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Layer.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Layer.ts @@ -34,7 +34,7 @@ export default class Layer extends Movable { } if (this._image != null) { const bitmap = UI_ROOT.getBitmap(this._image); - return bitmap.getHeight(); + if(bitmap) return bitmap.getHeight(); } return super.getheight(); } @@ -46,7 +46,7 @@ export default class Layer extends Movable { } if (this._image != null) { const bitmap = UI_ROOT.getBitmap(this._image); - return bitmap.getWidth(); + if(bitmap) return bitmap.getWidth(); } return super.getwidth(); } @@ -60,7 +60,7 @@ export default class Layer extends Movable { if (this._sysregion == 1 && this._image) { const canvas = UI_ROOT.getBitmap(this._image).getCanvas(); const edge = new Edges(); - edge.parseCanvasTransparency(canvas); + edge.parseCanvasTransparency(canvas, this.getwidth(), this.getheight()); if (edge.isSimpleRect()) { this.setXmlAttr("sysregion", "0"); } else { diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Layout.ts b/packages/webamp-modern-2/src/skin/makiClasses/Layout.ts index da6aed05..6b6a7e5c 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Layout.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Layout.ts @@ -14,13 +14,13 @@ import { px } from "../../utils"; // -- http://wiki.winamp.com/wiki/Modern_Skin:_Container export default class Layout extends Group { static GUID = "60906d4e482e537e94cc04b072568861"; - _parentContainer: Container | null = null; _resizingDiv: HTMLDivElement = null; _resizing: boolean = false; _resizable: number = 0; // combination of 4 directions: N/E/W/S _movingStartX: number; //container XY _movingStartY: number; _moving: boolean = false; + _snap = { left: 0, top: 0, right: 0, bottom: 0 }; constructor() { super(); @@ -41,8 +41,26 @@ export default class Layout extends Group { return true; } - setParentContainer(container: Container) { - this._parentContainer = container; + // setParent(container: Container) { + // this._parent = container; + // } + + getcontainer(): Container { + return this._parent as unknown as Container; + } + + gettop(): number { + return this._parent._y; + } + + /** + * Get the X position, in the screen, of the + * left edge of the object. + * + * @ret The left edge's position (in screen coordinates). + */ + getleft(): number { + return this._parent._x; } dispatchAction( @@ -60,12 +78,44 @@ export default class Layout extends Group { } switch (action) { default: - if (this._parentContainer != null) { - this._parentContainer.dispatchAction(action, param, actionTarget); + if (this._parent != null) { + this._parent.dispatchAction(action, param, actionTarget); } } } + snapadjust(left: number, top: number, right: number, bottom: number) { + this._snap.left = left; + this._snap.top = top; + this._snap.right = right; + this._snap.bottom = bottom; + } + + beforeredock() { + // TODO: + } + + redock() { + // TODO: + } + + getsnapadjustbottom(): number { + return 100; + } + + clienttoscreenh(h: number): number { + return h; + } + + islayoutanimationsafe(): boolean { + return true; + } + + init() { + super.init(); + this._invalidateSize(); + } + setResizing(cmd: string, dx: number, dy: number) { const clampW = (w): number => { w = this._maximumWidth ? Math.min(w, this._maximumWidth) : w; @@ -115,7 +165,7 @@ export default class Layout extends Group { this._resizing = false; this.setXmlAttr("w", this._resizingDiv.offsetWidth.toString()); this.setXmlAttr("h", this._resizingDiv.offsetHeight.toString()); - const container = this._parentContainer; + const container = this._parent; container.setXmlAttr( "x", (container._x + this._resizingDiv.offsetLeft).toString() @@ -126,12 +176,13 @@ export default class Layout extends Group { ); this._resizingDiv.remove(); this._resizingDiv = null; + this._invalidateSize(); } } // MOVING THINGS ===================== setMoving(cmd: string, dx: number, dy: number) { - const container = this._parentContainer; + const container = this._parent; if (cmd == "start") { this._moving = true; this._movingStartX = container._x; diff --git a/packages/webamp-modern-2/src/skin/makiClasses/LayoutStatus.ts b/packages/webamp-modern-2/src/skin/makiClasses/LayoutStatus.ts new file mode 100644 index 00000000..7816b5a2 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/LayoutStatus.ts @@ -0,0 +1,22 @@ +import GuiObj from "./GuiObj"; + +// Maybe this? +// http://wiki.winamp.com/wiki/ +export default class LayoutStatus extends GuiObj { + static GUID = "7fd5f21048dfacc45154a0a676dc6c57"; + + setXmlAttr(key: string, value: string): boolean { + if (super.setXmlAttr(key, value)) { + return true; + } + switch (key) { + default: + return false; + } + return true; + } + + callme(str: string){ + console.log('callme:', str) + } +} diff --git a/packages/webamp-modern-2/src/skin/makiClasses/MapObj.ts b/packages/webamp-modern-2/src/skin/makiClasses/MapObj.ts new file mode 100644 index 00000000..1f767a15 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/MapObj.ts @@ -0,0 +1,45 @@ +import UI_ROOT from "../../UIRoot"; +import { assert, px, removeAllChildNodes, toBool } from "../../utils"; +import Layout from "./Layout"; +import XmlObj from "../XmlObj"; + +// > A container is a top level object and it basically represents a window. +// > Nothing holds a container. It is an object that holds multiple related +// > layouts. Each layout represents an appearance for that window. You can design +// > different layouts for each window but only one can be visible at a time. +// +// -- http://wiki.winamp.com/wiki/Modern_Skin +export default class MapObj extends XmlObj { + static GUID = "38603665461B42a7AA75D83F6667BF73"; + + _layouts: Layout[] = []; + _activeLayout: Layout | null = null; + _visible: boolean = true; + _id: string; + _name: string; + constructor() { + super(); + } + + setXmlAttr(_key: string, value: string): boolean { + const key = _key.toLowerCase(); + if (super.setXmlAttr(key, value)) { + return true; + } + switch (key) { + case "name": + this._name = value; + break; + case "id": + this._id = value.toLowerCase(); + break; + case "default_visible": + this._visible = toBool(value); + break; + default: + return false; + } + return true; + } + +} diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Movable.ts b/packages/webamp-modern-2/src/skin/makiClasses/Movable.ts index cda2a4a1..bc4c02b1 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Movable.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Movable.ts @@ -2,7 +2,7 @@ * this file is needed to workaround of button-moving-layout issue */ -import { toBool } from "../../utils"; +import { throttle, toBool } from "../../utils"; import GuiObj from "./GuiObj"; import Layout from "./Layout"; @@ -141,7 +141,7 @@ export default class Movable extends GuiObj { const deltaX = newMouseX - startX; layout.setResizing("final", deltaX, deltaY); }; - document.addEventListener("mousemove", handleMove); + document.addEventListener("mousemove", throttle(handleMove, 75)); document.addEventListener("mouseup", handleMouseUp); }; @@ -187,17 +187,18 @@ export default class Movable extends GuiObj { const deltaX = newMouseX - startX; layout.setMoving("final", deltaX, deltaY); }; - document.addEventListener("mousemove", handleMove); + document.addEventListener("mousemove", throttle(handleMove, 50)); document.addEventListener("mouseup", handleMouseUp); }; draw() { super.draw(); - if (this._movable || this._resizable) { + if (this._ghost || this._sysregion == -2) { + this._div.style.pointerEvents = "none"; + } else if (this._movable || this._resizable) { this._div.style.pointerEvents = "auto"; } else if (this._ghost) { this._div.style.pointerEvents = "none"; - this._div.style.setProperty("--pointer-events-by", "movable"); } } } diff --git a/packages/webamp-modern-2/src/skin/makiClasses/ProgressGrid.ts b/packages/webamp-modern-2/src/skin/makiClasses/ProgressGrid.ts new file mode 100644 index 00000000..d7551939 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/ProgressGrid.ts @@ -0,0 +1,37 @@ +import Grid from "./Grid"; +import UI_ROOT from "../../UIRoot"; +import { px } from "../../utils"; + +// http://wiki.winamp.com/wiki/XML_GUI_Objects +export default class ProgressGrid extends Grid { +// static GUID = "5ab9fa1545579a7d5765c8aba97cc6a6"; +_disposeDisplaySubscription: () => void | null = null; + + constructor(){ + super(); + this._disposeDisplaySubscription = UI_ROOT.audio.onCurrentTimeChange( + () => { + this._middle.style.width = `${UI_ROOT.audio.getCurrentTimePercent()*100}%`; + } + ); + } + + setXmlAttr(key: string, value: string): boolean { + if (super.setXmlAttr(key, value)) { + return true; + } + switch (key) { + case "orientation": + break; + default: + return false; + } + return true; + } + + draw() { + super.draw() + this._div.style.removeProperty('display') + } + +} diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Slider.ts b/packages/webamp-modern-2/src/skin/makiClasses/Slider.ts index 1a6a5fcf..d5cfdc9b 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Slider.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Slider.ts @@ -1,11 +1,24 @@ import UI_ROOT from "../../UIRoot"; -import { assume, clamp, num, px } from "../../utils"; +import { assume, clamp, num, px, throttle } from "../../utils"; import GuiObj from "./GuiObj"; -interface ActionHandler { +class ActionHandler { + _slider: Slider; + constructor(slider: Slider) { + this._slider=slider; + } + _subscription: () => void = () => {}; // 0-255 - onsetposition(position: number): void; - dispose(): void; + onsetposition(position: number): void {} + onLeftMouseDown(x: number, y: number): void {} + onLeftMouseUp(x: number, y: number): void {} + onMouseMove(x: number, y: number): void { + this._slider._setPositionXY(x, y); + } + onFreeMouseMove(x: number, y: number): void {} + dispose(): void { + this._subscription(); + } } const MAX = 255; @@ -20,64 +33,107 @@ export default class Slider extends GuiObj { _downThumb: string; _hoverThumb: string; _action: string | null = null; - _low: number; - _high: number; + _low: number = 0; + _high: number = 1; + _thumbWidth: number = 0; + _thumbHeight: number = 0; _position: number = 0; _param: string | null = null; - _thumbDiv: HTMLDivElement = document.createElement("div"); + // _thumbDiv: HTMLDivElement = document.createElement("div"); _actionHandler: null | ActionHandler; + _onSetPositionEvenEaten: number; + _mouseX: number; + _mouseY: number; - constructor() { - super(); - this._thumbDiv.addEventListener("mousedown", (downEvent: MouseEvent) => { + getRealWidth() { + return this._div.getBoundingClientRect().width; + } + + /** + * set .position by X, Y + * where X,Y is mouseEvent.offsetX & Y + */ + _setPositionXY(x: number, y: number) { + //TODO: consider padding. where padding = thumbSize/2 + const width = this.getRealWidth() - this._thumbWidth; + const height = this.getheight() - this._thumbHeight; + const newPercent = this._vertical ? (height - y) / height : x / width; + this._position = clamp(newPercent, 0, 1); + this._renderThumbPosition(); + this.doSetPosition(this.getposition()); + } + + _registerDragEvents() { + // this._thumbDiv.addEventListener("mousedown", (downEvent: MouseEvent) => { + this._div.addEventListener("mousedown", (downEvent: MouseEvent) => { downEvent.stopPropagation(); - const bitmap = UI_ROOT.getBitmap(this._thumb); + if (downEvent.button != 0) return; // only care LeftButton + // const bitmap = UI_ROOT.getBitmap(this._thumb); + //TODO: change client/offset into pageX/Y const startX = downEvent.clientX; const startY = downEvent.clientY; - const width = this.getwidth() - bitmap.getWidth(); - const height = this.getheight() - bitmap.getHeight(); - const initialPostition = this._position; + const innerX = downEvent.offsetX; + const innerY = downEvent.offsetY; + // const width = this.getRealWidth() - this._thumbWidth; + // const height = this.getheight() - this._thumbHeight; + // const initialPostition = this._position; + // const newPercent = this._vertical ? startY / height : startX / width; + console.log("mouseDown:", downEvent.offsetX, downEvent.offsetY); + this.doLeftMouseDown(downEvent.offsetX, downEvent.offsetY); const handleMove = (moveEvent: MouseEvent) => { moveEvent.stopPropagation(); const newMouseX = moveEvent.clientX; const newMouseY = moveEvent.clientY; - const deltaY = newMouseY - startY; const deltaX = newMouseX - startX; + const deltaY = newMouseY - startY; - const deltaPercent = this._vertical ? deltaY / height : deltaX / width; - const newPercent = this._vertical - ? initialPostition - deltaPercent - : initialPostition + deltaPercent; + // const deltaPercent = this._vertical ? deltaY / height : deltaX / width; + // const newPercent = this._vertical + // ? initialPostition - deltaPercent + // : initialPostition + deltaPercent; - this._position = clamp(newPercent, 0, 1); - this._renderThumbPosition(); - this.onsetposition(this.getposition()); + // this._position = clamp(newPercent, 0, 1); + // this._renderThumbPosition(); + // this.doSetPosition(this.getposition()); + //below is mousePosition conversion relative to inner _div + this.doMouseMove(innerX + deltaX, innerY + deltaY); }; + const throttleMouseMove = throttle(handleMove,50) + const handleMouseUp = (upEvent: MouseEvent) => { upEvent.stopPropagation(); - UI_ROOT.vm.dispatch(this, "onsetfinalposition", [ - { type: "INT", value: this.getposition() }, - ]); - UI_ROOT.vm.dispatch(this, "onpostedposition", [ - { type: "INT", value: this.getposition() }, - ]); - document.removeEventListener("mousemove", handleMove); + if (upEvent.button != 0) return; // only care LeftButton + + document.removeEventListener("mousemove", throttleMouseMove); document.removeEventListener("mouseup", handleMouseUp); + this.doLeftMouseUp(upEvent.offsetX, upEvent.offsetY); }; - document.addEventListener("mousemove", handleMove); + document.addEventListener("mousemove", throttleMouseMove); document.addEventListener("mouseup", handleMouseUp); }); + + //? free mouse move, currently only Equalizers use it. ============= + this._div.addEventListener("mousemove", (moveEvent: MouseEvent) => { + // moveEvent.stopPropagation(); + this.doFreeMouseMove(moveEvent.offsetX, moveEvent.offsetY); + }); } - setXmlAttr(key: string, value: string): boolean { + + setXmlAttr(_key: string, value: string): boolean { + const key = _key.toLowerCase(); if (super.setXmlAttr(key, value)) { + if (key == "action") this._setAction(value); return true; } - switch (key) { + switch (key.toLowerCase()) { case "thumb": // (id) The bitmap element for the slider thumb. this._thumb = value; + const bitmap = UI_ROOT.getBitmap(this._thumb); + this._thumbWidth = bitmap.getWidth(); + this._thumbHeight = bitmap.getHeight(); break; case "downthumb": // (id) The bitmap element for the slider thumb when held by the user. @@ -126,7 +182,9 @@ export default class Slider extends GuiObj { } init() { + // console.log("SLIDER-INITED!"); this._initializeActionHandler(); + this._registerDragEvents(); } _initializeActionHandler() { @@ -135,7 +193,11 @@ export default class Slider extends GuiObj { this._actionHandler = new SeekActionHandler(this); break; case "eq_band": - this._actionHandler = new EqActionHandler(this, this._param); + if (this._param == "preamp") + this._actionHandler = new PreampActionHandler(this, this._param); + else this._actionHandler = new EqActionHandler(this, this._param); + break; + case "eq_preamp": break; case "pan": this._actionHandler = new PanActionHandler(this); @@ -171,47 +233,109 @@ export default class Slider extends GuiObj { } onsetposition(newPos: number) { - UI_ROOT.vm.dispatch(this, "onsetposition", [ + this._onSetPositionEvenEaten = UI_ROOT.vm.dispatch(this, "onsetposition", [ + //needed by seekerGhost { type: "INT", value: newPos }, ]); + } + doSetPosition(newPos: number) { + this.onsetposition(newPos); if (this._actionHandler != null) { this._actionHandler.onsetposition(newPos); } } + doLeftMouseDown(x: number, y: number) { + this._setPositionXY(x, y); + UI_ROOT.vm.dispatch(this, "onleftbuttondown", [ + { type: "INT", value: x }, + { type: "INT", value: y }, + ]); + if (this._actionHandler != null) { + this._actionHandler.onLeftMouseDown(x, y); + } + } + doMouseMove(x: number, y: number) { + if (this._actionHandler != null) { + this._actionHandler.onMouseMove(x, y); + } + } + doLeftMouseUp(x: number, y: number) { + // console.log("slider.doLeftMouseUp"); + UI_ROOT.vm.dispatch(this, "onleftbuttonup", [ + { type: "INT", value: x }, + { type: "INT", value: y }, + ]); + UI_ROOT.vm.dispatch(this, "onsetfinalposition", [ + { type: "INT", value: this.getposition() }, + ]); + UI_ROOT.vm.dispatch(this, "onpostedposition", [ + { type: "INT", value: this.getposition() }, + ]); + if (this._actionHandler != null) { + // console.log("slider_ACTION.doLeftMouseUp"); + this._actionHandler.onLeftMouseUp(x, y); + } + } + + doFreeMouseMove(x: number, y: number) { + // UI_ROOT.vm.dispatch(this, "onleftbuttondown", [ + // { type: "INT", value: x }, + // { type: "INT", value: y }, + // ]); + if (this._actionHandler != null) { + this._actionHandler.onFreeMouseMove(x, y); + } + } + _renderThumb() { - this._thumbDiv.style.position = "absolute"; - this._thumbDiv.setAttribute("data-obj-name", "Slider::Handle"); - this._thumbDiv.classList.add("webamp--img"); + // this._thumbDiv.style.position = "absolute"; + // this._thumbDiv.setAttribute("data-obj-name", "Slider::Handle"); + // this._thumbDiv.classList.add("webamp--img"); if (this._thumb != null) { const bitmap = UI_ROOT.getBitmap(this._thumb); - this._thumbDiv.style.width = px(bitmap.getWidth()); - this._thumbDiv.style.height = px(bitmap.getHeight()); - bitmap.setAsBackground(this._thumbDiv); + // this._thumbDiv.style.width = px(bitmap.getWidth()); + // this._thumbDiv.style.height = px(bitmap.getHeight()); + // bitmap.setAsBackground(this._thumbDiv); + + bitmap._setAsBackground(this._div, "thumb-"); + this._div.style.setProperty("--thumb-width", px(bitmap.getWidth())); + this._div.style.setProperty("--thumb-height", px(bitmap.getHeight())); } if (this._downThumb != null) { const bitmap = UI_ROOT.getBitmap(this._downThumb); - bitmap.setAsActiveBackground(this._thumbDiv); + // bitmap.setAsDownBackground(this._thumbDiv); + + bitmap._setAsBackground(this._div, "thumb-down-"); } if (this._hoverThumb != null) { const bitmap = UI_ROOT.getBitmap(this._hoverThumb); - bitmap.setAsHoverBackground(this._thumbDiv); + // bitmap.setAsHoverBackground(this._thumbDiv); + + bitmap._setAsBackground(this._div, "thumb-hover-"); } } _renderThumbPosition() { if (this._thumb != null) { - const bitmap = UI_ROOT.getBitmap(this._thumb); + // const bitmap = UI_ROOT.getBitmap(this._thumb); // TODO: What if the orientation has changed? if (this._vertical) { const top = - (1 - this._position) * (this.getheight() - bitmap.getHeight()); - this._thumbDiv.style.top = px(top); + (1 - this._position) * (this.getheight() - this._thumbHeight); + // this._thumbDiv.style.top = px(top); + + this._div.style.setProperty("--thumb-top", px(top)); } else { - const left = this._position * (this.getwidth() - bitmap.getWidth()); - this._thumbDiv.style.left = px(left); + // const left = (1 - this._position * (this.getwidth() - bitmap.getWidth()); + const curwidth = this.getRealWidth(); + const left = this._position * (curwidth - this._thumbWidth); + // console.log('thumb.left', this._position, left, 'w:',this.getwidth(),'bmp.w:', bitmap.getWidth()) + // this._thumbDiv.style.left = px(left); + + this._div.style.setProperty("--thumb-left", px(left)); } } } @@ -224,7 +348,7 @@ export default class Slider extends GuiObj { assume(this._barMiddle == null, "Need to handle Slider barmiddle"); this._renderThumb(); this._renderThumbPosition(); - this._div.appendChild(this._thumbDiv); + // this._div.appendChild(this._thumbDiv); } dispose() { @@ -251,31 +375,101 @@ export default class Slider extends GuiObj { **/ // eslint-disable-next-line rulesdir/proper-maki-types -class SeekActionHandler implements ActionHandler { - _subscription: () => void; + +class SeekActionHandler extends ActionHandler { + _pendingChange: boolean; + + isPendingChange(): boolean { + return true; // this._pendingChange || this._dragging; + // return this._dragging == true; + } + constructor(slider: Slider) { - const update = () => { - slider._position = UI_ROOT.audio.getCurrentTimePercent(); + super(slider); + this._registerOnAudioProgress(); + } + + _registerOnAudioProgress() { + this._subscription = UI_ROOT.audio.onCurrentTimeChange( + this._onAudioProgres + ); + } + + _onAudioProgres = () => { + if (!this._pendingChange) { + if (this._slider.getId() == "seekerghost") + console.log("thumb: not isPending()!"); + this._slider._position = UI_ROOT.audio.getCurrentTimePercent(); // TODO: We could throttle this, or only render if the change is "significant"? + this._slider._renderThumbPosition(); + } + }; + + onsetposition(position: number): void { + // console.log("seek:", position); + this._pendingChange = this._slider._onSetPositionEvenEaten != 0; + if (!this._pendingChange) { + UI_ROOT.audio.seekToPercent(position / MAX); + } + } + + // onLeftMouseDown(x: number, y: number) {} + onLeftMouseUp(x: number, y: number) { + // console.log("slider_ACTION.doLeftMouseUp"); + if (this._pendingChange) { + this._pendingChange = false; + UI_ROOT.audio.seekToPercent(this._slider.getposition() / MAX); + } + } +} + +const EqGlobalVar = { eqMouseDown: false, targetSlider: null }; +// eslint-disable-next-line rulesdir/proper-maki-types +class EqActionHandler extends ActionHandler { + _kind: string; + + constructor(slider: Slider, kind: string) { + super(slider); + this._kind = kind; + const update = () => { + slider._position = UI_ROOT.audio.getEq(kind); slider._renderThumbPosition(); }; update(); - this._subscription = UI_ROOT.audio.onCurrentTimeChange(update); + this._subscription = UI_ROOT.audio.onEqChange(kind, update); + } + + onLeftMouseDown(x: number, y: number): void { + EqGlobalVar.eqMouseDown = true; + } + onLeftMouseUp(x: number, y: number): void { + EqGlobalVar.eqMouseDown = false; + } + + onFreeMouseMove(x: number, y: number): void { + if (EqGlobalVar.eqMouseDown) { + EqGlobalVar.targetSlider = this._slider; + this._slider._setPositionXY(x, y); + } + } + + onMouseMove(x: number, y: number): void { + if (EqGlobalVar.eqMouseDown && EqGlobalVar.targetSlider) { + // send mouse pos to last hovered slider + EqGlobalVar.targetSlider._setPositionXY(x, y); + } } onsetposition(position: number): void { - UI_ROOT.audio.seekToPercent(position / MAX); - } - dispose(): void { - this._subscription(); + UI_ROOT.audio.setEq(this._kind, position / MAX); } } // eslint-disable-next-line rulesdir/proper-maki-types -class EqActionHandler implements ActionHandler { - _subscription: () => void; +class PreampActionHandler extends ActionHandler { _kind: string; constructor(slider: Slider, kind: string) { + super(slider); this._kind = kind; const update = () => { slider._position = UI_ROOT.audio.getEq(kind); @@ -288,37 +482,39 @@ class EqActionHandler implements ActionHandler { onsetposition(position: number): void { UI_ROOT.audio.setEq(this._kind, position / MAX); } - dispose(): void { - this._subscription(); +} + +// eslint-disable-next-line rulesdir/proper-maki-types +class PanActionHandler extends ActionHandler { + onsetposition(position: number): void { + // TODO } } // eslint-disable-next-line rulesdir/proper-maki-types -class PanActionHandler implements ActionHandler { - _subscription: () => void; +class VolumeActionHandler extends ActionHandler { + _changing: boolean = false; + constructor(slider: Slider) { - this._subscription = () => {}; + super(slider); + slider._position = UI_ROOT.audio.getVolume(); + slider._renderThumbPosition(); + + this._subscription = UI_ROOT.audio.onVolumeChanged(() => { + if (!this._changing) { + slider._position = UI_ROOT.audio.getVolume(); + slider._renderThumbPosition(); + } + }); } onsetposition(position: number): void { - // TODO + UI_ROOT.audio.setVolume(position / 255); } - dispose(): void { - this._subscription(); - } -} - -// eslint-disable-next-line rulesdir/proper-maki-types -class VolumeActionHandler implements ActionHandler { - _subscription: () => void; - constructor(slider: Slider) { - this._subscription = () => {}; - } - - onsetposition(position: number): void { - // TODO - } - dispose(): void { - this._subscription(); + onLeftMouseDown(x: number, y: number) { + this._changing = true; + } + onLeftMouseUp(x: number, y: number) { + this._changing = false; } } diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Status.ts b/packages/webamp-modern-2/src/skin/makiClasses/Status.ts index 2526f666..c1ca0330 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Status.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Status.ts @@ -1,17 +1,100 @@ import GuiObj from "./GuiObj"; +import UI_ROOT from "../../UIRoot"; +import { AUDIO_PAUSED, AUDIO_STOPPED, AUDIO_PLAYING } from "../AudioPlayer"; // Maybe this? // http://wiki.winamp.com/wiki/XML_GUI_Objects#.3CWasabi:StandardFrame:Status.2F.3E export default class Status extends GuiObj { static GUID = "0f08c9404b23af39c4b8f38059bb7e8f"; - setXmlAttr(key: string, value: string): boolean { + _stopbitmap: string; + _playbitmap: string; + _pausebitmap: string; + _state: string = AUDIO_STOPPED; + + constructor() { + super(); + UI_ROOT.audio.on("statchanged", () => this._updateStatus()); + } + + _updateStatus() { + this._state = UI_ROOT.audio.getState(); + this._renderBackground(); + } + + setXmlAttr(_key: string, value: string): boolean { + const key = _key.toLowerCase(); if (super.setXmlAttr(key, value)) { return true; } switch (key) { + case "stopbitmap": + this._stopbitmap = value; + this._renderBackground(); + break; + case "playbitmap": + this._playbitmap = value; + this._renderBackground(); + break; + case "pausebitmap": + this._pausebitmap = value; + this._renderBackground(); + break; default: return false; } return true; } + + // This shadows `getheight()` on GuiObj + getheight(): number { + if (this._height) { + return this._height; + } + if (this._stopbitmap != null) { + const bitmap = UI_ROOT.getBitmap(this._stopbitmap); + return bitmap ? bitmap.getHeight() : 15; + } + return super.getheight(); + } + + // This shadows `getwidth()` on GuiObj + getwidth(): number { + if (this._width) { + return this._width; + } + if (this._stopbitmap != null) { + const bitmap = UI_ROOT.getBitmap(this._stopbitmap); + return bitmap ? bitmap.getWidth() : 15; + } + return super.getwidth(); + } + + _renderBackground() { + let bitmap_id:string; + switch (this._state) { + case AUDIO_PLAYING: + bitmap_id = this._playbitmap; + break; + case AUDIO_PAUSED: + bitmap_id = this._pausebitmap; + break; + case AUDIO_STOPPED: + default: + bitmap_id = this._stopbitmap; + break; + } + const bitmap = UI_ROOT.getBitmap(bitmap_id); + if (bitmap != null) { + this.setBackgroundImage(bitmap); + } else { + this.setBackgroundImage(null); + } + } + + draw() { + super.draw(); + // this._div.setAttribute("data-obj-name", "Button"); + this._div.classList.add("webamp--img"); + this._renderBackground(); + } } diff --git a/packages/webamp-modern-2/src/skin/makiClasses/SystemObject.ts b/packages/webamp-modern-2/src/skin/makiClasses/SystemObject.ts index c9f1aaf7..56f426dd 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/SystemObject.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/SystemObject.ts @@ -8,12 +8,15 @@ import PRIVATE_CONFIG from "../PrivateConfig"; import UI_ROOT from "../../UIRoot"; import GuiObj from "./GuiObj"; +import { AUDIO_PAUSED, AUDIO_STOPPED, AUDIO_PLAYING } from "../AudioPlayer"; + const MOUSE_POS = { x: 0, y: 0 }; // TODO: Figure out how this could be unsubscribed eventually document.addEventListener("mousemove", (e: MouseEvent) => { - MOUSE_POS.x = e.clientX; - MOUSE_POS.y = e.clientY; + // https://stackoverflow.com/questions/6073505/what-is-the-difference-between-screenx-y-clientx-y-and-pagex-y + MOUSE_POS.x = e.pageX; + MOUSE_POS.y = e.pageY; }); export default class SystemObject extends BaseObject { @@ -21,16 +24,44 @@ export default class SystemObject extends BaseObject { _parentGroup: Group; _parsedScript: ParsedMaki; _param: string; + _id: string; - constructor(parsedScript: ParsedMaki, param: string) { + constructor(parsedScript: ParsedMaki, param: string, id: string) { super(); this._parsedScript = parsedScript; this._param = param; + this._id = id; // useful while debuggin UI_ROOT.audio.onSeek(() => { UI_ROOT.vm.dispatch(this, "onseek", [ { type: "INT", value: UI_ROOT.audio.getCurrentTimePercent() * 255 }, ]); }); + UI_ROOT.audio.on("play", () => UI_ROOT.vm.dispatch(this, "onplay", [])); + UI_ROOT.audio.on("pause", () => UI_ROOT.vm.dispatch(this, "onpause", [])); + UI_ROOT.audio.on("stop", () => UI_ROOT.vm.dispatch(this, "onstop", [])); + // UI_ROOT.audio.onPlay(() => UI_ROOT.vm.dispatch(this, "onplay", [])); + UI_ROOT.audio.onVolumeChanged(() => { + UI_ROOT.vm.dispatch(this, "onvolumechanged", [ + { type: "INT", value: UI_ROOT.audio.getVolume() * 255 }, + ]); + }); + const EqBandHandle = (band: number) => { + // console.log('eq.changed:',band, UI_ROOT.audio.getEq(String(band))) + UI_ROOT.vm.dispatch(this, "oneqbandchanged", [ + { type: "INT", value: band }, + { type: "INT", value: UI_ROOT.audio.getEq(String(band)) * 255 - 127 }, + ]); + }; + UI_ROOT.audio.onEqChange("1", () => EqBandHandle(1)); + UI_ROOT.audio.onEqChange("2", () => EqBandHandle(2)); + UI_ROOT.audio.onEqChange("3", () => EqBandHandle(3)); + UI_ROOT.audio.onEqChange("4", () => EqBandHandle(4)); + UI_ROOT.audio.onEqChange("5", () => EqBandHandle(5)); + UI_ROOT.audio.onEqChange("6", () => EqBandHandle(6)); + UI_ROOT.audio.onEqChange("7", () => EqBandHandle(7)); + UI_ROOT.audio.onEqChange("8", () => EqBandHandle(8)); + UI_ROOT.audio.onEqChange("9", () => EqBandHandle(9)); + UI_ROOT.audio.onEqChange("10", () => EqBandHandle(10)); } init() { @@ -426,7 +457,7 @@ export default class SystemObject extends BaseObject { * @param defvalue The default value to return if no item is found. */ getprivatestring(section: string, item: string, defvalue: string) { - // TODO + return PRIVATE_CONFIG.getPrivateString(section, item, defvalue); } setpublicstring(item: string, value: string) { @@ -984,7 +1015,7 @@ export default class SystemObject extends BaseObject { * @param value The integer to change into a string. */ integertostring(value: number): string { - return String(value); + return String(Math.round(value)); } /** @@ -1133,8 +1164,17 @@ export default class SystemObject extends BaseObject { * @ret STATUS_PAUSED (-1) if paused, STATUS_STOPPED (0) if stopped, STATUS_PLAYING (1) if playing. */ getstatus(): number { - // TODO: Pull this from the actual media player - return 1; + const audioState = UI_ROOT.audio.getState(); + switch (audioState) { + case AUDIO_PLAYING: + return 1; + case AUDIO_PAUSED: + return -1; + case AUDIO_STOPPED: + return 0; + default: + console.warn("Unknown audio state:", audioState); + } } /** @@ -1562,6 +1602,42 @@ export default class SystemObject extends BaseObject { // TODO: Should this return an int? return Math.random() * max; } + + oneqfreqchanged(isiso: number) {} + + getsonginfotext(): string { + return "123kbps stereo 79khz"; + } + + getsonginfotexttranslated(): string { + return this.getplayitemstring(); + } + + lockui() { + //TODO: + } + + unlockui() { + //TODO: + } + + istransparencyavailable():boolean { + return true + } + + translate(str: string): string { + return str; + } + + isvideo(): number { + return 0; + } + isvideofullscreen(): number { + return 0; + } + iskeydown(vk: number): number { + return 0; + } } function dumpScriptDebug(script: ParsedMaki) { diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Text.ts b/packages/webamp-modern-2/src/skin/makiClasses/Text.ts index f92bb5ae..0f0fc539 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Text.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Text.ts @@ -8,7 +8,9 @@ import { num, px, toBool, + clamp, } from "../../utils"; +import Timer from "./Timer"; // http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Ctext.2F.3E_.26_.3CWasabi:Text.2F.3E export default class Text extends GuiObj { @@ -18,27 +20,41 @@ export default class Text extends GuiObj { _disposeDisplaySubscription: () => void | null = null; _text: string; _bold: boolean; - _forceupcase: boolean; _forceuppercase: boolean; - _forcelocase: boolean; _forcelowercase: boolean; _align: string; - _font: string; + _font_id: string; + _font_obj: TrueTypeFont | BitmapFont; _fontSize: number; _color: string; - _ticker: boolean; + _ticker: string = "off"; // "scroll" | "bounce" | "off" + _paddingX: number = 2; _timeColonWidth: number | null = null; + _textWrapper: HTMLElement; + _scrollTimer: Timer; + _scrollDirection: -1 | 1; + _scrollPaused: boolean = false; + _scrollLeft: number = 0; // logically, not visually + _textFullWidth: number; //calculated, not runtime by css + _drawn: boolean = false; // needed to check has parents + + constructor() { + super(); + this._textWrapper = document.createElement("wrap"); + this._div.appendChild(this._textWrapper); + } setXmlAttr(key: string, value: string): boolean { if (super.setXmlAttr(key, value)) { return true; } - switch (key) { + switch (key.toLowerCase()) { case "display": // (str) Either a specific system display string or the string identifier of a text feed. Setting this value will override the text parameter. See below. this._setDisplay(value); break; case "text": + case "default": // (str) A static string to be displayed. this._text = value; this._renderText(); @@ -46,19 +62,22 @@ export default class Text extends GuiObj { case "bold": // (str) A static string to be displayed. this._bold = toBool(value); + this._prepareCss(); break; case "forceupcase": - // (bool) Force the system to make the display string all uppercase before display. - this._forceupcase = toBool(value); - break; case "forceuppercase": // (bool) Force the system to make the display string all uppercase before display. this._forceuppercase = toBool(value); + this._prepareCss(); + this._renderText(); break; case "font": // (id) The id of a bitmapfont or truetypefont element. If no element with that id can be found, the OS will be asked for a font with that name instead. - this._font = value; - this._renderText(); + this._font_id = value; + this._autoDetectFontType(); + this.ensureFontSize(); + this._prepareCss(); + // this._renderText(); break; case "align": // (str) One of the following three possible strings: "left" "center" "right" -- Default is "left." @@ -67,6 +86,9 @@ export default class Text extends GuiObj { case "fontsize": // (int) The size to render the chosen font. this._fontSize = num(value); + //this._renderText(); // + this.ensureFontSize(); + this._invalidateFullWidth(); break; case "color": // (int[sic?]) The comma delimited RGB color of the text. @@ -74,11 +96,13 @@ export default class Text extends GuiObj { break; case "ticker": /// (bool) Setting this flag causes the object to scroll left and right if the text does not fit the rectangular area of the text object. - this._ticker = toBool(value); + if (value == "0") value = "off"; + this._ticker = value.toLowerCase(); break; case "timecolonwidth": // (int) How many extra pixels wider or smaller should the colon be when displaying time. Default is -1. this._timeColonWidth = num(value); + this._prepareCss(); this._renderText(); /* antialias - (bool) Setting this flag causes the text to be rendered antialiased if possible. @@ -105,6 +129,41 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x return true; } + _autoDetectFontType() { + if (this._font_id) { + this._font_obj = UI_ROOT.getFont(this._font_id); + if (!this._font_obj) { + const newFont = new TrueTypeFont(); + newFont._inlineFamily = this._font_id; + UI_ROOT.addFont(newFont); + this._font_obj = newFont; + } + } + } + + ensureFontSize() { + if (this._font_obj instanceof TrueTypeFont && this._fontSize) { + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + context.font = `${this._fontSize}px ${ + this._font_obj.getFontFamily() || "Arial" + }`; + const metrics = context.measureText("IWH"); + const fontHeight = + metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent; + + this._fontSize = + this._fontSize * (1 - (fontHeight - this._fontSize) / this._fontSize); + } + } + + init() { + super.init(); + if (this._ticker && this._ticker != "off") { + this._prepareScrolling(); + } + } + _setDisplay(display: string) { if (display.toLowerCase() === this._display?.toLowerCase()) { return; @@ -135,10 +194,11 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x this._displayValue = "5:58"; break; case "songname": - this._displayValue = "Niente da Caprie (3"; - break; + // this._displayValue = "Niente da Caprie (3"; + // break; case "songtitle": - this._displayValue = "Niente da Caprie (3"; + this._displayValue = "Your Favorite MP3 Song Title, U R Reading"; + // this._displayValue = "Short MP3 Title"; break; case "songbitrate": case "songsamplerate": @@ -161,7 +221,13 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x } } - getText() { + gettext() { + if ((this._text || "").startsWith(":") && this._drawn) { + const layout = this.getparentlayout(); + if (layout) { + return layout.getcontainer()._name || this._text; + } + } if (this._display) { return this._displayValue; } @@ -180,26 +246,67 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x // TODO } - _renderText() { - removeAllChildNodes(this._div); - if (this._font) { - const font = UI_ROOT.getFont(this._font); + //to speedup, we spit render. This is only rendering style + _prepareCss() { + if (!this._font_obj) { + this._font_obj = UI_ROOT.getFont(this._font_id); + } + const font = this._font_obj; + if (font instanceof BitmapFont) { + this._textWrapper.setAttribute("font", "BitmapFont"); + this._div.style.setProperty( + "--fontSize", + (this._fontSize || "~").toString() + ); + this.setBackgroundImage(font); + this._div.style.backgroundSize = "0"; //disable parent background, because only children will use it + this._div.style.lineHeight = px(this._div.getBoundingClientRect().height); + this._div.style.setProperty("--charwidth", px(font._charWidth)); + this._div.style.setProperty("--charheight", px(font._charHeight)); + } else { + if (this._color) { + const color = UI_ROOT.getColor(this._color); + if (color) { + this._div.style.color = `var(${color.getCSSVar()}, ${color.getRgb})`; + } + } if (font instanceof TrueTypeFont) { - this._div.innerText = this.getText(); + this._textWrapper.setAttribute("font", "TrueType"); + this._div.style.fontFamily = font.getFontFamily(); - } else if (font instanceof BitmapFont) { - this._renderBitmapFont(font); + this._div.style.fontSize = px(this._fontSize ?? 12); + this._div.style.textTransform = this._forceuppercase + ? "uppercase" + : "none"; + if (this._bold) { + this._div.style.fontWeight = "bold"; + } + if (this._align) { + this._div.style.textAlign = this._align; + } } else if (font == null) { - this._div.innerText = this.getText(); - this._div.style.fontFamily = "Ariel"; + this._div.style.setProperty("--fontMode", "Null"); + this._div.style.fontFamily = "Arial"; } else { throw new Error("Unexpected font"); } } } + _renderText() { + //TODO: invalidating text width is only important when srolling? + this._invalidateFullWidth(); + + const font = this._font_obj; + if (font instanceof BitmapFont) { + this._renderBitmapFont(font); + } else { + this._textWrapper.innerText = this.gettext(); + } + } + _useColonWidth() { - if (this._timeColonWidth == null) { + if (this._timeColonWidth == null || this._display == null) { return false; } switch (this._display.toLowerCase()) { @@ -212,41 +319,117 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x } _renderBitmapFont(font: BitmapFont) { + removeAllChildNodes(this._textWrapper); this._div.style.whiteSpace = "nowrap"; const useColonWidth = this._useColonWidth(); - if (this.getText() != null) { - for (const char of this.getText().split("")) { + if (this.gettext() != null) { + for (const char of this.gettext().split("")) { const charNode = font.renderLetter(char); // TODO: This is quite hacky. if (char === ":" && useColonWidth) { charNode.style.width = px(this._timeColonWidth); } - this._div.appendChild(charNode); + this._textWrapper.appendChild(charNode); } } } + _renderBitmapFont1(font: BitmapFont) { + this._div.style.whiteSpace = "nowrap"; + let s = ""; + for (const char of this.gettext().split("")) { + s += `${char}`; + } + this._div.innerHTML = s; + } + + // it is needed for scrolltext. + _invalidateFullWidth() { + const font = this._font_obj; + if (font instanceof BitmapFont) { + this._textFullWidth = this._getBitmapFontTextWidth(font); + } else { + this._textFullWidth = this._getTrueTypeTextWidth(font); + } + this._div.style.setProperty("--full-width", px(this._textFullWidth)); + } + + getautowidth(): number { + this._invalidateFullWidth(); + let textWidth = this._textFullWidth; + if (this._relatw == "1") { + textWidth += this._width * -1; + } + return textWidth; + } + + gettextwidth(): number { + return this.getautowidth(); + } + + _getBitmapFontTextWidth(font: BitmapFont): number { + const charWidth = font._charWidth; + return this.gettext().length * charWidth + this._paddingX * 2; + } + + _getTrueTypeTextWidth(font: TrueTypeFont): number { + /** + * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. + * + * @param {String} text The text to be rendered. + * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). + * + * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 + */ + const self = this; + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + context.font = `${this._fontSize || 14}px ${ + (font && font.getFontFamily()) || "Arial" + }`; + const metrics = context.measureText(this.gettext()); + return metrics.width + self._paddingX * 2; + } + draw() { + this._drawn = true; super.draw(); this._renderText(); - this._div.style.overflow = "hidden"; - if (this._bold) { - this._div.style.fontWeight = "bold"; - } - if (this._align) { - this._div.style.textAlign = this._align; - } - /* - if (this._color) { - console.log(this._color); - const color = UI_ROOT.getColor(this._color); - console.log({ color }); - this._div.style.color = color.getRbg(); - } - */ + this._div.style.removeProperty("line-height"); + this._div.classList.add("webamp--img"); + } - this._div.style.fontSize = px(this._fontSize ?? 14); + _prepareScrolling() { + this._scrollDirection = -1; + const timer = (this._scrollTimer = new Timer()); + timer.setdelay(50); + timer.setOnTimer(() => { + this.doScrollText(); + }); + timer.start(); + } + + doScrollText() { + const curL = this._scrollLeft; + const step = 1; //pixel + const idle = 20; //when overflow + const container = this._div.getBoundingClientRect(); + const wrapperWidth = this._textFullWidth; + if (wrapperWidth <= container.width) return; + var l = curL + step * this._scrollDirection; + if (l + wrapperWidth < container.width - step * idle) { + // too left + this._scrollDirection *= -1; //? flip dir! + l = curL + step * this._scrollDirection; + } else if (l > step * idle) { + // too right + this._scrollDirection *= -1; //? flip dir! + l = curL + step * this._scrollDirection; + } + this._scrollLeft = l; + l = clamp(l, -(wrapperWidth - container.width), 0); + this._textWrapper.style.left = px(Math.round(l)); } dispose() { diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Timer.ts b/packages/webamp-modern-2/src/skin/makiClasses/Timer.ts index 2dab2710..ef5a8a21 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Timer.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Timer.ts @@ -2,16 +2,30 @@ import UI_ROOT from "../../UIRoot"; import { assume } from "../../utils"; import BaseObject from "./BaseObject"; +let TIMER_IDS = 0; + export default class Timer extends BaseObject { static GUID = "5d0c5bb64b1f7de1168d0fa741199459"; - _delay: number; + _delay: number = 5000; //x2nie _timeout: NodeJS.Timeout | null = null; + _nid: number; + _onTimer: ()=>void = null; + + constructor(){ + super(); + TIMER_IDS += 1; + this._nid = TIMER_IDS; + } + setdelay(millisec: number) { - assume( - this._timeout == null, - "Tried to change the delay on a running timer" - ); + // assume( + // this._timeout == null, + // "Tried to change the delay on a running timer" + // ); + const running = this.isrunning(); + if(running) this.stop(); this._delay = millisec; + if(running) this.start() } stop() { if (this._timeout != null) { @@ -19,11 +33,46 @@ export default class Timer extends BaseObject { this._timeout = null; } } - start() { - assume(this._delay != null, "Tried to start a timer without a delay"); - this._timeout = setInterval(() => { - UI_ROOT.vm.dispatch(this, "ontimer"); - }, this._delay); + async start(): Promise { + // console.log('timer.start()', this._nid) + if(!this._delay){ + return false; + } + const self=this; + + try{ + assume(this._delay != null, "Tried to start a timer without a delay"); + if(this.isrunning()){ + this.stop(); + } + this._timeout = setInterval(() => { + // console.log('timer.ontimer()', this._nid) + // UI_ROOT.vm.dispatch(self, "ontimer"); + self.doTimer(); + }, this._delay); + return true + } + catch(err){ + return false + } + return false + } + + doTimer(){ + // console.log('timer.ontimer()', this._nid) + if(this._onTimer!=null){ + this._onTimer() + } else { + UI_ROOT.vm.dispatch(this, "ontimer"); + } + } + + setOnTimer(callback:()=>void){ + const handler = ()=>{ + callback(); + } + this._onTimer = handler; + // this._onTimer = callback; } isrunning(): boolean { @@ -34,6 +83,9 @@ export default class Timer extends BaseObject { return this._delay; } + getskipped(): number { + return 0; + } /* extern Int Timer.getSkipped(); */ diff --git a/packages/webamp-modern-2/src/skin/makiClasses/ToggleButton.ts b/packages/webamp-modern-2/src/skin/makiClasses/ToggleButton.ts index 29fc940c..dc417e87 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/ToggleButton.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/ToggleButton.ts @@ -1,3 +1,5 @@ +import { V } from "../../maki/v"; +import UI_ROOT from "../../UIRoot"; import Button from "./Button"; // http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Cbutton.2F.3E_.26_.3Ctogglebutton.2F.3E @@ -8,6 +10,10 @@ export default class ToggleButton extends Button { return "button"; } + getcurcfgval(): number{ + return this._active? 1 : 0; + } + /** * This method is called by Button */ @@ -18,6 +24,10 @@ export default class ToggleButton extends Button { this.setactivated(!this._active); } + ontoggle(onoff: boolean){ + UI_ROOT.vm.dispatch(this, "ontoggle", [V.newBool(onoff)]); + } + draw() { super.draw(); this._div.setAttribute("data-obj-name", "ToggleButton"); diff --git a/packages/webamp-modern-2/src/skin/makiClasses/WasabiButton.ts b/packages/webamp-modern-2/src/skin/makiClasses/WasabiButton.ts new file mode 100644 index 00000000..da6cff27 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/WasabiButton.ts @@ -0,0 +1,55 @@ +import Button from "./Button"; + +export default class WasabiButton extends Button { +// static GUID = "unknown"; + _l: HTMLSpanElement = document.createElement("span"); + _r: HTMLSpanElement = document.createElement("span"); + _m: HTMLSpanElement = document.createElement("span"); + + + getElTag():string{ + return 'button'; + } + + constructor(){ + super() + this._div.appendChild(this._l); + this._div.appendChild(this._m); + this._div.appendChild(this._r); + // this._image = 'studio.button' + // this._downimage = 'studio.button.pressed' + } + init(){ + super.init(); + this.setXmlAttr('image','studio.button') + this.setXmlAttr('downimage', 'studio.button.pressed'); + } + + setXmlAttr(key: string, value: string): boolean { + if (super.setXmlAttr(key, value)) { + return true; + } + switch (key) { + case "text": + this._m.innerText = value; + break; + default: + return false; + } + return true; + } + + draw() { + super.draw(); + this._div.classList.add('wasabi-button') + this._div.setAttribute("data-obj-name", "WasabiButton"); + } + +// _renderBackground() { + +// } + /* + extern ToggleButton.onToggle(Boolean onoff); + extern int TOggleButton.getCurCfgVal() + */ +} diff --git a/packages/webamp-modern-2/src/skin/makiClasses/WasabiFrame.ts b/packages/webamp-modern-2/src/skin/makiClasses/WasabiFrame.ts index 78dfacb9..a447198d 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/WasabiFrame.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/WasabiFrame.ts @@ -39,7 +39,6 @@ export default class WasabiFrame extends Group { } init() { - // console.error('wasabi:standard->> INITing:', this._content) if (this.__inited) return; this.__inited = true; diff --git a/packages/webamp-modern-2/src/skin/makiClasses/WasabiTitle.ts b/packages/webamp-modern-2/src/skin/makiClasses/WasabiTitle.ts new file mode 100644 index 00000000..99674e7d --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/WasabiTitle.ts @@ -0,0 +1,56 @@ +// import Text from "./Text"; +import UI_ROOT from "../../UIRoot"; +import { num, px, relative } from "../../utils"; +import Group from "./Group"; + +export default class WasabiTitleBar extends Group { + static GUID = "7DFD324437514e7cBF4082AE5F3ADC33"; + _padtitleleft: number = 0; + _padtitleright: number = 0; + + setXmlAttr(_key: string, value: string): boolean { + const lowerkey = _key.toLowerCase(); + // console.log('wasabi:frame.key=',lowerkey,':=', value) + if (super.setXmlAttr(lowerkey, value)) { + return true; + } + switch (lowerkey) { + case "padtitleleft": + this._padtitleleft = num(value); + this._renderX(); + break; + case "padtitleright": + this._padtitleright = num(value); + this._renderWidth(); + break; + default: + return false; + } + return true; + } + + _renderX() { + this._div.style.left = this._relatx=='1' ? relative(this._padtitleleft + this._x ?? 0) : px(this._padtitleleft + this._x ?? 0); + // this._div.setAttribute('pad-left', this._padtitleleft.toString()) + } + + _renderWidth() { + // this._div.setAttribute('pad-right', this._padtitleright.toString()) + // this._div.setAttribute('_width', this._width.toString()) + // this._div.setAttribute('_width_', this.getwidth().toString()) + // if(this._autowidthsource) return; + this._div.style.width = this._relatw=='1' ? relative(-this._padtitleleft + -this._padtitleright + this._width??0) : px(-this._padtitleright + this.getwidth()); + } + + + init() { + super.init() + UI_ROOT.vm.dispatch(this, "onresize", [ + { type: "INT", value: 0 }, + { type: "INT", value: 0 }, + { type: "INT", value: this.getwidth() }, + { type: "INT", value: this.getheight() }, + ]); + } + +} diff --git a/packages/webamp-modern-2/src/skin/makiClasses/WinampConfig.ts b/packages/webamp-modern-2/src/skin/makiClasses/WinampConfig.ts new file mode 100644 index 00000000..c56f9acb --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/WinampConfig.ts @@ -0,0 +1,32 @@ +import ConfigItem from "./ConfigItem"; + +const _items: { [key: string]: ConfigItem } = {}; + +export default class WinampConfig { + static GUID = "b2ad3f2b4e3131ed95e96dbcbb55d51c"; + // _items : {[key:string]: ConfigItem} = {}; + + getgroup(config_group_guid: string): WinampConfigGroup { + return new WinampConfigGroup(); + } +} + +export class WinampConfigGroup { + static GUID = "fc17844e4518c72bf9a868a080baa530"; + + getbool(itemName: string): boolean { + return true; + } + + getint(itemName: string): number { + return 0; + } + + getstring(itemName: string): string { + return ""; + } +} + +// Global Singleton for now +// export const Config = new ConfigClass(); +// export Config; diff --git a/packages/webamp-modern-2/src/skin/makiClasses/WindowHolder.ts b/packages/webamp-modern-2/src/skin/makiClasses/WindowHolder.ts new file mode 100644 index 00000000..84ec1e90 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/WindowHolder.ts @@ -0,0 +1,40 @@ +import UI_ROOT from "../../UIRoot"; +import { assert, px, removeAllChildNodes, toBool } from "../../utils"; +import GuiObj from "./GuiObj"; +import Group from "./Group"; + +// > A container is a top level object and it basically represents a window. +// > Nothing holds a container. It is an object that holds multiple related +// > layouts. Each layout represents an appearance for that window. You can design +// > different layouts for each window but only one can be visible at a time. +// +// -- http://wiki.winamp.com/wiki/Modern_Skin +export default class WindowHolder extends Group { +// static GUID = "38603665461B42a7AA75D83F6667BF73"; + + constructor() { + super(); + } + +// setXmlAttr(_key: string, value: string): boolean { +// const key = _key.toLowerCase(); +// if (super.setXmlAttr(key, value)) { +// return true; +// } +// switch (key) { +// case "name": +// this._name = value; +// break; +// case "id": +// this._id = value.toLowerCase(); +// break; +// case "default_visible": +// this._visible = toBool(value); +// break; +// default: +// return false; +// } +// return true; +// } + +} diff --git a/packages/webamp-modern-2/src/skin/parse.ts b/packages/webamp-modern-2/src/skin/parse.ts index 714947f3..2896a720 100644 --- a/packages/webamp-modern-2/src/skin/parse.ts +++ b/packages/webamp-modern-2/src/skin/parse.ts @@ -23,41 +23,95 @@ import Color from "./Color"; import GammaGroup from "./GammaGroup"; import ColorThemesList from "./ColorThemesList"; import { UIRoot } from "../UIRoot"; +import AlbumArt from "./makiClasses/AlbumArt"; +import WindowHolder from "./makiClasses/WindowHolder"; import WasabiFrame from "./makiClasses/WasabiFrame"; +import Grid from "./makiClasses/Grid"; +import ProgressGrid from "./makiClasses/ProgressGrid"; +import WasabiTitle from "./makiClasses/WasabiTitle"; +import ComponentBucket from "./makiClasses/ComponentBucket"; +import GroupXFade from "./makiClasses/GroupXFade"; +import { classResolver } from "./resolver"; + +function hack() { + // Without this Snowpack will try to treeshake out resolver causing a circular + // dependency. + classResolver("A funny joke about why this is needed."); +} class ParserContext { container: Container | null = null; parentGroup: Group | /* Group includes Layout | */ null = null; } +const RESOURCE_PHASE = 1; //full async + Promise.all() +const ResourcesTag = [ + // below are some resource that immediatelly popped (removed) from xml structure. + // so wouldn't be parsed twice. + "color", + "bitmap", + "bitmapfont", + "skininfo", + "accelerators", + // other resource not listed here are also parsed/loaded first in RESOURCE_PHASE (eg script) + // but will be kept in xml (unremoved). +]; + +const GROUP_PHASE = 2; //full sync mode, because of inheritance + export default class SkinParser { - _zip: JSZip; _imageManager: ImageManager; _path: string[] = []; - _context: ParserContext = new ParserContext(); - _gammaSet: GammaGroup[] = []; + _includedXml = {}; // {file:xmlelement} + _scripts = {}; // {file:SystemObject} _uiRoot: UIRoot; + _phase: number = 0; + _res = { + bitmaps: { + // 'studio.basetexture': false, + "studio.button": false, + "studio.button.pressed": false, + "studio.scrollbar.vertical.background": false, + "studio.scrollbar.vertical.left": false, + "studio.scrollbar.vertical.right": false, + "studio.scrollbar.vertical.button": false, + "studio.scrollbar.horizontal.background": false, + "studio.scrollbar.horizontal.left": false, + "studio.scrollbar.horizontal.right": false, + "studio.scrollbar.horizontal.button": false, + }, + colors: {}, + }; //requested by skin, later compared with UiRoot._bitmaps - constructor( - zip: JSZip, - uiRoot: UIRoot /* Once UI_ROOT is not a singleton, we can create that objet in the constructor */ - ) { - this._zip = zip; - this._imageManager = new ImageManager(zip); + constructor(uiRoot: UIRoot) { + /* Once UI_ROOT is not a singleton, we can create that objet in the constructor */ + this._imageManager = new ImageManager(); this._uiRoot = uiRoot; } + async parse(): Promise { // Load built-in xui elements // await this.parseFromUrl("assets/xml/xui/standardframe.xml"); - const includedXml = await this.getCaseInsensitiveFile("skin.xml").async( - "string" - ); + const includedXml = await this._uiRoot.getFileAsString("skin.xml"); + // const includedXml = skinXmlContent; // Note: Included files don't have a single root node, so we add a synthetic one. // A different XML parser library might make this unnessesary. - const parsed = parseXml(includedXml); + const parsed = parseXml(includedXml) as unknown as XmlElement; + console.log("RESOURCE_PHASE #################"); + this._phase = RESOURCE_PHASE; await this.traverseChildren(parsed); + await this._solveMissingBitmaps(); + await this._imageManager.loadUniquePaths(); + await this._imageManager.ensureBitmapsLoaded(); + + console.log("GROUP_PHASE #################"); + this._phase = GROUP_PHASE; + await this.traverseChildren(parsed); + + console.log("BUCKET_PHASE #################"); + await this.rebuildBuckets(); return this._uiRoot; } @@ -70,147 +124,244 @@ export default class SkinParser { await this.traverseChildren(parsed); } - async traverseChildren(parent: XmlElement | XmlDocument) { - for (const child of parent.children) { - if (child instanceof XmlElement) { - await this.traverseChild(child); - } + _scanRes(node: XmlElement) { + if (node.attributes.background) { + this._res.bitmaps[node.attributes.background] = false; // just add, dont need to check } } - async traverseChild(node: XmlElement) { + + /** + * Some bitmap al called by group/layer + * but has no explicit declaration in a loaded skin */ + async _solveMissingBitmaps() { + //? checkmark the already availble + for (const bitmap of this._uiRoot._bitmaps) { + this._res.bitmaps[bitmap._id.toLowerCase()] = true; + } + //? build not available bitmap + // ------- ONLY APPLICABLE ONCE WE ABLE TO LOAD FROM FILEPATH ------- + // for (const [key, available] of Object.entries(this._res.bitmaps)) { + // if (!available) { + // const lowercaseId = key.toLowerCase(); + // const dict = getBitmap_system_elements(lowercaseId); + // if(dict!=null){ + // const bitmapEl = new XmlElement('bitmap', {...dict}) + // await this.bitmap(bitmapEl); + // console.log('solving bitmap:', lowercaseId) + // } + // } + // } + } + + async traverseChildren(node: XmlElement, parent: any = null) { + //? NOTE: I am considering to speedup resource loading by Promise.all + //? But in the same time we need to reduce code complexity + //? So, temporary we are trying to not do Promise.all + + // if (this._phase == RESOURCE_PHASE) { + // return await Promise.all( + // node.children.map((child) => { + // if (child instanceof XmlElement) { + // // console.log('traverse->', parent.name, child.name) + // this._scanRes(child); + // return this.traverseChild(child, parent); + // } + // }) + // ); + // } else { + for (const child of node.children) { + if (child instanceof XmlElement) { + this._scanRes(child); + await this.traverseChild(child, parent); + } + } + // } + } + + async traverseChild(node: XmlElement, parent: any) { switch (node.name.toLowerCase()) { + case "albumart": + return this.albumart(node, parent); case "wasabixml": - return this.wasabiXml(node); + return this.wasabiXml(node, parent); case "winampabstractionlayer": - return this.winampAbstractionLayer(node); + return this.winampAbstractionLayer(node, parent); case "include": - return this.include(node); + return this.include(node, parent); case "skininfo": - return this.skininfo(node); + return this.skininfo(node, parent); case "elements": - return this.elements(node); + return this.elements(node, parent); case "bitmap": return this.bitmap(node); case "bitmapfont": - return this.bitmapFont(node); + return await this.bitmapFont(node); case "color": - return this.color(node); + return await this.color(node, parent); case "groupdef": - return this.groupdef(node); + return this.groupdef(node, parent); case "animatedlayer": - return this.animatedLayer(node); + return this.animatedLayer(node, parent); case "layer": - return this.layer(node); + return this.layer(node, parent); case "container": - return this.container(node); + return this.container(node, parent); case "layoutstatus": - return this.layoutStatus(node); - case "hideobject": - return this.hideobject(node); + return this.layoutStatus(node, parent); + case "grid": + return this.grid(node, parent); + case "progressgrid": + return this.progressGrid(node, parent); case "button": - return this.button(node); + return this.button(node, parent); case "togglebutton": - return this.toggleButton(node); + case "nstatesbutton": + return this.toggleButton(node, parent); + case "rect": case "group": - return this.group(node); + return this.group(node, parent); + case "groupxfade": + return this.groupXFade(node, parent); case "layout": - return this.layout(node); + return this.layout(node, parent); + case "windowholder": + return this.windowholder(node, parent); case "component": - return this.component(node); + return this.component(node, parent); case "gammaset": - return this.gammaset(node); + return this.gammaset(node, parent); case "gammagroup": - return this.gammagroup(node); + return this.gammagroup(node, parent); case "slider": - return this.slider(node); + return this.slider(node, parent); case "script": - return this.script(node); + return this.script(node, parent); case "scripts": - return this.scripts(node); + return this.scripts(node, parent); case "text": - return this.text(node); + return this.text(node, parent); + case "songticker": + return this.songticker(node, parent); + case "hideobject": case "sendparams": - return this.sendparams(node); + return this.sendparams(node, parent); case "wasabi:titlebar": - return this.wasabiTitleBar(node); + return this.wasabiTitleBar(node, parent); case "wasabi:button": - return this.wasabiButton(node); + return this.wasabiButton(node, parent); case "truetypefont": - return this.trueTypeFont(node); + return this.trueTypeFont(node, parent); case "eqvis": - return this.eqvis(node); + return this.eqvis(node, parent); + case "colorthemes:mgr": case "colorthemes:list": - return this.colorThemesList(node); + return this.colorThemesList(node, parent); case "status": - return this.status(node); + return this.status(node, parent); case "wasabi:mainframe:nostatus": case "wasabi:medialibraryframe:nostatus": case "wasabi:playlistframe:nostatus": case "wasabi:standardframe:nostatus": case "wasabi:standardframe:status": case "wasabi:visframe:nostatus": - return this.wasabiFrame(node); - case "nstatesbutton": + return this.wasabiFrame(node, parent); case "componentbucket": + return this.componentBucket(node, parent); case "playlisteditor": case "wasabi:tabsheet": case "snappoint": case "accelerators": case "elementalias": case "browser": - case "grid": case "syscmds": // TODO return; // TODO: This should be the default fall through - // return this.xuiElement(node); + // return this.xuiElement(node, parent); case "vis": - return this.vis(node); + return this.vis(node, parent); // Note: Included files don't have a single root node, so we add a synthetic one. // A different XML parser library might make this unnessesary. case "wrapper": - return this.traverseChildren(node); + return this.traverseChildren(node, parent); default: console.warn(`Unhandled XML node type: ${node.name}`); return; } } - addToGroup(obj: GuiObj) { - this._context.parentGroup.addChild(obj); + addToGroup(obj: GuiObj, parent: Group) { + try { + parent.addChild(obj); + } catch (err) { + console.warn("addToGroup failed. child:", obj, "pareng:", parent); + } + } + + async newGui( + Type, + node: XmlElement, + parent: any + ): Promise> { + const gui = new Type(); + gui.setXmlAttributes(node.attributes); + this.addToGroup(gui, parent); + return gui; + } + + async newGroup( + Type, + node: XmlElement, + parent: any + ): Promise> { + const group = new Type(); + await this.maybeApplyGroupDef(group, node); + group.setXmlAttributes(node.attributes); + await this.traverseChildren(node, group); + this.addToGroup(group, parent); + if (node.attributes.instanceid) + group.setxmlparam("id", node.attributes.instanceid); + return group; } /* Individual Element Parsers */ - async wasabiXml(node: XmlElement) { - await this.traverseChildren(node); + async wasabiXml(node: XmlElement, parent: any) { + await this.traverseChildren(node, parent); } - async winampAbstractionLayer(node: XmlElement) { - await this.traverseChildren(node); + async winampAbstractionLayer(node: XmlElement, parent: any) { + await this.traverseChildren(node, parent); } - async elements(node: XmlElement) { - await this.traverseChildren(node); + async elements(node: XmlElement, parent: any) { + await this.traverseChildren(node, parent); } - async group(node: XmlElement) { - const group = new Group(); - const previousParent = this._context.parentGroup; - await this.maybeApplyGroupDef(group, node); - group.setXmlAttributes(node.attributes); - this._context.parentGroup = group; - await this.traverseChildren(node); - this._context.parentGroup = previousParent; - this.addToGroup(group); + async group(node: XmlElement, parent: any): Promise { + return await this.newGroup(Group, node, parent); } - async wasabiFrame(node: XmlElement) { + async groupXFade(node: XmlElement, parent: any) { + const xFade: GroupXFade = await this.newGroup(GroupXFade, node, parent); + this._uiRoot.addXFade(xFade); + } + + async componentBucket(node: XmlElement, parent: any) { + const bucket: ComponentBucket = await this.newGroup( + ComponentBucket, + node, + parent + ); + this._uiRoot.addComponentBucket(bucket.getWindowType(), bucket); + } + + async wasabiFrame(node: XmlElement, parent: any) { const frame = new WasabiFrame(); - const previousParent = this._context.parentGroup; + this.addToGroup(frame, parent); //? Search Wasabi Inheritace - const xuitag: string = node.name; //Wasabi:MainFrame:NoStatus + const xuitag: string = node.name; // eg. Wasabi:MainFrame:NoStatus const xuiEl: XmlElement = this._uiRoot.getXuiElement(xuitag); if (xuiEl) { const xuiFrame = new XmlElement("dummy", { id: xuiEl.attributes.id }); @@ -220,28 +371,24 @@ export default class SkinParser { const groupDef = this._uiRoot.getGroupDef(groupdef_id); if (groupDef) { await this.maybeApplyGroupDef(frame, groupDef); - // console.log('WasabiFrame success to apply groupDef.id=', groupdef_id) - } else { - // console.warn('WasabiFrame failed to apply groupDef.id=', groupdef_id) } } frame.setXmlAttributes(node.attributes); //?content if (node.attributes.content) { - this._context.parentGroup = frame; - await this.group( + const content = await this.group( new XmlElement("group", { id: node.attributes.content, w: "0", h: "0", relatw: "1", relath: "1", - }) + }), + frame ); + frame.addChild(content); } - this._context.parentGroup = previousParent; - this.addToGroup(frame); } /** taken from Winamp Modern skin */ @@ -276,9 +423,10 @@ export default class SkinParser { ); const bitmap = new Bitmap(); bitmap.setXmlAttributes(node.attributes); - await bitmap.ensureImageLoaded(this._imageManager); + this._imageManager.addBitmap(bitmap); this._uiRoot.addBitmap(bitmap); + this._res.bitmaps[node.attributes.id] = true; } async bitmapFont(node: XmlElement) { @@ -288,117 +436,161 @@ export default class SkinParser { ); const font = new BitmapFont(); font.setXmlAttributes(node.attributes); - await font.ensureFontLoaded(this._imageManager); + + const externalBitmap = font._file.indexOf("/") < 0; + if (externalBitmap) { + font.setExternalBitmap(true); + } else { + this._imageManager.addBitmap(font); + } this._uiRoot.addFont(font); } - async text(node: XmlElement) { - assume( - node.children.length === 0, - "Unexpected children in XML node." - ); - - const text = new Text(); - text.setXmlAttributes(node.attributes); - const { parentGroup } = this._context; - if (parentGroup == null) { - console.warn( - `FIXME: Expected to be within a | ` - ); - return; - } - parentGroup.addChild(text); + async text(node: XmlElement, parent: any): Promise { + return this.newGui(Text, node, parent); } - async script(node: XmlElement) { + async songticker(node: XmlElement, parent: any): Promise { + const text = await this.text(node, parent); + text.setxmlparam("display", "songtitle"); + text.setxmlparam("ticker", "1"); + return text; + } + + async wasabiTitleBar(node: XmlElement, parent: any) { + const group = (await this.newGroup(WasabiTitle, node, parent)) as Group; + let text = null; + + //? Search Wasabi Inheritace + const xuitag: string = node.name; // eg "Wasabi:Titlebar" + const xuiEl: XmlElement = this._uiRoot.getXuiElement(xuitag); + if (xuiEl && node.attributes.id != xuiEl.attributes.id) { + const xuiFrame = new XmlElement("groupdev", { id: xuiEl.attributes.id }); + await this.maybeApplyGroupDef(group, xuiFrame); + text = group.findobject(xuiEl.attributes.embed_xui); + } else { + text = group.findobject("window.titlebar.title"); + } + + if (text) { + text.setxmlparam("text", ":componentname"); // or display:componentname? + } + + return text; + } + + async script(node: XmlElement, parent: any) { assume( node.children.length === 0, "Unexpected children in