mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-22 17:47:16 +00:00
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:
parent
6c325e9ec5
commit
b0e24fb8a3
54 changed files with 3250 additions and 722 deletions
BIN
packages/webamp-modern-2/assets/WinampModern566.wal
Normal file
BIN
packages/webamp-modern-2/assets/WinampModern566.wal
Normal file
Binary file not shown.
|
|
@ -8,6 +8,9 @@ module.exports = {
|
|||
src: "/",
|
||||
assets: "/assets",
|
||||
},
|
||||
exclude: [
|
||||
'*.tmp'
|
||||
],
|
||||
plugins: [
|
||||
/* ... */
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -20,9 +20,10 @@
|
|||
click me! +->
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
|
|
|||
|
|
@ -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})`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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})`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
43
packages/webamp-modern-2/src/skin/makiClasses/AlbumArt.ts
Normal file
43
packages/webamp-modern-2/src/skin/makiClasses/AlbumArt.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
124
packages/webamp-modern-2/src/skin/makiClasses/ComponentBucket.ts
Normal file
124
packages/webamp-modern-2/src/skin/makiClasses/ComponentBucket.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
98
packages/webamp-modern-2/src/skin/makiClasses/Grid.ts
Normal file
98
packages/webamp-modern-2/src/skin/makiClasses/Grid.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
106
packages/webamp-modern-2/src/skin/makiClasses/GroupXFade.ts
Normal file
106
packages/webamp-modern-2/src/skin/makiClasses/GroupXFade.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
45
packages/webamp-modern-2/src/skin/makiClasses/MapObj.ts
Normal file
45
packages/webamp-modern-2/src/skin/makiClasses/MapObj.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
*/
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
56
packages/webamp-modern-2/src/skin/makiClasses/WasabiTitle.ts
Normal file
56
packages/webamp-modern-2/src/skin/makiClasses/WasabiTitle.ts
Normal 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() },
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue