Use WinampModern (skin name) (#1162)

* Toggle container (action)
it works but the container is currently empty.

* Wasabi:Frame:Xxx : parse,load,init
this patch has no error, but not visible due relatW/relatH not available

* movable,resizable
Tested with MMD3 only

* allow unregister eventListener, etc. minor.

* make the group movable

* trial to move MMD3 main window

* Prettier.

* speedup devtool: simplify tag,style

* bugfix clip-path.
Issue: the bigColorTheme didn't rendered properly

* del "../winamp modern" (specific skin only)

* prettier

* +comment, prettier

* del dead code

Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>

* warn for premateur bitmap that has no id.

* del dead code

* bugfix invalid css caused by empty array

* del premateur logic (no use case with MMD3)

* add comment: clarify that getToken is 0 based.

* +WinampConfig files

* don't stop on bitmap not found

* +skin as usecase

* showing main-window, for the first time

* del dead code

* showing less complainging errrors

* avoid shutdown while the skin.wal is updated.

* direct show obj (instead of stringify)

* incoming Audio: play/stop/pause

* + big elements, -big parser mod

* parsing in simple and safe way

* BitmapFont & Text

* okay. just an error (not initilaized) avoided.

* +doResize

* +implementation: autoWidthSource

* show long song title

* +api:sendparams

* bugfix interpreter: brutal boolean

* Seeker: song & volume

* load modern skin without thrown any error

* MuteBtn works

* bugfix transparency of seekerghost

* bugfix orphan sys to run (MMD3, etc)

* auto reapply regions after resizing

* bugfix titlebar.right.width==0

* MainWindow movable!

* color! it is now also gamma-aware

* colortheme: doubleclickable,bold,header-column

* allow handleAction:actionTarget, clTheme:next,prev

* close my Container!

* bugifx titlebar.getText()

* bugfix volume (knob) click.

* bugfix volume (knob) click on moved Container

* bugfix layer: cropped by transparent

* del dead code

* bugfix volum know conflict with layer crop

* bugfix font-size

* bugfix slider moves container

* bugfix 2pixel higher than expected.

* speedup resize + avoid flickering

* Equalizer: showInfo()

* integrate all eqSliders in single mouseMove

* reducing lock: anti-freeze mouseMove

* less error/warn. prettier.

* enableGammaSet accept string | null

* public function that exposes font._externalBitmap

* del dead code

Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>

* prettier

* + a note for temporary hack.

* revert a dirty hack

* del overkill logic

Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>

* del dead code

Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>

* del dead code

Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>

* del dead code

* improve readability

Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>

* improve readability

Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>

* nothing

Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>

* nothing

Actually I hate implicit parameter, it may drive to wrong logic. But since it just local function, it is acceptable, even still I hate that way.

Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>

* del dead code

* turn to arrow function

Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>

* bugfix ticker="scroll" | "bounce" | "off"

* Promise.all([].map(async)) !

* Promise.all([].map(async)) !

* remove double__underscore

* set AUDIO_* to string constant

* cleanup comments

* del dead code

* bugfix config-tabs, cleanup, prettier

* expose _id and _file via public method

* introducing new eventListener. works well.

* mute console

* bugfix mouse-move behavior

* remove old custom Emitter

* add note of why returning a function

* allow BOOLs to do logical AND.
without this, the play button click will throw error

* loading componentBucket entries

* add a wrapper for bucket children

* componentBucket: scrollable

* finalize the componentBucket

* +groupxfade. allow do skinParse(xmlnode) runtime.

* animate XFade content

* Show XFade sub-page

* del dead code

Co-authored-by: Fathony <fathony@smart-leaders.net>
Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>
This commit is contained in:
Fathony Luthfillah 2022-03-25 11:45:30 +07:00 committed by GitHub
parent 6c325e9ec5
commit b0e24fb8a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 3250 additions and 722 deletions

Binary file not shown.

View file

@ -8,6 +8,9 @@ module.exports = {
src: "/",
assets: "/assets",
},
exclude: [
'*.tmp'
],
plugins: [
/* ... */
],

View file

@ -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<string>;
getFileAsBytes: (filePath: string) => Promise<ArrayBuffer>;
getFileAsBlob: (filePath: string) => Promise<Blob>;
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<string> {
if (!filePath) return null;
const zipObj = getCaseInsensitiveFile(this._zip, filePath);
if (!zipObj) return null;
return await zipObj.async("string");
}
async getFileAsBytesZip(filePath: string): Promise<ArrayBuffer> {
if (!filePath) return null;
const zipObj = getCaseInsensitiveFile(this._zip, filePath);
if (!zipObj) return null;
return await zipObj.async("arraybuffer");
}
async getFileAsBlobZip(filePath: string): Promise<Blob> {
if (!filePath) return null;
const zipObj = getCaseInsensitiveFile(this._zip, filePath);
if (!zipObj) return null;
return await zipObj.async("blob");
}
async getFileAsStringPath(filePath: string): Promise<string> {
const response = await fetch(this._skinPath + filePath);
return await response.text();
}
async getFileAsBytesPath(filePath: string): Promise<ArrayBuffer> {
const response = await fetch(this._skinPath + filePath);
return await response.arrayBuffer();
}
async getFileAsBlobPath(filePath: string): Promise<Blob> {
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

View file

@ -20,9 +20,10 @@
click me! +-&gt;
<u id="app">
<!-- <img id="img1" src="/assets/skins/webamp/window/main.png"/> -->
<!-- <img id="img1" src="/assets/skins/webamp/window/main2.png"/> -->
<img id="img1" src="/assets/extracted/wacup-modern-skin/window/main2.png"/>
<!-- <img id="img1" src="/assets/extracted/skins/MMD3/player/Bgoverlay.png"/> -->
<img id="img1" src="/assets/extracted/skins/MMD3/component/comp-right-stretch-bg.png"/>
<!-- <img id="img1" src="/assets/extracted/MMD3/component/comp-right-stretch-bg.png"/> -->
<!-- <img id="img1" src="/assets/extracted/MMD3/player/volume.png" data-width="44" data-height="44"/> -->
<!-- <img id="img1" src="/assets/skins/webamp/player/videoavs_button_bg.png" /> -->
</u>
</div>

View file

@ -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"));

View file

@ -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;
}
</style>
<style id="bitmap-css"></style>

View file

@ -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()) {

View file

@ -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.");
}

