diff --git a/.prettierignore b/.prettierignore index 56d06499..3b682f96 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,5 +2,5 @@ packages/webamp/demo/built/ packages/webamp/built/ -packages/webamp-modern/src/build/ +packages/webamp-modern/build/ packages/webamp-modern/tools/eslint-rules/dist/ \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index 02e0737d..a1c2f5a4 100755 --- a/deploy.sh +++ b/deploy.sh @@ -3,4 +3,4 @@ yarn workspace ani-cursor build yarn workspace webamp build yarn workspace webamp build-library yarn workspace webamp-modern build -mv packages/webamp-modern/src/build packages/webamp/demo/built/modern \ No newline at end of file +mv packages/webamp-modern/build packages/webamp/demo/built/modern \ No newline at end of file diff --git a/packages/webamp-modern/.gitignore b/packages/webamp-modern/.gitignore index d1638636..87e3aea6 100644 --- a/packages/webamp-modern/.gitignore +++ b/packages/webamp-modern/.gitignore @@ -1 +1,2 @@ -build/ \ No newline at end of file +build/ +temp/ \ No newline at end of file diff --git a/packages/webamp-modern/README.md b/packages/webamp-modern/README.md index 8272b7f0..b6cf2f7d 100644 --- a/packages/webamp-modern/README.md +++ b/packages/webamp-modern/README.md @@ -3,39 +3,43 @@ Assuming you have [Yarn](https://yarnpkg.com/) installed: ```bash -cd packages/webamp-modern-2 +cd packages/webamp-modern yarn yarn start ``` ## Performance Improvements -- [ ] We could use WebGL to try to improve the speed of switching gamma colors -- [ ] We could use some CSS techniques to avoid having to appply inline style to each BitmapFont character's DOM node. +- [ ] We could use CSS `filter` to try to improve the speed of switching gamma colors? +- [ ] We could use WebGL to try to improve the speed of switching gamma colors? +- [x] We could use some CSS techniques to avoid having to appply inline style to each BitmapFont character's DOM node. - [ ] We should profile the parse phase to see what's taking time. Perhaps there's some sync image work that could be done lazily. - [ ] Remove some paranoid validation in the VM. - [ ] Consider throttling time updates coming from audio +- [ ] Attach method binding on script init. # TODO Next +- [ ] Implement event-listner-pool for native `on('eventname')`. It will improves readability & speed - [ ] Why doesn't scrolling work property in MMD3? -- [ ] Implement proper color - - [ ] Move gammacolor to GPU +- [x] Implement proper color + - [ ] Move gammacolor to GPU? - [ ] Requires VM - [ ] Look at componentbucket (Where can I find the images) - [ ] How is the scroll window for colors supposed to work? - [ ] How is the position slider supposed to work? - [ ] Standardize handling of different type condition permutations in interpreter -- [ ] Implement EQ +- [x] Implement EQ - [ ] Implament global actions - - [ ] TOGGLE + - [x] TOGGLE - [ ] MINIMIZE -- [ ] Allow for skins which don't have gamma sets +- [x] Allow for skins which don't have gamma sets - [ ] Figure out if global NULL is actually typed as INT in Maki. I suspect there is no NULL type, but only an INT who happens to be zero. +- [ ] Implement custom list instead of html `select`, so scrollbars can be rendered properly. - [ ] Fix all `// FIXME` - [ ] SystemObject.getruntimeversion - [ ] SystemObject.getskinname -- [ ] Handle clicking through transparent: https://stackoverflow.com/questions/38487569/click-through-png-image-only-if-clicked-coordinate-is-transparent +- [x] Handle clicking through transparent: Using css `clip-path`. ~~https://stackoverflow.com/questions/38487569/click-through-png-image-only-if-clicked-coordinate-is-transparent~~ # TODO Some day diff --git a/packages/webamp-modern/package.json b/packages/webamp-modern/package.json index c4e3936c..ae966195 100644 --- a/packages/webamp-modern/package.json +++ b/packages/webamp-modern/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "snowpack dev", "build": "snowpack build", - "serve": "http-server ./built", + "serve": "http-server ./build", "lint": "yarn build-lint && eslint . --ext .js,.jsx,.ts,.tsx", "test": "yarn jest", "extract-object-types": "node tools/extract-object-types.js", diff --git a/packages/webamp-modern/snowpack.config.js b/packages/webamp-modern/snowpack.config.js index dd222416..c33b8656 100644 --- a/packages/webamp-modern/snowpack.config.js +++ b/packages/webamp-modern/snowpack.config.js @@ -22,5 +22,6 @@ module.exports = { }, buildOptions: { /* ... */ + out: './build' }, }; diff --git a/packages/webamp-modern/src/UIRoot.ts b/packages/webamp-modern/src/UIRoot.ts index 872196be..a75a13e6 100644 --- a/packages/webamp-modern/src/UIRoot.ts +++ b/packages/webamp-modern/src/UIRoot.ts @@ -19,6 +19,7 @@ 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 { PlEdit, Track } from "./skin/makiClasses/PlayList"; export class UIRoot { _div: HTMLDivElement = document.createElement("div"); @@ -26,6 +27,7 @@ export class UIRoot { _bitmaps: Bitmap[] = []; _fonts: (TrueTypeFont | BitmapFont)[] = []; _colors: Color[] = []; + _dimensions: { [id: string]: number } = {}; //css: width _groupDefs: XmlElement[] = []; _gammaSets: Map = new Map(); _gammaNames = {}; @@ -38,12 +40,25 @@ export class UIRoot { _buckets: { [wndType: string]: ComponentBucket } = {}; _bucketEntries: { [wndType: string]: XmlElement[] } = {}; _xFades: GroupXFade[] = []; + _input: HTMLInputElement = document.createElement("input"); // A list of all objects created for this skin. _objects: BaseObject[] = []; + //published vm: Vm = new Vm(); audio: AudioPlayer = AUDIO_PLAYER; + playlist: PlEdit = new PlEdit(); + + constructor() { + //"https://raw.githubusercontent.com/captbaritone/webamp-music/4b556fbf/Auto-Pilot_-_03_-_Seventeen.mp3"; + this._input.type = "file"; + this._input.setAttribute("multiple", "true"); + // document.body.appendChild(this._input); + // TODO: dispose + this._input.onchange = this._inputChanged; + } + getFileAsString: (filePath: string) => Promise; getFileAsBytes: (filePath: string) => Promise; getFileAsBlob: (filePath: string) => Promise; @@ -101,6 +116,23 @@ export class UIRoot { this._colors.push(color); } + // to reduce polution of inline style. + addDimension(id: string, size: number) { + this._dimensions[id] = size; + } + addWidth(id: string, bitmapId: string) { + const bitmap = this.getBitmap(bitmapId); + if (bitmap) { + this.addDimension(id, bitmap.getWidth()); + } + } + addHeight(id: string, bitmapId: string) { + const bitmap = this.getBitmap(bitmapId); + if (bitmap) { + this.addDimension(id, bitmap.getHeight()); + } + } + getColor(id: string): Color { const lowercaseId = id.toLowerCase(); const found = findLast( @@ -236,6 +268,7 @@ export class UIRoot { const bitmapFonts: BitmapFont[] = this._fonts.filter( (font) => font instanceof BitmapFont && !font.useExternalBitmap() ) as BitmapFont[]; + // css of bitmaps for (const bitmap of [...this._bitmaps, ...bitmapFonts]) { const img = bitmap.getImg(); if (!img) { @@ -253,16 +286,21 @@ export class UIRoot { ); cssRules.push(` ${bitmap.getCSSVar()}: url(${url});`); } + // css of colors 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("}"); + // css of dimensions + for (const [dimension, size] of Object.entries(this._dimensions)) { + cssRules.push(` --dim-${dimension}: ${size}px;`); + } + // cssRules.unshift(":root{"); + // cssRules.push("}"); const cssEl = document.getElementById("bitmap-css"); - cssEl.textContent = cssRules.join("\n"); + cssEl.textContent = `:root{${cssRules.join("\n")}}`; } getXuiElement(name: string): XmlElement | null { @@ -281,7 +319,7 @@ export class UIRoot { (font) => font instanceof TrueTypeFont ) as TrueTypeFont[]; for (const ttf of truetypeFonts) { - if(!ttf.hasUrl()) { + if (!ttf.hasUrl()) { continue; // some dummy ttf (eg Arial) doesn't has url. } // src: url(data:font/truetype;charset=utf-8;base64,${ttf.getBase64()}) format('truetype'); @@ -290,7 +328,7 @@ export class UIRoot { src: url(${ttf.getBase64()}) format('truetype'); font-weight: normal; font-style: normal; - }`) + }`); } const cssEl = document.getElementById("truetypefont-css"); cssEl.textContent = cssRules.join("\n"); @@ -308,13 +346,13 @@ export class UIRoot { this.audio.stop(); break; case "next": - this.audio.next(); + this.next(); break; case "prev": - this.audio.previous(); + this.previous(); break; case "eject": - this.audio.eject(); + this.eject(); break; case "toggle": this.toggleContainer(param); @@ -327,6 +365,42 @@ export class UIRoot { } } + next() { + const currentTrack = this.playlist.getcurrentindex(); + if (currentTrack < this.playlist.getnumtracks() - 1) { + this.playlist.playtrack(currentTrack + 1); + } + this.audio.play(); + //TODO: check if "repeat" is take account + } + + previous() { + const currentTrack = this.playlist.getcurrentindex(); + if (currentTrack > 0) { + this.playlist.playtrack(currentTrack - 1); + } + this.audio.play(); + //TODO: check if "repeat" is take account + } + + eject() { + // this will call _inputChanged() + this._input.click(); + } + + _inputChanged = () => { + this.playlist.clear(); + for (var i = 0; i < this._input.files.length; i++) { + const newTrack: Track = { + filename: this._input.files[i].name, + file: this._input.files[i], + }; + this.playlist.addTrack(newTrack); + } + + this.audio.play(); + }; + toggleContainer(param: string) { const container = this.findContainer(param); assume(container != null, `Can not toggle on unknown container: ${param}`); @@ -390,7 +464,7 @@ export class UIRoot { if (!filePath) return null; const zipObj = getCaseInsensitiveFile(this._zip, filePath); if (!zipObj) return null; - return await zipObj.async("string"); + return await zipObj.async("text"); } async getFileAsBytesZip(filePath: string): Promise { diff --git a/packages/webamp-modern/src/css/button.check.html b/packages/webamp-modern/src/css/button.check.html new file mode 100644 index 00000000..de66d857 --- /dev/null +++ b/packages/webamp-modern/src/css/button.check.html @@ -0,0 +1,83 @@ + + + + + + + Webamp Modern + + + + + + +
+ +
+

+ +

+
+ + diff --git a/packages/webamp-modern/src/css/button.css b/packages/webamp-modern/src/css/button.css new file mode 100644 index 00000000..b0e91d5b --- /dev/null +++ b/packages/webamp-modern/src/css/button.css @@ -0,0 +1,16 @@ +button.wasabi { + background-image: none; + border: 4px solid transparent; + border-image-source: var(--bitmap-studio-button); + /* border-image-slice: 4 4 4 5 fill; */ + border-image-slice: 4 fill; + vertical-align:middle; +} + +/* button.wasabi:hover { + border-image-source: var(--bitmap-wasabi-button-hover); +} */ + +button.wasabi:active { + border-image-source: var(--bitmap-studio-button-pressed); +} \ No newline at end of file diff --git a/packages/webamp-modern/src/css/list.css b/packages/webamp-modern/src/css/list.css new file mode 100644 index 00000000..40d1ed5e --- /dev/null +++ b/packages/webamp-modern/src/css/list.css @@ -0,0 +1,96 @@ +.list { + color: var(--color-studio-list-text, var(--color-wasabi-list-text)); + background-color: var(--color-wasabi-list-background, transparent); + background-image: var(--bitmap-studio-list-background, none); +} +.list > * { + user-select: none; +} +.list .selected { + background-color: var( + --color-studio-list-item-selected, + var(--color-wasabi-list-text-selected-background) + ); + color: var( + --color-studio-list-item-selected-fg, + var(--color-wasabi-list-text-selected) + ); +} + +/* == COLORTHEMELIST == */ +colorthemeslist { + border: 1px solid black; + border: none; +} +colorthemeslist > select { + border: none; +} + +/* == PLAYLIST == */ +/* .pl.list { + pointer-events: all; +} */ +.pl > .content-list { + margin-right: 15px; + max-height: 100%; + overflow: auto; +} +.pl > .content-list::-webkit-scrollbar { + display: none; +} +.pl::before, +.pl::after { + content: ""; + position: absolute; + top: 0; + width: 15px; + height: 100%; + right: 0; + box-sizing: border-box; + background: var(--color-wasabi-window-background, transparent); + z-index: 0; + pointer-events: none; +} +.pl::after { + width: 8px; + right: 2px; + border-left: 1px solid + var(--color-wasabi-border-sunken, rgba(192, 192, 192, 0.8)); + border-right: 1px solid + var(--color-wasabi-border-sunken, rgba(192, 192, 192, 0.8)); + background: var(--color-wasabi-scrollbar-background-inverted, black); +} +.pl > slider { + z-index: 1; +} + +.pl > slider::before /* button.wasabi */ { + box-sizing: border-box; + background-image: none; + border: 4px solid transparent; + border-image-source: var(--bitmap-studio-button); + /* border-image-slice: 4 4 4 5 fill; */ + border-image-slice: 4 fill; + vertical-align: middle; +} +.pl > slider:active:before /* button.wasabi:active */ { + border-image-source: var(--bitmap-studio-button-pressed); +} + +.pl > slider::after { + content: ""; + position: absolute; + /* TODO: do centering it by calc the real grip's bitmap height/width. */ + left: calc(var(--thumb-left) + 1px); + top: calc(var(--thumb-top) + 5px); + width: 6px; + height: 8px; + background-image: var(--bitmap-wasabi-scrollbar-vertical-grip); +} + +.pl .current { + color: var( + --color-pledit-text-current, + var(--color-wasabi-list-text-current) + ); +} diff --git a/packages/webamp-modern/src/css/scrollbar.css b/packages/webamp-modern/src/css/scrollbar.css new file mode 100644 index 00000000..db2b91a4 --- /dev/null +++ b/packages/webamp-modern/src/css/scrollbar.css @@ -0,0 +1,76 @@ +/* Let's get this party started */ +/*? VERTICAL */ +::-webkit-scrollbar { + width: var(--dim-vscrollbar-width); + background-image: var(--bitmap-wasabi-scrollbar-vertical-background); +} + +/* Track */ +/* ::-webkit-scrollbar-track { + background-image: var(--bitmap-wasabi-scrollbar-vertical-background); +} */ +::-webkit-scrollbar-button { + background-image: var(--bitmap-wasabi-scrollbar-vertical-left); +} +::-webkit-scrollbar-button:vertical { + height: var(--dim-vscrollbar-btn-height); +} +::-webkit-scrollbar-button:vertical:increment { + background-image: var(--bitmap-wasabi-scrollbar-vertical-right); +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background-image: var( + --bitmap-wasabi-scrollbar-vertical-button, + var(--bitmap-studio-scrollbar-vertical-button) + ); + /*background-repeat: repeat;*/ +} +::-webkit-scrollbar-thumb { + max-height: var( + --dim-vscrollbar-thumb-height, + var(--dim-vscrollbar-thumb-height2) + ); + min-height: var( + --dim-vscrollbar-thumb-height, + var(--dim-vscrollbar-thumb-height2) + ); + background-repeat: no-repeat; +} +/* ::-webkit-scrollbar-thumb::after { + content: 'HALO'; + position: absolute; + display: block; + inset: 0; + background: rgba(255, 230, 0, 1); + z-index: 100; +} */ +/* ::-webkit-scrollbar-thumb:window-inactive { + background: rgba(255,0,0,0.4); +} */ + +/*? HORIZONTAL */ +::-webkit-scrollbar:horizontal { + height: var(--dim-hscrollbar-height); + background-image: var(--bitmap-wasabi-scrollbar-horizontal-background); +} +/* Track */ +/* ::-webkit-scrollbar-track:horizontal { +} */ +::-webkit-scrollbar-button:horizontal { + background-image: var(--bitmap-wasabi-scrollbar-horizontal-left); + width: var(--dim-hscrollbar-btn-width); +} +::-webkit-scrollbar-button:horizontal:increment { + background-image: var(--bitmap-wasabi-scrollbar-horizontal-right); +} +/* Handle */ +::-webkit-scrollbar-thumb:horizontal { + background-image: var(--bitmap-studio-scrollbar-horizontal-button); +} + +::-webkit-scrollbar-corner { + background: transparent; +} +/* ---------- EOF SCROLLBAR ------------ */ diff --git a/packages/webamp-modern/src/index.html b/packages/webamp-modern/src/index.html index 2b4b3f59..a64bc1bb 100644 --- a/packages/webamp-modern/src/index.html +++ b/packages/webamp-modern/src/index.html @@ -10,17 +10,29 @@ body { margin: 0; background-color: rgb(58, 110, 165); + font-family: Arial, Helvetica, sans-serif; + } + * { + box-sizing: border-box; } textarea, + group, + text, input, select { font-size: 10.5px; + user-select: none; } textarea:focus, input:focus, select:focus { outline: none; } + #ui-root + select { + background-color: transparent; + color:var(--color-studio-list-text, var(--color-wasabi-list-text)); + } select option { padding-left: 5px; width: 300%; @@ -89,7 +101,7 @@ slider > div { display: none; } - slider::after { + slider::before { content: ""; position: absolute; left: var(--thumb-left); @@ -98,13 +110,13 @@ height: var(--thumb-height); background-image: var(--thumb-background-image); } - slider:hover:after { + slider:hover:before { background-image: var( --thumb-hover-background-image, var(--thumb-background-image) ); } - slider:active:after { + slider:active:before { background-image: var( --thumb-down-background-image, var(--thumb-background-image) @@ -121,6 +133,7 @@ font-style: normal; } text wrap { + display: block; background-image: inherit; background-size: 0px; position: relative; @@ -133,17 +146,18 @@ display: flex; /* vertical align: */ align-items: center; - justify-content: center; + justify-content: var(--align, center); } text span { user-select: none; pointer-events: none; - display: inline-block; + /* display: inline-block; */ background-image: inherit; /* vertical-align: bottom; */ color: transparent; width: var(--charwidth); height: var(--charheight); + margin-right: var(--hspacing, 0); background-position-x: var(--x); background-position-y: var(--y); overflow: hidden; @@ -159,7 +173,7 @@ progressgrid, group, layer, - animatedlayer, + animatedlayer, vis, button, slider, text, @@ -214,19 +228,15 @@ transition: opacity var(--fade-out-speed, 0.25); } - .wasabi-button { - border: 3px solid transparent; - --border-image: var(--bitmap-studio-button); - border-image: var(--border-image) 3 fill/ 1 / 0px stretch; - } - .wasabi-button:active { - --border-image: var(--bitmap-studio-button-pressed); - } - .autowidthsource { width: auto; } + /* .pl { + background: white; + color: black; + } */ + /* titleBar active state */ [inactivealpha="0"] { opacity: 0; @@ -250,6 +260,9 @@ transition: width 0.1s, height 0.1s, left 0.1s, top 0.1s; } + + + diff --git a/packages/webamp-modern/src/index.ts b/packages/webamp-modern/src/index.ts index 88553011..f3f82ea0 100644 --- a/packages/webamp-modern/src/index.ts +++ b/packages/webamp-modern/src/index.ts @@ -24,16 +24,34 @@ function setStatus(status: string) { const DEFAULT_SKIN = "assets/WinampModern566.wal"; async function main() { + // Purposefully don't await, let this load in parallel. + initializeSkinListMenu(); + setStatus("Downloading skin..."); const skinPath = getUrlQuery(window.location, "skin") || DEFAULT_SKIN; const response = await fetch(skinPath); const data = await response.blob(); await loadSkin(data); + + setStatus("Downloading MP3..."); + UI_ROOT.playlist.enqueuefile("assets/Just_Plain_Ant_-_05_-_Stumble.mp3"); + UI_ROOT.playlist.enqueuefile("assets/Just_Plain_Ant_-_05_-_Stumble.mp3"); + UI_ROOT.playlist.enqueuefile("assets/Just_Plain_Ant_-_05_-_Stumble.mp3"); + UI_ROOT.playlist.enqueuefile("assets/Just_Plain_Ant_-_05_-_Stumble.mp3"); + UI_ROOT.playlist.enqueuefile("assets/Just_Plain_Ant_-_05_-_Stumble.mp3"); + UI_ROOT.playlist.enqueuefile("assets/Just_Plain_Ant_-_05_-_Stumble.mp3"); + UI_ROOT.playlist.enqueuefile("assets/Just_Plain_Ant_-_05_-_Stumble.mp3"); + UI_ROOT.playlist.enqueuefile("assets/Just_Plain_Ant_-_05_-_Stumble.mp3"); + UI_ROOT.playlist.enqueuefile("assets/Just_Plain_Ant_-_05_-_Stumble.mp3"); + UI_ROOT.playlist.enqueuefile("assets/Just_Plain_Ant_-_05_-_Stumble.mp3"); + UI_ROOT.playlist.enqueuefile("assets/Just_Plain_Ant_-_05_-_Stumble.mp3"); + UI_ROOT.playlist.enqueuefile("assets/Just_Plain_Ant_-_05_-_Stumble.mp3"); + UI_ROOT.playlist.enqueuefile("assets/Just_Plain_Ant_-_05_-_Stumble.mp3"); + + setStatus(""); } async function loadSkin(skinData: Blob) { - // Purposefully don't await, let this load in parallel. - initializeSkinListMenu(); UI_ROOT.reset(); document.body.appendChild(UI_ROOT.getRootDiv()); @@ -103,11 +121,18 @@ async function initializeSkinListMenu() { downloadLink.style.position = "absolute"; downloadLink.style.bottom = "0"; downloadLink.style.left = "320px"; - downloadLink.text = "Download" + downloadLink.text = "Download"; const current = getUrlQuery(window.location, "skin"); - for (const skin of data.data.modern_skins.nodes) { + const internalSkins = [ + { filename: "default", download_url: "" }, + { filename: "MMD3", download_url: "assets/MMD3.wal" }, + ]; + + const skins = [...internalSkins, ...data.data.modern_skins.nodes]; + + for (const skin of skins) { const option = document.createElement("option"); option.value = skin.download_url; option.textContent = skin.filename; diff --git a/packages/webamp-modern/src/maki/interpreter.ts b/packages/webamp-modern/src/maki/interpreter.ts index a41d3367..0a540784 100644 --- a/packages/webamp-modern/src/maki/interpreter.ts +++ b/packages/webamp-modern/src/maki/interpreter.ts @@ -313,21 +313,6 @@ class Interpreter { } 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, diff --git a/packages/webamp-modern/src/maki/v.ts b/packages/webamp-modern/src/maki/v.ts index 2088df34..b06cb3c5 100644 --- a/packages/webamp-modern/src/maki/v.ts +++ b/packages/webamp-modern/src/maki/v.ts @@ -16,6 +16,7 @@ export type Variable = | { type: "OBJECT"; value: BaseObject; + guid?: string; } | { type: "NULL"; diff --git a/packages/webamp-modern/src/skin/AudioPlayer.ts b/packages/webamp-modern/src/skin/AudioPlayer.ts index a259e685..d83e6135 100644 --- a/packages/webamp-modern/src/skin/AudioPlayer.ts +++ b/packages/webamp-modern/src/skin/AudioPlayer.ts @@ -7,10 +7,10 @@ export const AUDIO_STOPPED = "stopped"; export const AUDIO_PLAYING = "playing"; export class AudioPlayer { - _input: HTMLInputElement = document.createElement("input"); _audio: HTMLAudioElement = document.createElement("audio"); _context: AudioContext; __preamp: GainNode; + _analyser: AnalyserNode; _bands: GainNode[] = []; _source: MediaElementAudioSourceNode; _eqValues: { [kind: string]: number } = {}; @@ -19,8 +19,10 @@ export class AudioPlayer { _isStop: boolean = true; //becaue we can't audio.stop() currently _trackInfo: {}; _albumArtUrl: string = null; + _timeRemaining: boolean = false; //temporary. to show minus //events aka addEventListener() _eventListener: Emitter = new Emitter(); + _vuMeter: number = 0; constructor() { this._context = this._context = new (window.AudioContext || @@ -48,15 +50,39 @@ export class AudioPlayer { document.body.addEventListener("click", resume, false); document.body.addEventListener("keydown", resume, false); } - this._audio.src = "assets/Just_Plain_Ant_-_05_-_Stumble.mp3"; - //"https://raw.githubusercontent.com/captbaritone/webamp-music/4b556fbf/Auto-Pilot_-_03_-_Seventeen.mp3"; - this._input.type = "file"; this._source = this._context.createMediaElementSource(this._audio); this.__preamp = this._context.createGain(); - const connectionNodes: AudioNode[] = [this._source, this.__preamp]; + // Create the analyser node for the visualizer + this._analyser = this._context.createAnalyser(); + this._analyser.fftSize = 2048; + this._analyser.fftSize = 32; + // don't smooth audio analysis + // this._analyser.smoothingTimeConstant = 0.0; + + const connectionNodes: AudioNode[] = [ + this._source, + this.__preamp, + this._analyser, + ]; + + const analyserNode = this._analyser; + + //TODO: generate vuMeter only once needed. + const pcmData = new Float32Array(analyserNode.fftSize); + const onFrame = () => { + analyserNode.getFloatTimeDomainData(pcmData); + let sumSquares = 0.0; + for (let i = 0; i < pcmData.length; i++) { + const amplitude = pcmData[i]; + sumSquares += amplitude * amplitude; + } + this._vuMeter = Math.sqrt(sumSquares / pcmData.length); + window.requestAnimationFrame(onFrame); + }; + window.requestAnimationFrame(onFrame); BANDS.forEach((band, i) => { const filter = this._context.createBiquadFilter(); @@ -85,17 +111,6 @@ export class AudioPlayer { current = next; } - // document.body.appendChild(this._input); - // TODO: dispose - this._input.onchange = (e) => { - const file = this._input.files[0]; - if (file == null) { - return; - } - 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()); @@ -112,6 +127,10 @@ export class AudioPlayer { this._eventListener.off(event, callback); } + setAudioSource(url: string) { + this._audio.src = url; + } + // 0-1 getVolume(): number { return this._audio.volume; @@ -139,14 +158,6 @@ export class AudioPlayer { this.trigger("statchanged"); } - eject() { - this._input.click(); - } - - next() {} - - previous() {} - // 0-1 setVolume(volume: number) { this._audio.volume = volume; @@ -160,9 +171,15 @@ export class AudioPlayer { this._audio.currentTime = this._audio.duration * percent; } + toggleRemainingTime() { + this._timeRemaining = !this._timeRemaining; + } // In seconds getCurrentTime(): number { - return this._audio.currentTime; + // return this._audio.currentTime; + return this._timeRemaining + ? this._audio.currentTime - this._audio.duration + : this._audio.currentTime; } getCurrentTimePercent(): number { @@ -238,7 +255,7 @@ export class AudioPlayer { } } - onEqChange(kind: string, cb: () => void): () => void { + onEqChange(kind: string, cb: () => void): Function { switch (kind) { case "preamp": case "1": @@ -284,8 +301,6 @@ export class AudioPlayer { return dispose; } - - // Current track length in seconds getLength(): number { return this._audio.duration; diff --git a/packages/webamp-modern/src/skin/Bitmap.ts b/packages/webamp-modern/src/skin/Bitmap.ts index a13a7781..0bc43cfe 100644 --- a/packages/webamp-modern/src/skin/Bitmap.ts +++ b/packages/webamp-modern/src/skin/Bitmap.ts @@ -68,6 +68,14 @@ export default class Bitmap { return this._height; } + getLeft() { + return this._x; + } + + getTop() { + return this._y; + } + getCSSVar(): string { return this._cssVar; } diff --git a/packages/webamp-modern/src/skin/BitmapFont.ts b/packages/webamp-modern/src/skin/BitmapFont.ts index d2453730..bbe352e0 100644 --- a/packages/webamp-modern/src/skin/BitmapFont.ts +++ b/packages/webamp-modern/src/skin/BitmapFont.ts @@ -17,7 +17,7 @@ for (const [line, chars] of CHARS.split("\n").entries()) { export default class BitmapFont extends Bitmap { _charWidth: number; _charHeight: number; - _horizontalSpacing: number; + _horizontalSpacing: number = 0; _verticalSpacing: number; _externalBitmap: boolean = false; //? true == _file = another.bitmap.id _bitmap: Bitmap = null; // the real external bitmap @@ -47,6 +47,10 @@ export default class BitmapFont extends Bitmap { return true; } + getHorizontalSpacing(): number { + return this._horizontalSpacing; + } + _setAsBackground(div: HTMLElement, prefix: string) { if (this._externalBitmap) { if (!this._bitmap) { diff --git a/packages/webamp-modern/src/skin/ColorThemesList.ts b/packages/webamp-modern/src/skin/ColorThemesList.ts index 51214cb5..de5250fe 100644 --- a/packages/webamp-modern/src/skin/ColorThemesList.ts +++ b/packages/webamp-modern/src/skin/ColorThemesList.ts @@ -111,6 +111,7 @@ export default class ColorThemesList extends GuiObj { draw() { super.draw(); + this._div.classList.add('list'); this._div.setAttribute("data-obj-name", "ColorThemes:List"); this._renderGammaSets(); } diff --git a/packages/webamp-modern/src/skin/GammaGroup.ts b/packages/webamp-modern/src/skin/GammaGroup.ts index cac61a80..b2ca4c12 100644 --- a/packages/webamp-modern/src/skin/GammaGroup.ts +++ b/packages/webamp-modern/src/skin/GammaGroup.ts @@ -67,6 +67,7 @@ export default class GammaGroup { canvas.width = safeWidth; canvas.height = safeHeight; const ctx = canvas.getContext("2d"); + ctx.clearRect(0, 0, canvas.width, canvas.height); // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage ctx.drawImage(img, safeLeft, safeTop); const imageData = ctx.getImageData(0, 0, safeWidth, safeHeight); diff --git a/packages/webamp-modern/src/skin/ImageManager.ts b/packages/webamp-modern/src/skin/ImageManager.ts index 0daf3b9e..4b1b6a41 100644 --- a/packages/webamp-modern/src/skin/ImageManager.ts +++ b/packages/webamp-modern/src/skin/ImageManager.ts @@ -36,6 +36,10 @@ export default class ImageManager { this._bitmaps[id] = bitmap; } + isFilePathAdded(filePath:string) { + return Object.keys(this._pathofBitmap).includes(filePath) + } + // Ensure we've loaded the image into our image loader. async loadUniquePaths() { for (const filePath of Object.keys(this._pathofBitmap)) { diff --git a/packages/webamp-modern/src/skin/VM.ts b/packages/webamp-modern/src/skin/VM.ts index 896f73ec..717235ba 100644 --- a/packages/webamp-modern/src/skin/VM.ts +++ b/packages/webamp-modern/src/skin/VM.ts @@ -10,20 +10,19 @@ export default class Vm { // 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[] = []): number { - let ran = 0; - for (const [scriptId, script] of this._scripts.entries()) { + for (const script of this._scripts) { for (const binding of script.bindings) { if ( script.methods[binding.methodOffset].name === event && script.variables[binding.variableOffset].value === object ) { const reversedArgs = [...args].reverse(); - this.interpret(scriptId, binding.commandOffset, reversedArgs); - ran++; + this.interpret(script, binding.commandOffset, reversedArgs); + return 1 } } } - return ran + return 0 } addScript(maki: ParsedMaki): number { @@ -32,8 +31,7 @@ export default class Vm { return index; } - interpret(scriptId: number, commandOffset: number, args: Variable[]) { - const script = this._scripts[scriptId]; + interpret(script: ParsedMaki, commandOffset: number, args: Variable[]) { interpret(commandOffset, script, args, classResolver); } } diff --git a/packages/webamp-modern/src/skin/makiClasses/AnimatedLayer.ts b/packages/webamp-modern/src/skin/makiClasses/AnimatedLayer.ts index 106268df..ecb645d9 100644 --- a/packages/webamp-modern/src/skin/makiClasses/AnimatedLayer.ts +++ b/packages/webamp-modern/src/skin/makiClasses/AnimatedLayer.ts @@ -68,6 +68,7 @@ export default class AnimatedLayer extends Layer { this.gotoframe(frame); UI_ROOT.vm.dispatch(this, "onplay"); if (frame === end) { + this.stop() return; } this._animationInterval = setInterval(() => { @@ -76,6 +77,7 @@ export default class AnimatedLayer extends Layer { if (frame === end) { clearInterval(this._animationInterval); this._animationInterval = null; + this.stop() } }, this._speed); } diff --git a/packages/webamp-modern/src/skin/makiClasses/BaseObject.ts b/packages/webamp-modern/src/skin/makiClasses/BaseObject.ts index 4d3e38f0..fef13820 100644 --- a/packages/webamp-modern/src/skin/makiClasses/BaseObject.ts +++ b/packages/webamp-modern/src/skin/makiClasses/BaseObject.ts @@ -1,19 +1,16 @@ -import UI_ROOT from "../../UIRoot"; - /** * This is the base class from which all other classes inherit. */ export default class BaseObject { - constructor() { - UI_ROOT.addObject(this); - } + static GUID = "516549714a510d87b5a6e391e7f33532"; + /** * Returns the class name for the object. * * @ret The class name. */ - getClassName(): string { - throw new Error("Unimplemented"); + getclassname(): string { + return this.constructor.name; } getId() { diff --git a/packages/webamp-modern/src/skin/makiClasses/Button.ts b/packages/webamp-modern/src/skin/makiClasses/Button.ts index e8f9a164..164ed44a 100644 --- a/packages/webamp-modern/src/skin/makiClasses/Button.ts +++ b/packages/webamp-modern/src/skin/makiClasses/Button.ts @@ -108,25 +108,18 @@ export default class Button extends GuiObj { } setactivatednocallback(onoff: boolean){ - //TODO: + if (onoff !== this._active) { + this._active = onoff; + if (this._active) { + this._div.classList.add("active"); + } else { + this._div.classList.remove("active"); + } + } } 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() { @@ -136,12 +129,13 @@ export default class Button extends GuiObj { handleAction( action: string, param: string | null = null, - actionTarget: string | null = null + actionTarget: string | null = null, + source: GuiObj = null ): boolean { if (actionTarget) { const guiObj = this.findobject(actionTarget); if (guiObj) { - guiObj.handleAction(action, param); + guiObj.handleAction(action, param, null, this); return true; } } diff --git a/packages/webamp-modern/src/skin/makiClasses/ComponentBucket.ts b/packages/webamp-modern/src/skin/makiClasses/ComponentBucket.ts index fa8fe8f6..18369948 100644 --- a/packages/webamp-modern/src/skin/makiClasses/ComponentBucket.ts +++ b/packages/webamp-modern/src/skin/makiClasses/ComponentBucket.ts @@ -66,7 +66,8 @@ export default class ComponentBucket extends Group { handleAction( action: string, param: string | null = null, - actionTarget: string | null = null + actionTarget: string | null = null, + source: GuiObj = null ) { switch (action.toLowerCase()) { case "cb_prev": diff --git a/packages/webamp-modern/src/skin/makiClasses/Config.ts b/packages/webamp-modern/src/skin/makiClasses/Config.ts index c86875b5..e21045cc 100644 --- a/packages/webamp-modern/src/skin/makiClasses/Config.ts +++ b/packages/webamp-modern/src/skin/makiClasses/Config.ts @@ -1,9 +1,10 @@ import XmlObj from "../XmlObj"; +import BaseObject from "./BaseObject"; import ConfigItem from "./ConfigItem"; -const _items : {[key:string]: ConfigItem} = {}; +const _items: { [key: string]: ConfigItem } = {}; -export default class ConfigClass { +export default class Config extends BaseObject { static GUID = "593dba224976d07771f452b90b405536"; newitem(itemName: string, itemGuid: string): ConfigItem { @@ -15,10 +16,13 @@ export default class ConfigClass { getitem(itemGuid: string): ConfigItem { let cfg = _items[itemGuid]; - if(!cfg){ + if (!cfg) { cfg = new ConfigItem(); - _items[itemGuid] = cfg; - } + _items[itemGuid] = cfg; + } return cfg; } } + +// Global Singleton +export const CONFIG: Config = new Config(); diff --git a/packages/webamp-modern/src/skin/makiClasses/Container.ts b/packages/webamp-modern/src/skin/makiClasses/Container.ts index b141d58a..0f7c9ce7 100644 --- a/packages/webamp-modern/src/skin/makiClasses/Container.ts +++ b/packages/webamp-modern/src/skin/makiClasses/Container.ts @@ -69,13 +69,13 @@ export default class Container extends XmlObj { resolveAlias() { const knownContainerGuids = { - "{0000000a-000c-0010-ff7b-01014263450c}": "vis", - "{45f3f7c1-a6f3-4ee6-a15e-125e92fc3f8d}": "pl", - "{6b0edf80-c9a5-11d3-9f26-00c04f39ffc6}": "ml", - "{7383a6fb-1d01-413b-a99a-7e6f655f4591}": "con", - "{7a8b2d76-9531-43b9-91a1-ac455a7c8242}": "lir", - "{a3ef47bd-39eb-435a-9fb3-a5d87f6f17a5}": "dl", - "{f0816d7b-fffc-4343-80f2-e8199aa15cc3}": "video", + "{0000000a-000c-0010-ff7b-01014263450c}": "vis", // visualization + "{45f3f7c1-a6f3-4ee6-a15e-125e92fc3f8d}": "pl", // playlist editor + "{6b0edf80-c9a5-11d3-9f26-00c04f39ffc6}": "ml", // media library + "{7383a6fb-1d01-413b-a99a-7e6f655f4591}": "con", // config? + "{7a8b2d76-9531-43b9-91a1-ac455a7c8242}": "lir", // lyric? + "{a3ef47bd-39eb-435a-9fb3-a5d87f6f17a5}": "dl", // download?? + "{f0816d7b-fffc-4343-80f2-e8199aa15cc3}": "video",// independent video window }; const guid = this._componentGuid; this._componentAlias = knownContainerGuids[guid]; diff --git a/packages/webamp-modern/src/skin/makiClasses/GroupXFade.ts b/packages/webamp-modern/src/skin/makiClasses/GroupXFade.ts index f1158f0c..a9b6d98a 100644 --- a/packages/webamp-modern/src/skin/makiClasses/GroupXFade.ts +++ b/packages/webamp-modern/src/skin/makiClasses/GroupXFade.ts @@ -37,13 +37,21 @@ export default class GroupXFade extends Group { handleAction( action: string, param: string | null = null, - actionTarget: string | null = null - ) { - // if(action.toLowerCase().startsWith('switchto;')){ - // UI_ROOT.vm.dispatch(this, 'onaction', [ - - // ]) - // } + actionTarget: string | null = null, + source: GuiObj = null + ): boolean { + if(action.toLowerCase().startsWith('switchto;')){ + UI_ROOT.vm.dispatch(this, 'onaction', [ + { type: "STRING", value: action }, + { type: "STRING", value: param }, + { type: "INT", value: 0 }, + { type: "INT", value: 0 }, + { type: "INT", value: 0 }, + { type: "INT", value: 0 }, + { type: "OBJECT", value: source }, + ]) + return true + } switch (action.toLowerCase()) { case "groupid": // this._switchTo(action.toLowerCase()); diff --git a/packages/webamp-modern/src/skin/makiClasses/GuiObj.ts b/packages/webamp-modern/src/skin/makiClasses/GuiObj.ts index 5b34f458..e8a9b7db 100644 --- a/packages/webamp-modern/src/skin/makiClasses/GuiObj.ts +++ b/packages/webamp-modern/src/skin/makiClasses/GuiObj.ts @@ -446,6 +446,7 @@ export default class GuiObj extends XmlObj { y >= this.gettop(), "Expected click to be below the component's top" ); + this.getparentlayout().bringtofront() UI_ROOT.vm.dispatch(this, "onleftbuttondown", [ { type: "INT", value: x }, { type: "INT", value: y }, @@ -752,15 +753,10 @@ export default class GuiObj extends XmlObj { handleAction( action: string, param: string | null = null, - actionTarget: string | null = null + actionTarget: string | null = null, + source: GuiObj = null ): boolean { - if (actionTarget) { - const guiObj = this.findobject(actionTarget); - if (guiObj) { - guiObj.handleAction(action, param); - return true; - } - } + // ancestor may override this function. return false; } @@ -783,7 +779,6 @@ export default class GuiObj extends XmlObj { y: number, p1: number, p2: number, - source: GuiObj, ): number { return UI_ROOT.vm.dispatch(this, "onaction", [ { type: "STRING", value: action }, @@ -792,7 +787,7 @@ export default class GuiObj extends XmlObj { { type: "INT", value: y }, { type: "INT", value: p1 }, { type: "INT", value: p2 }, - { type: "OBJECT", value: source }, + { type: "OBJECT", value: this }, ]); } diff --git a/packages/webamp-modern/src/skin/makiClasses/Layout.ts b/packages/webamp-modern/src/skin/makiClasses/Layout.ts index 6b6a7e5c..4aeb8afa 100644 --- a/packages/webamp-modern/src/skin/makiClasses/Layout.ts +++ b/packages/webamp-modern/src/skin/makiClasses/Layout.ts @@ -111,6 +111,10 @@ export default class Layout extends Group { return true; } + getscale(): number { + return 1.0; + } + init() { super.init(); this._invalidateSize(); diff --git a/packages/webamp-modern/src/skin/makiClasses/PlayList.ts b/packages/webamp-modern/src/skin/makiClasses/PlayList.ts new file mode 100644 index 00000000..1f7e6f7a --- /dev/null +++ b/packages/webamp-modern/src/skin/makiClasses/PlayList.ts @@ -0,0 +1,186 @@ +import { Emitter } from "../../utils"; +import AUDIO_PLAYER from "../AudioPlayer"; +// import BaseObject from "./BaseObject"; + +export type Track = { + filename: string; // full url, or just File.name + file?: File; // Blob + metadata?: string; // http://forums.winamp.com/showthread.php?t=345521 + title?: string; + rating?: number; // 0..5 +}; + +/** + * Non GUI element. + * Hold tracs. + * It still exist (not interfered) when skin changed + */ +export class PlEdit { + static GUID = "345beebc49210229b66cbe90d9799aa4"; + // taken from lib/pldir.mi + static guid = "{345BEEBC-0229-4921-90BE-6CB6A49A79D9}"; + _tracks: Track[] = []; + _currentIndex: number = -1; + _selection: number[] = []; + _eventListener: Emitter = new Emitter(); + + // shortcut of this.Emitter + on(event: string, callback: Function): Function { + return 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); + } + + //? ======= General PlEdit Information ======= + getnumtracks(): number { + return this._tracks.length; + } + + getcurrentindex(): number { + return this._currentIndex; + } + + getnumselectedtracks(): number { + return this._selection.length; + } + + getnextselectedtrack(i: number): number { + const current = this._selection.indexOf(i); + const next = this._selection[current + 1]; + return next; + } + + //? ======= Manipulate PlEdit View ======= + // Scrolls the PL to the currently playling + // item (mostly used with onKeyDown: space) + showcurrentlyplayingtrack(): void { + // return unimplementedWarning("showcurrentlyplayingtrack"); + } + + showtrack(item: number): void { + // return unimplementedWarning("showtrack"); + } + + addTrack(track: Track) { + this._tracks.push(track); + + // set audio source if it is the first + if (this._tracks.length == 1) { + this.playtrack(0); + } + + this.trigger("trackchange"); + } + + enqueuefile(file: string): void { + const newTrack: Track = { filename: file }; + this.addTrack(newTrack); + } + + clear(): void { + this._selection = []; + this._tracks = []; + this._currentIndex = null; + } + + removetrack(item: number): void { + // return unimplementedWarning("removetrack"); + } + + swaptracks(item1: number, item2: number): void { + // return unimplementedWarning("swaptracks"); + } + + moveup(item: number): void { + // return unimplementedWarning("moveup"); + } + + movedown(item: number): void { + // return unimplementedWarning("movedown"); + } + + moveto(item: number, pos: number): void { + // return unimplementedWarning("moveto"); + } + + playtrack(item: number): void { + this._currentIndex = item; + const track = this._tracks[item]; + const url = track.file ? URL.createObjectURL(track.file) : track.filename; + AUDIO_PLAYER.setAudioSource(url); + this.trigger("trackchange"); + } + + getCurrentTrackTitle(): string { + if (this._currentIndex < 0) { + return ""; + } + return this.gettitle(this._currentIndex); + } + + getrating(item: number): number { + return this._tracks[item].rating ?? 0; + } + + setrating(item: number, rating: number): void { + this._tracks[item].rating = rating; + } + + gettitle(item: number): string { + return this._tracks[item].filename.split("/").pop(); + } + + // getlength(item: number): string { + // // return unimplementedWarning("getlength"); + // } + + // getmetadata(item: number, metadatastring: string): string { + // // return unimplementedWarning("getmetadata"); + // } + + // getfilename(item: number): string { + // // return unimplementedWarning("getfilename"); + // } + + onpleditmodified(): void { + // return unimplementedWarning("onpleditmodified"); + } +} + +export class PlDir { + static GUID = "61a7abad41f67d7980e1d0b1f4a40386"; + // taken from lib/pldir.mi + static guid = "{61A7ABAD-7D79-41f6-B1D0-E1808603A4F4}"; + + showcurrentlyplayingentry(): void { + // return unimplementedWarning("showcurrentlyplayingentry"); + } + + // getnumitems(): number { + // // return unimplementedWarning("getnumitems"); + // } + + // getitemname(item: number): string { + // // return unimplementedWarning("getitemname"); + // } + + refresh(): void { + // return unimplementedWarning("refresh"); + } + + renameitem(item: number, name: string): void { + // return unimplementedWarning("renameitem"); + } + + enqueueitem(item: number): void { + // return unimplementedWarning("enqueueitem"); + } + + playitem(item: number): void { + // return unimplementedWarning("playitem"); + } +} diff --git a/packages/webamp-modern/src/skin/makiClasses/PlayListGui.ts b/packages/webamp-modern/src/skin/makiClasses/PlayListGui.ts new file mode 100644 index 00000000..584df2df --- /dev/null +++ b/packages/webamp-modern/src/skin/makiClasses/PlayListGui.ts @@ -0,0 +1,118 @@ +import UI_ROOT from "../../UIRoot"; +import { removeAllChildNodes } from "../../utils"; +import Group from "./Group"; +import Slider, { ActionHandler } from "./Slider"; + +export default class PlayListGui extends Group { + static GUID = "pl"; + static guid = "{45F3F7C1-A6F3-4EE6-A15E-125E92FC3F8D}"; + _selectedIndex: number = -1; + _contentPanel: HTMLDivElement = document.createElement("div"); + _slider: Slider = new Slider(); + _sliderHandler: ActionHandler; + + getElTag(): string { + return "group"; + } + + init() { + super.init(); + UI_ROOT.playlist.on("trackchange", this.refresh); + } + + _prepareScrollbar() { + this._slider.setXmlAttributes({ + orientation: "v", + x: "-10", + relatx: "1", + y: "0", + w: "8", + h: "0", + relath: "1", + }); + this._slider.setThumbSize(8, 18); + this._sliderHandler = new PlaylistScrollActionHandler(this._slider, this); + this._slider.setActionHandler(this._sliderHandler); + this._slider.getDiv().classList.add("scrollbar"); + this._slider.draw(); + this.addChild(this._slider); + + this._contentPanel.addEventListener("scroll", this._contentScrolled); + } + + _contentScrolled = () => { + const list = this._contentPanel; + const newPercent = list.scrollTop / (list.scrollHeight - list.clientHeight); + this._slider.setposition((1 - newPercent) * 255); + }; + + _scrollTo(percent: number) { + const list = this._contentPanel; + const newScrollTop = percent * (list.scrollHeight - list.clientHeight); + list.scrollTop = newScrollTop; + } + + // experimental, brutal, just to see reflection of PlayList changes + refresh = () => { + removeAllChildNodes(this._contentPanel); + const pl = UI_ROOT.playlist; + const currentTrack = pl.getcurrentindex(); + for (let i = 0; i < pl.getnumtracks(); i++) { + const line = document.createElement("div"); + if (i == currentTrack) { + line.classList.add("current"); + } + if (i == this._selectedIndex) { + line.classList.add("selected"); + } + line.addEventListener("click", (ev: MouseEvent) => { + this._selectedIndex = i; + this.refresh(); + }); + line.addEventListener("dblclick", (ev: MouseEvent) => { + UI_ROOT.playlist.playtrack(i); + UI_ROOT.audio.play(); + this.refresh(); + }); + line.textContent = `${i+1}. ${pl.gettitle(i)}`; + this._contentPanel.appendChild(line); + } + }; + + itemClick = () => {}; + + draw() { + super.draw(); + this._prepareScrollbar(); + this._div.appendChild(this._contentPanel); + + this._contentPanel.classList.add("content-list"); + this._div.setAttribute("tabindex", "0"); + this._div.classList.add("pl"); + this._div.classList.add("list"); + this._div.style.pointerEvents = "auto"; + } +} + +class PlaylistScrollActionHandler extends ActionHandler { + _pl: PlayListGui; + _scrolling: boolean = false; + + constructor(slider: Slider, pl: PlayListGui) { + super(slider); + this._pl = pl; + } + + onLeftMouseDown(x: number, y: number): void { + this._scrolling = true; + } + onLeftMouseUp(x: number, y: number): void { + this._scrolling = false; + } + + onsetposition(position: number): void { + if (this._scrolling) { + this._pl._scrollTo(1 - position / 255); + } + } +} diff --git a/packages/webamp-modern/src/skin/makiClasses/Slider.ts b/packages/webamp-modern/src/skin/makiClasses/Slider.ts index d5cfdc9b..b58efa01 100644 --- a/packages/webamp-modern/src/skin/makiClasses/Slider.ts +++ b/packages/webamp-modern/src/skin/makiClasses/Slider.ts @@ -2,12 +2,12 @@ import UI_ROOT from "../../UIRoot"; import { assume, clamp, num, px, throttle } from "../../utils"; import GuiObj from "./GuiObj"; -class ActionHandler { +export class ActionHandler { _slider: Slider; constructor(slider: Slider) { - this._slider=slider; + this._slider = slider; } - _subscription: () => void = () => {}; + _subscription: Function = () => {}; // 0-255 onsetposition(position: number): void {} onLeftMouseDown(x: number, y: number): void {} @@ -22,6 +22,9 @@ class ActionHandler { } const MAX = 255; +// Note: FreeMouseMove is about receiving mousemove without mousedown precedent. +// It is useful for equalizer sliders that may changed once a slider is moved + // http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Cslider.2F.3E_.26_.3CWasabi:HSlider.2F.3E_.26_.3CWasabi:VSlider.2F.3E export default class Slider extends GuiObj { static GUID = "62b65e3f408d375e8176ea8d771bb94a"; @@ -39,46 +42,69 @@ export default class Slider extends GuiObj { _thumbHeight: number = 0; _position: number = 0; _param: string | null = null; - // _thumbDiv: HTMLDivElement = document.createElement("div"); _actionHandler: null | ActionHandler; _onSetPositionEvenEaten: number; - _mouseX: number; - _mouseY: number; + _mouseDx: number = 0; // mouseDown inside thumb. 0..thumbHeight + _mouseDy: number = 0; - getRealWidth() { - return this._div.getBoundingClientRect().width; + _getActualSize() { + return this._div.getBoundingClientRect(); } /** + * Central logic of setting new position using mouse + * * set .position by X, Y - * where X,Y is mouseEvent.offsetX & Y + * where X,Y is mouse position inside _div */ _setPositionXY(x: number, y: number) { - //TODO: consider padding. where padding = thumbSize/2 - const width = this.getRealWidth() - this._thumbWidth; - const height = this.getheight() - this._thumbHeight; + if (this._vertical) { + y = y - this._thumbHeight / 2 - this._mouseDy; + } else { + x = x - this._thumbWidth / 2 - this._mouseDx; + } + const actual = this._getActualSize(); + const width = actual.width - this._thumbWidth; + const height = actual.height - this._thumbHeight; const newPercent = this._vertical ? (height - y) / height : x / width; this._position = clamp(newPercent, 0, 1); this._renderThumbPosition(); this.doSetPosition(this.getposition()); } + /** + * Part of central logic that detect whether mouseDown is inside thumb + * @param x mouse position inside _div + * @param y mouse position inside _div + */ + _checkMouseDownInThumb(x: number, y: number) { + if (this._vertical) { + const thumbTop = parseInt( + this._div.style.getPropertyValue("--thumb-top") + ); + const dy = y - thumbTop; + this._mouseDy = + dy >= 0 && dy <= this._thumbHeight ? dy - this._thumbHeight / 2 : 0; + } else { + //? horizontal + const thumbLeft = parseInt( + this._div.style.getPropertyValue("--thumb-left") + ); + const dx = x - thumbLeft; + this._mouseDx = + dx >= 0 && dx <= this._thumbWidth ? dx - this._thumbWidth / 2 : 0; + } + } _registerDragEvents() { - // this._thumbDiv.addEventListener("mousedown", (downEvent: MouseEvent) => { this._div.addEventListener("mousedown", (downEvent: MouseEvent) => { downEvent.stopPropagation(); 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 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._checkMouseDownInThumb(downEvent.offsetX, downEvent.offsetY); this.doLeftMouseDown(downEvent.offsetX, downEvent.offsetY); const handleMove = (moveEvent: MouseEvent) => { @@ -88,19 +114,11 @@ export default class Slider extends GuiObj { const deltaX = newMouseX - startX; const deltaY = newMouseY - startY; - // 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.doSetPosition(this.getposition()); //below is mousePosition conversion relative to inner _div this.doMouseMove(innerX + deltaX, innerY + deltaY); }; - const throttleMouseMove = throttle(handleMove,50) + const throttleMouseMove = throttle(handleMove, 50); const handleMouseUp = (upEvent: MouseEvent) => { upEvent.stopPropagation(); @@ -182,7 +200,6 @@ export default class Slider extends GuiObj { } init() { - // console.log("SLIDER-INITED!"); this._initializeActionHandler(); this._registerDragEvents(); } @@ -227,11 +244,36 @@ export default class Slider extends GuiObj { } } + // called by playlist.scrollbar + setActionHandler(actionHandler: ActionHandler) { + if (this._actionHandler != null) { + this._actionHandler.dispose(); + this._actionHandler = null; + } + this._actionHandler = actionHandler; + } + // called by playlist.scrollbar + setThumbSize(width: number, height: number) { + this._thumbWidth = width; + this._thumbHeight = height; + } + // extern Int Slider.getPosition(); getposition(): number { return this._position * MAX; } + /** + * + * @param newpos 0..MAX + */ + setposition(newpos: number) { + this._position= newpos / MAX; + this._renderThumbPosition(); + this.doSetPosition(this.getposition()); + // console.log("Slider.setPosition:", newpos); + } + onsetposition(newPos: number) { this._onSetPositionEvenEaten = UI_ROOT.vm.dispatch(this, "onsetposition", [ //needed by seekerGhost @@ -261,7 +303,6 @@ export default class Slider extends GuiObj { } } doLeftMouseUp(x: number, y: number) { - // console.log("slider.doLeftMouseUp"); UI_ROOT.vm.dispatch(this, "onleftbuttonup", [ { type: "INT", value: x }, { type: "INT", value: y }, @@ -273,7 +314,6 @@ export default class Slider extends GuiObj { { type: "INT", value: this.getposition() }, ]); if (this._actionHandler != null) { - // console.log("slider_ACTION.doLeftMouseUp"); this._actionHandler.onLeftMouseUp(x, y); } } @@ -288,65 +328,46 @@ export default class Slider extends GuiObj { } } - _renderThumb() { - // this._thumbDiv.style.position = "absolute"; - // this._thumbDiv.setAttribute("data-obj-name", "Slider::Handle"); + _prepareThumbBitmaps() { // 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); - bitmap._setAsBackground(this._div, "thumb-"); - this._div.style.setProperty("--thumb-width", px(bitmap.getWidth())); - this._div.style.setProperty("--thumb-height", px(bitmap.getHeight())); } + this._div.style.setProperty("--thumb-width", px(this._thumbWidth)); + this._div.style.setProperty("--thumb-height", px(this._thumbHeight)); if (this._downThumb != null) { const bitmap = UI_ROOT.getBitmap(this._downThumb); - // 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._setAsBackground(this._div, "thumb-hover-"); } } _renderThumbPosition() { - if (this._thumb != null) { - // const bitmap = UI_ROOT.getBitmap(this._thumb); - // TODO: What if the orientation has changed? - if (this._vertical) { - const 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 = (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)); - } + const actual = this._getActualSize(); + if (this._vertical) { + const top = (1 - this._position) * (actual.height - this._thumbHeight); + this._div.style.setProperty("--thumb-top", px(Math.max(0, top))); + } else { + const left = this._position * (actual.width - this._thumbWidth); + this._div.style.setProperty("--thumb-left", px(left)); } } - + draw() { super.draw(); this._div.setAttribute("data-obj-name", "Slider"); assume(this._barLeft == null, "Need to handle Slider barleft"); assume(this._barRight == null, "Need to handle Slider barright"); assume(this._barMiddle == null, "Need to handle Slider barmiddle"); - this._renderThumb(); + this._div.style.setProperty("--thumb-left", px(0)); + this._div.style.setProperty("--thumb-top", px(0)); + this._prepareThumbBitmaps(); this._renderThumbPosition(); // this._div.appendChild(this._thumbDiv); } @@ -397,8 +418,8 @@ class SeekActionHandler extends ActionHandler { _onAudioProgres = () => { if (!this._pendingChange) { - if (this._slider.getId() == "seekerghost") - console.log("thumb: not isPending()!"); + // 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(); diff --git a/packages/webamp-modern/src/skin/makiClasses/SystemObject.ts b/packages/webamp-modern/src/skin/makiClasses/SystemObject.ts index 56f426dd..2563d8f3 100644 --- a/packages/webamp-modern/src/skin/makiClasses/SystemObject.ts +++ b/packages/webamp-modern/src/skin/makiClasses/SystemObject.ts @@ -7,6 +7,8 @@ import Group from "./Group"; import PRIVATE_CONFIG from "../PrivateConfig"; import UI_ROOT from "../../UIRoot"; import GuiObj from "./GuiObj"; +import Config, { CONFIG } from "./Config"; +import WinampConfig, { WINAMP_CONFIG } from "./WinampConfig"; import { AUDIO_PAUSED, AUDIO_STOPPED, AUDIO_PLAYING } from "../AudioPlayer"; @@ -72,6 +74,16 @@ export default class SystemObject extends BaseObject { } initialVariable.value = this; + for (const vari of this._parsedScript.variables) { + if (vari.type == "OBJECT") { + if (vari.guid == Config.GUID) { + vari.value = CONFIG; + } else if (vari.guid == WinampConfig.GUID) { + vari.value = WINAMP_CONFIG; + } + } + } + UI_ROOT.vm.addScript(this._parsedScript); UI_ROOT.vm.dispatch(this, "onscriptloaded"); } @@ -371,7 +383,7 @@ export default class SystemObject extends BaseObject { * @param str The string. */ strlen(str: string): number { - return str.length; + return str ? str.length : 0; } /** @@ -641,7 +653,7 @@ export default class SystemObject extends BaseObject { * @ret The value of the left vu meter. */ getleftvumeter(): number { - return 0; + return UI_ROOT.audio._vuMeter; } /** @@ -651,7 +663,7 @@ export default class SystemObject extends BaseObject { * @ret The value of the right vu meter. */ getrightvumeter(): number { - return 0; + return UI_ROOT.audio._vuMeter; } /** @@ -770,6 +782,14 @@ export default class SystemObject extends BaseObject { return "Niente da Caprie"; } + getplaylistlength(): number { + return UI_ROOT.playlist.getnumtracks(); + } + + getplaylistindex(): number { + return UI_ROOT.playlist.getcurrentindex(); + } + /** * getPlayItemMetaDataString() * @@ -1621,10 +1641,10 @@ export default class SystemObject extends BaseObject { //TODO: } - istransparencyavailable():boolean { - return true + istransparencyavailable(): boolean { + return true; } - + translate(str: string): string { return str; } @@ -1638,6 +1658,9 @@ export default class SystemObject extends BaseObject { iskeydown(vk: number): number { return 0; } + isminimized(): number { + return 0; + } } function dumpScriptDebug(script: ParsedMaki) { diff --git a/packages/webamp-modern/src/skin/makiClasses/Text.ts b/packages/webamp-modern/src/skin/makiClasses/Text.ts index d13e8c33..52dc1190 100644 --- a/packages/webamp-modern/src/skin/makiClasses/Text.ts +++ b/packages/webamp-modern/src/skin/makiClasses/Text.ts @@ -18,11 +18,12 @@ export default class Text extends GuiObj { _display: string; _displayValue: string = ""; _disposeDisplaySubscription: () => void | null = null; + _disposeTrackChangedSubscription: () => void | null = null; _text: string; _bold: boolean; _forceuppercase: boolean; _forcelowercase: boolean; - _align: string; + _align: string = "center"; _font_id: string; _font_obj: TrueTypeFont | BitmapFont; _fontSize: number; @@ -36,6 +37,9 @@ export default class Text extends GuiObj { _scrollPaused: boolean = false; _scrollLeft: number = 0; // logically, not visually _textFullWidth: number; //calculated, not runtime by css + _shadowColor: string; + _shadowX: number = 0; + _shadowY: number = 0; _drawn: boolean = false; // needed to check has parents constructor() { @@ -82,6 +86,7 @@ export default class Text extends GuiObj { case "align": // (str) One of the following three possible strings: "left" "center" "right" -- Default is "left." this._align = value; + this._prepareCss(); break; case "fontsize": // (int) The size to render the chosen font. @@ -105,14 +110,29 @@ export default class Text extends GuiObj { this._timeColonWidth = num(value); this._prepareCss(); this._renderText(); + break; + + case "shadowcolor": + // (int) The comma delimited RGB color for underrendered shadow text. + this._shadowColor = value; + this._prepareCss(); + break; + case "shadowx": + // (int) The x offset of the shadowrender. + this._shadowX = num(value); + this._prepareCss(); + break; + case "shadowy": + // (int) The x offset of the shadowrender. + this._shadowY = num(value); + this._prepareCss(); + break; + /* antialias - (bool) Setting this flag causes the text to be rendered antialiased if possible. default - (str) A parameter alias for text. align - (str) One of the following three possible strings: "left" "center" "right" -- Default is "left." valign - (str) One of the following three possible strings: "top" "center" "bottom" -- Default is "top." -shadowcolor - (int) The comma delimited RGB color for underrendered shadow text. -shadowx - (int) The x offset of the shadowrender. -shadowy - (int) The y offset of the shadowrender. timeroffstyle - (int) How to display an empty timer: "0" = " : ", "1" = "00:00", and "2"="" (if one is displaying time) nograb - (bool) Setting this flag will cause the text object to ignore left button down messages. Default is off. showlen - (bool) Setting this flag will cause the text display to be appended with the length in minutes and seconds of the current song. Default is off. @@ -142,6 +162,20 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x } } + // only applicable for TrueType Font + _autoDetectColor() { + if (this._color) { + if (this._color.split(",").length == 3) { + this._div.style.color = `rgb(${this._color})`; + return; + } + const color = UI_ROOT.getColor(this._color); + if (color) { + this._div.style.color = `var(${color.getCSSVar()}, ${color.getRgb()})`; + } + } + } + ensureFontSize() { if (this._font_obj instanceof TrueTypeFont && this._fontSize) { const canvas = document.createElement("canvas"); @@ -163,8 +197,16 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x if (this._ticker && this._ticker != "off") { this._prepareScrolling(); } + this._div.addEventListener("click", this._onClick); } + _onClick = () => { + if (this._display.toLowerCase() == "time") { + UI_ROOT.audio.toggleRemainingTime(); + this.setDisplayValue(integerToTime(UI_ROOT.audio.getCurrentTime())); + } + }; + _setDisplay(display: string) { if (display.toLowerCase() === this._display?.toLowerCase()) { return; @@ -172,6 +214,9 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x if (this._disposeDisplaySubscription != null) { this._disposeDisplaySubscription(); } + if (this._disposeTrackChangedSubscription != null) { + this._disposeTrackChangedSubscription(); + } this._display = display; switch (this._display.toLowerCase()) { case "": @@ -198,8 +243,14 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x // this._displayValue = "Niente da Caprie (3"; // break; case "songtitle": - this._displayValue = "Your Favorite MP3 Song Title, U R Reading"; - // this._displayValue = "Short MP3 Title"; + this._displayValue = UI_ROOT.playlist.getCurrentTrackTitle(); + this._disposeTrackChangedSubscription = UI_ROOT.playlist.on( + "trackchange", + () => { + this._displayValue = UI_ROOT.playlist.getCurrentTrackTitle(); + this._renderText(); + } + ); break; case "songbitrate": case "songsamplerate": @@ -219,6 +270,9 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x if (newValue !== this._displayValue) { this._displayValue = newValue; this._renderText(); + UI_ROOT.vm.dispatch(this, "ontextchanged", [ + { type: "STRING", value: this.gettext() }, + ]); } } @@ -247,7 +301,8 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x // TODO } - //to speedup, we spit render. This is only rendering style + //to speedup animation like text-scrolling, we spit rendering processes. + //This function is only rendering static styles _prepareCss() { if (!this._font_obj && this._font_id) { this._font_obj = UI_ROOT.getFont(this._font_id); @@ -255,21 +310,29 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x const font = this._font_obj; if (font instanceof BitmapFont) { this._textWrapper.setAttribute("font", "BitmapFont"); + //? font-size this._div.style.setProperty( "--fontSize", (this._fontSize || "~").toString() ); + //? text-align + if (this._align != "center") { + this._div.style.setProperty("--align", this._align); + } else { + this._div.style.removeProperty("--align"); + } + //? margin + this._div.style.setProperty("--hspacing", px(font.getHorizontalSpacing())); + 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()})`; - } + this._autoDetectColor(); + if (this._shadowColor) { + this._div.style.textShadow = `${this._shadowX}px ${this._shadowY}px rgb(${this._shadowColor})`; } if (font instanceof TrueTypeFont) { this._textWrapper.setAttribute("font", "TrueType"); @@ -412,7 +475,7 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x } doScrollText() { - const curL = this._scrollLeft; + const curL = this._scrollLeft; const step = 1; //pixel const idle = 20; //when overflow const container = this._div.getBoundingClientRect(); diff --git a/packages/webamp-modern/src/skin/makiClasses/ToggleButton.ts b/packages/webamp-modern/src/skin/makiClasses/ToggleButton.ts index dc417e87..13a91541 100644 --- a/packages/webamp-modern/src/skin/makiClasses/ToggleButton.ts +++ b/packages/webamp-modern/src/skin/makiClasses/ToggleButton.ts @@ -28,6 +28,10 @@ export default class ToggleButton extends Button { UI_ROOT.vm.dispatch(this, "ontoggle", [V.newBool(onoff)]); } + onactivate(activated: number){ + UI_ROOT.vm.dispatch(this, "onactivate", [{type: "INT", value: activated}]); + } + draw() { super.draw(); this._div.setAttribute("data-obj-name", "ToggleButton"); diff --git a/packages/webamp-modern/src/skin/makiClasses/WasabiButton.ts b/packages/webamp-modern/src/skin/makiClasses/WasabiButton.ts index da6cff27..9e6276a7 100644 --- a/packages/webamp-modern/src/skin/makiClasses/WasabiButton.ts +++ b/packages/webamp-modern/src/skin/makiClasses/WasabiButton.ts @@ -1,28 +1,16 @@ +import UI_ROOT from "../../UIRoot"; 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"); + // static GUID = "unknown"; + getElTag(): string { + return "button"; + } - 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'); + constructor() { + super(); + this.registerDimensions(); } setXmlAttr(key: string, value: string): boolean { @@ -31,7 +19,7 @@ export default class WasabiButton extends Button { } switch (key) { case "text": - this._m.innerText = value; + this._div.innerText = value; break; default: return false; @@ -39,15 +27,20 @@ export default class WasabiButton extends Button { return true; } + registerDimensions() { + UI_ROOT.addHeight("button-border-top", "wasabi.button.top"); + UI_ROOT.addHeight("button-border-bottom", "wasabi.button.bottom"); + UI_ROOT.addWidth("button-border-left", "wasabi.button.left"); + UI_ROOT.addWidth("button-border-right", "wasabi.button.right"); + } + draw() { super.draw(); - this._div.classList.add('wasabi-button') + this._div.classList.remove("webamp--img"); + this._div.classList.add("wasabi"); this._div.setAttribute("data-obj-name", "WasabiButton"); } -// _renderBackground() { - -// } /* extern ToggleButton.onToggle(Boolean onoff); extern int TOggleButton.getCurCfgVal() diff --git a/packages/webamp-modern/src/skin/makiClasses/WinampConfig.ts b/packages/webamp-modern/src/skin/makiClasses/WinampConfig.ts index c56f9acb..03c5339e 100644 --- a/packages/webamp-modern/src/skin/makiClasses/WinampConfig.ts +++ b/packages/webamp-modern/src/skin/makiClasses/WinampConfig.ts @@ -1,8 +1,9 @@ +import BaseObject from "./BaseObject"; import ConfigItem from "./ConfigItem"; const _items: { [key: string]: ConfigItem } = {}; -export default class WinampConfig { +export default class WinampConfig extends BaseObject { static GUID = "b2ad3f2b4e3131ed95e96dbcbb55d51c"; // _items : {[key:string]: ConfigItem} = {}; @@ -27,6 +28,5 @@ export class WinampConfigGroup { } } -// Global Singleton for now -// export const Config = new ConfigClass(); -// export Config; +// Global Singleton +export const WINAMP_CONFIG: WinampConfig = new WinampConfig(); diff --git a/packages/webamp-modern/src/skin/makiClasses/XuiElement.ts b/packages/webamp-modern/src/skin/makiClasses/XuiElement.ts new file mode 100644 index 00000000..043ca652 --- /dev/null +++ b/packages/webamp-modern/src/skin/makiClasses/XuiElement.ts @@ -0,0 +1,53 @@ +import Group from "./Group"; +import UI_ROOT from "../../UIRoot"; +import { num } from "../../utils"; + +export default class XuiElement extends Group { + __inited: boolean = false; + + _unhandledXuiParams: { key: string; value: string }[] = []; //https://github.com/captbaritone/webamp/pull/1161#discussion_r830527754 + // _content: string; + // _shade: string; + // _padtitleleft: string; + // _padtitleright: string; + + getElTag(): string { + return "group"; + } + + setXmlAttr(_key: string, value: string): boolean { + const lowerkey = _key.toLowerCase(); + // console.log('wasabi:frame.key=',lowerkey,':=', value) + if (super.setXmlAttr(lowerkey, value)) { + return true; + } + this._unhandledXuiParams.push({ key: lowerkey, value }); + return true; + } + + init() { + if (this.__inited) return; + this.__inited = true; + + super.init(); + + for (const systemObject of this._systemObjects) { + this._unhandledXuiParams.forEach(({ key, value }) => { + UI_ROOT.vm.dispatch(systemObject, "onsetxuiparam", [ + { type: "STRING", value: key }, + { type: "STRING", value: value }, + ]); + }); + // ["content", "padtitleleft", "padtitleright", "shade"].forEach((att) => { + // const myValue = this["_" + att]; + // if (myValue != null) { + // UI_ROOT.vm.dispatch(systemObject, "onsetxuiparam", [ + // { type: "STRING", value: att }, + // { type: "STRING", value: myValue }, + // ]); + // } + // }); + } + this._unhandledXuiParams = []; + } +} diff --git a/packages/webamp-modern/src/skin/parse.ts b/packages/webamp-modern/src/skin/parse.ts index 37b55fa0..00305a55 100644 --- a/packages/webamp-modern/src/skin/parse.ts +++ b/packages/webamp-modern/src/skin/parse.ts @@ -32,6 +32,9 @@ import WasabiTitle from "./makiClasses/WasabiTitle"; import ComponentBucket from "./makiClasses/ComponentBucket"; import GroupXFade from "./makiClasses/GroupXFade"; import { classResolver } from "./resolver"; +import WasabiButton from "./makiClasses/WasabiButton"; +import PlayListGui from "./makiClasses/PlayListGui"; +import XuiElement from "./makiClasses/XuiElement"; function hack() { // Without this Snowpack will try to treeshake out resolver causing a circular @@ -159,28 +162,29 @@ export default class SkinParser { //? But in the same time we need to reduce code complexity //? So, temporary we are trying to not do Promise.all - // if (this._phase == RESOURCE_PHASE) { - // return await Promise.all( - // node.children.map((child) => { - // if (child instanceof XmlElement) { - // // console.log('traverse->', parent.name, child.name) - // this._scanRes(child); - // return this.traverseChild(child, parent); - // } - // }) - // ); - // } else { - for (const child of node.children) { - if (child instanceof XmlElement) { - this._scanRes(child); - await this.traverseChild(child, parent); + if (this._phase == RESOURCE_PHASE) { + return await Promise.all( + node.children.map((child) => { + if (child instanceof XmlElement) { + // console.log('traverse->', parent.name, child.name) + this._scanRes(child); + return this.traverseChild(child, parent); + } + }) + ); + } else { + for (const child of node.children) { + if (child instanceof XmlElement) { + this._scanRes(child); + await this.traverseChild(child, parent); + } } } - // } } async traverseChild(node: XmlElement, parent: any) { - switch (node.name.toLowerCase()) { + const tag = node.name.toLowerCase(); + switch (tag) { case "albumart": return this.albumart(node, parent); case "wasabixml": @@ -263,9 +267,17 @@ export default class SkinParser { case "wasabi:medialibraryframe:nostatus": case "wasabi:playlistframe:nostatus": case "wasabi:standardframe:nostatus": + case "wasabi:standardframe:nostatus:short": case "wasabi:standardframe:status": + case "wasabi:standardframe:modal:short": case "wasabi:visframe:nostatus": return this.wasabiFrame(node, parent); + case "buttonled": + case "fadebutton": + case "fadetogglebutton": + case "configcheckbox": + //temporary, to localize error + return this.dynamicXuiElement(node, parent) case "componentbucket": return this.componentBucket(node, parent); case "playlisteditor": @@ -286,6 +298,9 @@ export default class SkinParser { case "wrapper": return this.traverseChildren(node, parent); default: + // if(this._uiRoot.getXuiElement(tag)) { + // return this.dynamicXuiElement(node, parent) + // } console.warn(`Unhandled XML node type: ${node.name}`); return; } @@ -357,6 +372,16 @@ export default class SkinParser { this._uiRoot.addComponentBucket(bucket.getWindowType(), bucket); } + async dynamicXuiElement(node: XmlElement, parent: any) { + const xuitag: string = node.name; // eg. Wasabi:MainFrame:NoStatus + const xuiEl: XmlElement = this._uiRoot.getXuiElement(xuitag); + if (xuiEl) { + const xuiFrame = new XmlElement("dummy", { id: xuiEl.attributes.id }); + const Element:XuiElement = await this.newGroup(XuiElement, xuiFrame, parent); + Element.setXmlAttributes(node.attributes); + // await this.maybeApplyGroupDef(frame, xuiFrame); + } + } async wasabiFrame(node: XmlElement, parent: any) { const frame = new WasabiFrame(); this.addToGroup(frame, parent); @@ -584,7 +609,91 @@ export default class SkinParser { this._res.bitmaps["studio.button.pressed.bottom"] = false; this._res.bitmaps["studio.button.pressed.lowerRight"] = false; - return this.newGui(Button, node, parent); + await this.buildWasabiButtonFace(); + + return this.newGui(WasabiButton, node, parent); + } + + async buildWasabiButtonFace() { + const face = this._uiRoot.getBitmap("studio.button"); + // if (face == null && upperLeft !== null) { + if (!face) { + let upperLeft = this._uiRoot.getBitmap("studio.button.upperLeft"); + if (upperLeft) { + //? default + let bottomRight = this._uiRoot.getBitmap("studio.button.lowerRight"); + let dict: { + [attrName: string]: string; + } = { + id: "studio.button", + file: upperLeft.getFile(), + x: String(upperLeft.getLeft()), + y: String(upperLeft.getTop()), + w: String( + bottomRight.getLeft() - upperLeft.getLeft() + bottomRight.getWidth() + ), + h: String( + bottomRight.getTop() - upperLeft.getTop() + bottomRight.getHeight() + ), + }; + const btnFace = new XmlElement("bitmap", { ...dict }); + await this.bitmap(btnFace); + + //? pressed + upperLeft = this._uiRoot.getBitmap("studio.button.pressed.upperLeft"); + bottomRight = this._uiRoot.getBitmap( + "studio.button.pressed.lowerRight" + ); + dict = { + id: "studio.button.pressed", + file: upperLeft.getFile(), + x: String(upperLeft.getLeft()), + y: String(upperLeft.getTop()), + w: String( + bottomRight.getLeft() - upperLeft.getLeft() + bottomRight.getWidth() + ), + h: String( + bottomRight.getTop() - upperLeft.getTop() + bottomRight.getHeight() + ), + }; + const btnPressedFace = new XmlElement("bitmap", { ...dict }); + await this.bitmap(btnPressedFace); + } else { + // we can't find ingredient, lets search the raw material + if (!this._imageManager.isFilePathAdded("window/window-elements.png")) + return; + + //? default + let dict: { + [attrName: string]: string; + } = { + id: "studio.button", + file: "window/window-elements.png", + x: "1", + y: "135", + w: "31", + h: "31", + }; + const btnFace = new XmlElement("bitmap", { ...dict }); + await this.bitmap(btnFace); + + //? pressed + dict = { + id: "studio.button.pressed", + file: "window/window-elements.png", + x: "67", + y: "135", + w: "31", + h: "31", + }; + const btnPressedFace = new XmlElement("bitmap", { ...dict }); + await this.bitmap(btnPressedFace); + } + + //TODO: why this new created bitmap doesn't loaded? + await this._imageManager.loadUniquePaths(); + await this._imageManager.ensureBitmapsLoaded(); + } } async toggleButton(node: XmlElement, parent: any) { @@ -679,6 +788,12 @@ export default class SkinParser { const groupDef = this._uiRoot.getGroupDef(groupdef_id); if (groupDef != null) { group.setXmlAttributes(groupDef.attributes); + if (groupDef.attributes.inherit_group) { + await this.maybeApplyGroupDefId( + group, + groupDef.attributes.inherit_group + ); + } await this.traverseChildren(groupDef, group); // TODO: Maybe traverse groupDef's children? } @@ -705,6 +820,13 @@ export default class SkinParser { } async component(node: XmlElement, parent: any) { + //TODO: parse dynamic element by guid value + if ( + node.attributes.param == "guid:{45F3F7C1-A6F3-4ee6-A15E-125E92FC3F8D}" + ) { + await this.buildWasabiButtonFace(); + return this.newGui(PlayListGui, node, parent); + } await this.traverseChildren(node, parent); } @@ -716,9 +838,23 @@ export default class SkinParser { } async colorThemesList(node: XmlElement, parent: any) { + this.buildWasabiScrollbarDimension() return this.newGui(ColorThemesList, node, parent); } + buildWasabiScrollbarDimension() { + this._uiRoot.addWidth("vscrollbar-width", "wasabi.scrollbar.vertical.left"); + this._uiRoot.addHeight("vscrollbar-btn-height", "wasabi.scrollbar.vertical.left"); + this._uiRoot.addHeight("vscrollbar-thumb-height", "wasabi.scrollbar.vertical.button"); + this._uiRoot.addHeight("vscrollbar-thumb-height2", "studio.scrollbar.vertical.button"); + + this._uiRoot.addHeight("hscrollbar-height", "wasabi.scrollbar.horizontal.left"); + this._uiRoot.addWidth("hscrollbar-btn-width", "wasabi.scrollbar.horizontal.left"); + this._uiRoot.addWidth("hscrollbar-thumb-width", "wasabi.scrollbar.horizontal.button"); + this._uiRoot.addWidth("hscrollbar-thumb-width2", "studio.scrollbar.horizontal.button"); + + } + async layoutStatus(node: XmlElement, parent: any) { assume( node.children.length === 0, @@ -812,7 +948,7 @@ export default class SkinParser { } //replace children mother.children.splice(0, mother.children.length, ...nonGroupDefs); - } //eof function + }; //eof function // Note: Included files don't have a single root node, so we add a synthetic one. // A different XML parser library might make this unnessesary. diff --git a/packages/webamp-modern/src/skin/resolver.ts b/packages/webamp-modern/src/skin/resolver.ts index ef36eb5f..a15266d2 100644 --- a/packages/webamp-modern/src/skin/resolver.ts +++ b/packages/webamp-modern/src/skin/resolver.ts @@ -1,4 +1,5 @@ import { getClass } from "../maki/objects"; +import BaseObject from "./makiClasses/BaseObject"; import Button from "./makiClasses/Button"; import SystemObject from "./makiClasses/SystemObject"; import Container from "./makiClasses/Container"; @@ -23,11 +24,13 @@ import WinampConfig, { WinampConfigGroup } from "./makiClasses/WinampConfig"; import ComponentBucket from "./makiClasses/ComponentBucket"; import AlbumArt from "./makiClasses/AlbumArt"; import Region from "./makiClasses/Region"; +import { PlEdit, PlDir } from "./makiClasses/PlayList"; const CLASSES = [ + BaseObject, Config, - Config, ConfigItem, ConfigAttribute, + ConfigItem, ConfigAttribute, WinampConfig, WinampConfigGroup, ComponentBucket, Region, @@ -48,6 +51,7 @@ const CLASSES = [ Timer, Slider, Vis, + PlEdit, PlDir, GuiObj, ]; diff --git a/packages/webamp-modern/src/utils.ts b/packages/webamp-modern/src/utils.ts index 86895da0..1d8abd13 100644 --- a/packages/webamp-modern/src/utils.ts +++ b/packages/webamp-modern/src/utils.ts @@ -72,7 +72,7 @@ export function removeAllChildNodes(parent: Element) { export function integerToTime(seconds: number): string { const mins = Math.floor(seconds / 60); - const secs = String(Math.round(seconds % 60)).padStart(2, "0"); + const secs = String(Math.abs(Math.round(seconds % 60))).padStart(2, "0"); return `${mins}:${secs}`; } @@ -122,14 +122,14 @@ export class Emitter { _cbs: { [event: string]: Array } = {}; // call this to register a callback to a specific event - on(event: string, cb: Function) { + on(event: string, cb: Function): Function { if (this._cbs[event] == null) { this._cbs[event] = []; } this._cbs[event].push(cb); // return a function for later unregistering - return () => { + return () => { //TODO: consider using this.off(), or integrate both this._cbs[event] = this._cbs[event].filter((c) => c !== cb); };