diff --git a/packages/webamp-modern-2/src/UIRoot.ts b/packages/webamp-modern-2/src/UIRoot.ts index 583eea7e..89f45785 100644 --- a/packages/webamp-modern-2/src/UIRoot.ts +++ b/packages/webamp-modern-2/src/UIRoot.ts @@ -4,10 +4,11 @@ import TrueTypeFont from "./skin/TrueTypeFont"; import { assert, assume } from "./utils"; import BitmapFont from "./skin/BitmapFont"; import Color from "./skin/Color"; -import AUDIO_PLAYER from "./skin/AudioPlayer"; import GammaGroup from "./skin/GammaGroup"; import Container from "./skin/Container"; import Vm from "./skin/VM"; +import BaseObject from "./skin/BaseObject"; +import AudioPlayer from "./skin/AudioPlayer"; export class UIRoot { // Just a temporary place to stash things @@ -20,10 +21,32 @@ export class UIRoot { _activeGammaSet: GammaGroup[] | null = null; _containers: Container[] = []; - vm: Vm; + // A list of all objects created for this skin. + _objects: BaseObject[] = []; + + vm: Vm = new Vm(); + audio: AudioPlayer = new AudioPlayer(); + + reset() { + this.dispose(); + this._bitmaps = []; + this._fonts = []; + this._colors = []; + this._groupDefs = []; + this._gammaSets = new Map(); + this._xuiElements = []; + this._activeGammaSet = null; + this._containers = []; + + // A list of all objects created for this skin. + this._objects = []; - constructor() { this.vm = new Vm(); + this.audio = new AudioPlayer(); + } + + addObject(obj: BaseObject) { + this._objects.push(obj); } addBitmap(bitmap: Bitmap) { @@ -127,7 +150,12 @@ export class UIRoot { const found = this._activeGammaSet.find((gammaGroup) => { return gammaGroup.getId().toLowerCase() === lower; }); - assume(found != null, `Cold not find a gammagroup for "${id}"`); + assume( + found != null, + `Cold not find a gammagroup for "${id}" from ${Array.from( + this._gammaSets.keys() + ).join(", ")}` + ); return found; } @@ -168,29 +196,35 @@ export class UIRoot { dispatch(action: string, param: string | null, actionTarget: string | null) { switch (action) { case "PLAY": - AUDIO_PLAYER.play(); + this.audio.play(); break; case "PAUSE": - AUDIO_PLAYER.pause(); + this.audio.pause(); break; case "STOP": - AUDIO_PLAYER.stop(); + this.audio.stop(); break; case "NEXT": - AUDIO_PLAYER.next(); + this.audio.next(); break; case "PREV": - AUDIO_PLAYER.previous(); + this.audio.previous(); break; case "EJECT": - AUDIO_PLAYER.eject(); + this.audio.eject(); break; default: assume(false, `Unknown global action: ${action}`); } } + + dispose() { + for (const obj of this._objects) { + obj.dispose(); + } + } } // Global Singleton for now -const UI_ROOT = new UIRoot(); +let UI_ROOT = new UIRoot(); export default UI_ROOT; diff --git a/packages/webamp-modern-2/src/dropTarget.ts b/packages/webamp-modern-2/src/dropTarget.ts new file mode 100644 index 00000000..141b9975 --- /dev/null +++ b/packages/webamp-modern-2/src/dropTarget.ts @@ -0,0 +1,36 @@ +export function addDropHandler(onDrop: (blob: Blob) => void) { + const dropTarget = document.createElement("div"); + dropTarget.style.top = "0px"; + dropTarget.style.left = "0px"; + dropTarget.style.position = "absolute"; + dropTarget.style.bottom = "0"; + dropTarget.style.right = "0"; + document.body.appendChild(dropTarget); + + function dropHandler(ev: DragEvent) { + // Prevent default behavior (Prevent file from being opened) + ev.preventDefault(); + + const file = ev.dataTransfer.files[0]; + if (file == null) { + return; + } + + clearDropTargetStyle(); + onDrop(file); + } + + function clearDropTargetStyle() { + dropTarget.style.border = "none"; + } + + function dragOverHandler(ev: DragEvent) { + dropTarget.style.border = "4px dotted white"; + // Prevent default behavior (Prevent file from being opened) + ev.preventDefault(); + } + + dropTarget.addEventListener("dragover", dragOverHandler); + dropTarget.addEventListener("dragleave", clearDropTargetStyle); + dropTarget.addEventListener("drop", dropHandler); +} diff --git a/packages/webamp-modern-2/src/index.ts b/packages/webamp-modern-2/src/index.ts index 9b94cffa..117e12e5 100644 --- a/packages/webamp-modern-2/src/index.ts +++ b/packages/webamp-modern-2/src/index.ts @@ -2,6 +2,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 { removeAllChildNodes } from "./utils"; +import { addDropHandler } from "./dropTarget"; function hack() { // Without this Snowpack will try to treeshake out resolver causing a circular @@ -9,58 +12,51 @@ function hack() { classResolver("A funny joke about why this is needed."); } +const NODE = document.createElement("div"); +document.body.appendChild(NODE); + +const STATUS = document.getElementById("status"); + +function setStatus(status: string) { + STATUS.innerText = status; +} + async function main() { - const status = document.getElementById("status"); - status.innerText = "Downloading skin..."; + addDropHandler(loadSkin); + setStatus("Downloading skin..."); // const response = await fetch("assets/CornerAmp_Redux.wal"); // const response = await fetch("assets/Default_winamp3_build499.wal"); const response = await fetch("assets/MMD3.wal"); const data = await response.blob(); - status.innerText = "Loading .wal archive..."; - const zip = await JSZip.loadAsync(data); + await loadSkin(data); +} - status.innerText = "Parsing XML and initializing images..."; - const parser = new SkinParser(zip); +async function loadSkin(skinData: Blob) { + removeAllChildNodes(NODE); + UI_ROOT.reset(); + + setStatus("Loading .wal archive..."); + const zip = await JSZip.loadAsync(skinData); + + setStatus("Parsing XML and initializing images..."); + const parser = new SkinParser(zip, UI_ROOT); const uiRoot = await parser.parse(); - let node = document.createElement("div"); - - status.innerText = "Enabling Colors..."; + setStatus("Enabling Colors..."); uiRoot.enableDefaultGammaSet(); - status.innerText = "Rendering skin for the first time..."; + setStatus("Rendering skin for the first time..."); for (const container of uiRoot.getContainers()) { container.draw(); - node.appendChild(container.getDiv()); + NODE.appendChild(container.getDiv()); } - const select = document.createElement("select"); - select.style.position = "absolute"; - select.style.bottom = "0px"; - select.style.left = "0px"; - select.addEventListener("change", (e) => { - uiRoot.enableGammaSet((e.target as HTMLInputElement).value); - }); - for (const set of uiRoot._gammaSets.keys()) { - const option = document.createElement("option"); - option.innerText = set; - option.value = set; - select.appendChild(option); - } - - document.body.appendChild(select); - - const div = document.createElement("div"); - document.body.appendChild(div); - - document.body.appendChild(node); - - status.innerText = "Initializing Maki..."; + setStatus("Initializing Maki..."); for (const container of uiRoot.getContainers()) { container.init(); } - status.innerText = ""; + setStatus(""); } main(); diff --git a/packages/webamp-modern-2/src/skin/AudioPlayer.ts b/packages/webamp-modern-2/src/skin/AudioPlayer.ts index 02e458f6..60cb27cc 100644 --- a/packages/webamp-modern-2/src/skin/AudioPlayer.ts +++ b/packages/webamp-modern-2/src/skin/AudioPlayer.ts @@ -1,4 +1,4 @@ -class AudioPlayer { +export default class AudioPlayer { _input: HTMLInputElement = document.createElement("input"); _audio: HTMLAudioElement = document.createElement("audio"); constructor() { @@ -53,7 +53,3 @@ class AudioPlayer { return this._audio.duration; } } - -const AUDIO_PLAYER = new AudioPlayer(); - -export default AUDIO_PLAYER; diff --git a/packages/webamp-modern-2/src/skin/BaseObject.ts b/packages/webamp-modern-2/src/skin/BaseObject.ts index f27b96b1..d7e8401c 100644 --- a/packages/webamp-modern-2/src/skin/BaseObject.ts +++ b/packages/webamp-modern-2/src/skin/BaseObject.ts @@ -1,11 +1,12 @@ -/** - Object Class. +import UI_ROOT from "../UIRoot"; - @short This is the base class from which all other classes inherit. - @author Nullsoft Inc. - @ver 1.0 -*/ +/** + * This is the base class from which all other classes inherit. + */ export default class BaseObject { + constructor() { + UI_ROOT.addObject(this); + } /** * Returns the class name for the object. * @@ -18,4 +19,8 @@ export default class BaseObject { getId() { throw new Error("Unimplemented"); } + + dispose() { + // Pass + } } diff --git a/packages/webamp-modern-2/src/skin/SystemObject.ts b/packages/webamp-modern-2/src/skin/SystemObject.ts index cbe12eec..1326510a 100644 --- a/packages/webamp-modern-2/src/skin/SystemObject.ts +++ b/packages/webamp-modern-2/src/skin/SystemObject.ts @@ -1,6 +1,5 @@ import { getClass } from "../maki/objects"; import { ParsedMaki } from "../maki/parser"; -import AUDIO_PLAYER from "./AudioPlayer"; import BaseObject from "./BaseObject"; import Container from "./Container"; import { clamp } from "../utils"; @@ -148,7 +147,7 @@ export default class SystemObject extends BaseObject { * @ret The current volume. **/ getvolume() { - return AUDIO_PLAYER.getVolume() * 255; + return UI_ROOT.audio.getVolume() * 255; } /** @@ -160,7 +159,7 @@ export default class SystemObject extends BaseObject { setvolume(_vol: number) { const vol = clamp(_vol, 0, 255); - AUDIO_PLAYER.setVolume(vol / 255); + UI_ROOT.audio.setVolume(vol / 255); } /** @@ -169,7 +168,7 @@ export default class SystemObject extends BaseObject { * @ret Length of the track, in seconds. */ getplayitemlength(): number { - return AUDIO_PLAYER.getLength(); + return UI_ROOT.audio.getLength(); // } @@ -179,7 +178,7 @@ export default class SystemObject extends BaseObject { */ seekto(pos: number) { // Note: For some reason I seem to be getting passed seconds here not MS - AUDIO_PLAYER.seekTo(pos); + UI_ROOT.audio.seekTo(pos); } /** diff --git a/packages/webamp-modern-2/src/skin/parse.ts b/packages/webamp-modern-2/src/skin/parse.ts index 69e90a45..42be7032 100644 --- a/packages/webamp-modern-2/src/skin/parse.ts +++ b/packages/webamp-modern-2/src/skin/parse.ts @@ -22,7 +22,7 @@ import BitmapFont from "./BitmapFont"; import Color from "./Color"; import GammaGroup from "./GammaGroup"; import ColorThemesList from "./ColorThemesList"; -import UI_ROOT, { UIRoot } from "../UIRoot"; +import { UIRoot } from "../UIRoot"; class ParserContext { container: Container | null = null; @@ -37,10 +37,13 @@ export default class SkinParser { _gammaSet: GammaGroup[] = []; _uiRoot: UIRoot; - constructor(zip: JSZip) { + 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); - this._uiRoot = UI_ROOT; + this._uiRoot = uiRoot; } async parse(): Promise { // Load built-in xui elements