View file

@ -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"
]
]
}
]
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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[] = [];

View file

@ -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})`;
}

View file

@ -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;
}

View file

@ -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})`;
}
}

View file

@ -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<string, string> = new Map();
_imgCache: Map<string, HTMLImageElement> = new Map();
_cssVarCache: Map<string, string> = 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<string | null> {
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<HTMLImageElement | null> {
if (!this._imgCache.has(filePath)) {
// TODO: We could cache this

View file

@ -1,19 +1,26 @@
// TODO: Persist this to local state?
class PrivateConfig {
_sections: Map<string, Map<string, number>> = new Map();
_sections: Map<string, Map<string, string>> = 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();

View file

@ -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() {

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -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();

View file

@ -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");
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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");
}
}
}

View file

@ -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`);
}
}

View file

@ -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 <sendparams> and <hideobject>
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";

View file

@ -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 {

View file

@ -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;

View file

@ -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)
}
}

View file

@ -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;
}
}

View file

@ -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");
}
}
}

View file

@ -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')
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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) {

View file

@ -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 += `<i>${char}</i>`;
}
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() {

View file

@ -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<boolean> {
// 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();
*/

View file

@ -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");

View file

@ -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()
*/
}

View file

@ -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;

View file

@ -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() },
]);
}
}

View file

@ -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;

View file

@ -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;
// }
}

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ import Layer from "./makiClasses/Layer";
import AnimatedLayer from "./makiClasses/AnimatedLayer";
import PopupMenu from "./makiClasses/PopupMenu";
import ToggleButton from "./makiClasses/ToggleButton";
import LayoutStatus from "./makiClasses/LayoutStatus";
import Status from "./makiClasses/Status";
import Text from "./makiClasses/Text";
import Group from "./makiClasses/Group";
@ -17,10 +18,18 @@ import Vis from "./makiClasses/Vis";
import GuiObj from "./makiClasses/GuiObj";
import Config from "./makiClasses/Config";
import ConfigItem from "./makiClasses/ConfigItem";
import ConfigAttribute from "./makiClasses/ConfigAttribute";
import WinampConfig, { WinampConfigGroup } from "./makiClasses/WinampConfig";
import ComponentBucket from "./makiClasses/ComponentBucket";
import AlbumArt from "./makiClasses/AlbumArt";
const CLASSES = [
Config,
// ConfigItem,
Config, ConfigItem, ConfigAttribute,
WinampConfig, WinampConfigGroup,
ComponentBucket,
AlbumArt,
Button,
SystemObject,
Container,
@ -30,6 +39,7 @@ const CLASSES = [
PopupMenu,
ToggleButton,
Status,
LayoutStatus,
Text,
Group,
MakiMap,

View file

@ -10,8 +10,11 @@ export function assert(condition: boolean, message: string) {
// don't need to break the world, but I would like to know if/when my
// assumptions are invalidated.
// In the future these can be turned into warnings.
export function assume(condtion: boolean, message: string) {
return assert(condtion, message);
export function assume(condition: boolean, message: string) {
if (!condition) {
console.warn(message);
}
return condition;
}
export function getCaseInsensitiveFile(
@ -89,24 +92,69 @@ export function getUrlQuery(location: Location, variable: string): string {
return new URL(location.href).searchParams.get(variable);
}
export const throttle = (fn: Function, wait: number = 300) => {
let inThrottle: boolean,
lastFn: ReturnType<typeof setTimeout>,
lastTime: number;
return function (this: any) {
const context = this,
args = arguments;
if (!inThrottle) {
fn.apply(context, args);
lastTime = Date.now();
inThrottle = true;
} else {
clearTimeout(lastFn);
lastFn = setTimeout(() => {
if (Date.now() - lastTime >= wait) {
fn.apply(context, args);
lastTime = Date.now();
}
}, Math.max(wait - (Date.now() - lastTime), 0));
}
};
};
/**
* Purpuse: to hold eventListeners
*/
export class Emitter {
_cbs: { [event: string]: Array<() => void> } = {};
on(event: string, cb: () => void) {
_cbs: { [event: string]: Array<Function> } = {};
// call this to register a callback to a specific event
on(event: string, cb: Function) {
if (this._cbs[event] == null) {
this._cbs[event] = [];
}
this._cbs[event].push(cb);
return () => {
// return a function for later unregistering
return () => {
//TODO: consider using this.off(), or integrate both
this._cbs[event] = this._cbs[event].filter((c) => c !== cb);
};
}
trigger(event: string) {
// remove an registered callback from a specific event
off(event: string, cb: Function) {
if (this._cbs[event] == null) {
return;
}
const cbs = this._cbs[event];
const index = cbs.indexOf(cb, 0);
if (index > -1) {
cbs.splice(index, 1);
}
}
// call this to run registered callbacks of an event
trigger(event: string, ...args: any[]) {
const subscriptions = this._cbs[event];
if (subscriptions == null) {
return;
}
for (const cb of subscriptions) {
cb();
cb(...args);
}
}
}

View file

@ -1,5 +1,6 @@
{
"compilerOptions": {
"target": "es2015",
"esModuleInterop": true,
"allowJs": true,
"resolveJsonModule": true