A music player that connects to your cloud/distributed storage.
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: proper window management in webamp theme

+303 -87
+4 -1
deno.jsonc
··· 13 13 "@vicary/debounce-microtask": "jsr:@vicary/debounce-microtask@^0.1.8", 14 14 "alien-signals": "npm:alien-signals@^3.0.0", 15 15 "idb-keyval": "npm:idb-keyval@^6.2.2", 16 + "lit-html": "npm:lit-html@^3.3.1", 16 17 "morphdom": "npm:morphdom@^2.7.7/dist/morphdom.js", 17 18 "query-string": "npm:query-string@^9.3.1", 18 19 "subsonic-api": "npm:subsonic-api@^3.2.0", 19 20 "throttle-debounce": "npm:throttle-debounce@^5.0.2", 20 21 "uint8arrays": "npm:uint8arrays@^5.1.0", 21 22 "uri-js": "npm:uri-js@^4.4.1", 22 - "webamp": "npm:webamp@^2.2.0", 23 23 "xxh32": "npm:xxh32@^2.0.5", 24 24 25 25 // music-metadata ··· 27 27 "@tokenizer/http": "https://esm.sh/@tokenizer/http@0.9.2/lib/http-client.js", 28 28 "@tokenizer/range": "https://esm.sh/@tokenizer/range@0.13.0/lib/index.js", 29 29 "music-metadata": "https://esm.sh/music-metadata@11.9.0/lib/core.js", 30 + 31 + // Webamp 32 + "webamp": "npm:webamp@^2.2.0", 30 33 31 34 // Paths 32 35 "@common/": "./src/common/",
+11
deno.lock
··· 41 41 "npm:autoprefixer@10.4.21": "10.4.21_postcss@8.5.6", 42 42 "npm:idb-keyval@^6.2.2": "6.2.2", 43 43 "npm:lightningcss-wasm@1.30.1": "1.30.1", 44 + "npm:lit-html@^3.3.1": "3.3.1", 44 45 "npm:markdown-it-attrs@4.3.1": "4.3.1_markdown-it@14.1.0", 45 46 "npm:markdown-it-deflist@3.0.0": "3.0.0", 46 47 "npm:markdown-it@14.1.0": "14.1.0", ··· 258 259 "dependencies": [ 259 260 "csstype" 260 261 ] 262 + }, 263 + "@types/trusted-types@2.0.7": { 264 + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" 261 265 }, 262 266 "@types/use-sync-external-store@0.0.3": { 263 267 "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" ··· 607 611 "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", 608 612 "dependencies": [ 609 613 "uc.micro" 614 + ] 615 + }, 616 + "lit-html@3.3.1": { 617 + "integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==", 618 + "dependencies": [ 619 + "@types/trusted-types" 610 620 ] 611 621 }, 612 622 "lodash@4.17.21": { ··· 1392 1402 "npm:98.css@~0.1.21", 1393 1403 "npm:alien-signals@3", 1394 1404 "npm:idb-keyval@^6.2.2", 1405 + "npm:lit-html@^3.3.1", 1395 1406 "npm:morphdom@^2.7.7", 1396 1407 "npm:query-string@^9.3.1", 1397 1408 "npm:subsonic-api@^3.2.0",
+7 -31
src/common/element.js
··· 1 - import morphdom from "morphdom"; 1 + import { html, render } from "lit-html"; 2 2 3 3 import { effect, signal } from "@common/signal.js"; 4 4 import { define, use } from "@common/worker.js"; 5 5 6 6 /** 7 7 * @import {BroadcastingStatus, FnParams, FnReturn, HtmlTagFunction, MorphOptions} from "./element.d.ts" 8 - * @import {Signal, SignalReader} from "./signal.d.ts" 8 + * @import {Signal} from "./signal.d.ts" 9 9 */ 10 10 11 11 /** ··· 21 21 22 22 constructor() { 23 23 super(); 24 - 25 24 this.group = this.getAttribute("group") || crypto.randomUUID(); 26 - this.morphedRender = this.morphedRender.bind(this); 27 25 } 28 26 29 27 /** ··· 32 30 * @param {string} newValue 33 31 */ 34 32 attributeChangedCallback(_name, oldValue, newValue) { 35 - if (oldValue !== newValue) this.morphedRender(); 33 + if (oldValue !== newValue) this.#render(); 36 34 } 37 35 38 36 /** ··· 46 44 } 47 45 48 46 /** 49 - * @type {HtmlTagFunction} 50 - */ 51 - html(strings, ...values) { 52 - return String.raw({ raw: strings }, ...values); 53 - } 54 - 55 - /** 56 47 * Avoid replacing the whole subtree, 57 48 * morph the existing DOM into the new given tree. 58 49 */ 59 - morphedRender() { 50 + #render() { 60 51 if (!("render" in this && typeof this.render === "function")) return; 61 52 62 53 const tmp = this.render({ 63 - html: this.html, 54 + html: html, 64 55 state: "state" in this ? this.state : undefined, 65 56 }); 66 57 67 - const updated = document.createElement("div"); 68 - updated.innerHTML = tmp.trim(); 69 58 const root = this.shadowRoot ? this.shadowRoot : this; 70 - 71 - morphdom( 72 - root, 73 - updated, 74 - { 75 - ...this.morphOptions, 76 - childrenOnly: true, 77 - }, 78 - ); 59 + render(tmp, root); 79 60 } 80 61 81 - // MORPH STUFF 82 - 83 - /** @type {MorphOptions} */ 84 - morphOptions = {}; 85 - 86 62 // LIFECYCLE 87 63 88 64 connectedCallback() { ··· 90 66 91 67 this.effect(() => { 92 68 if (!("render" in this && typeof this.render === "function")) return; 93 - this.morphedRender(); 69 + this.#render(); 94 70 }); 95 71 } 96 72
+68
src/theme/webamp/index.css
··· 26 26 font-family: "Pixelated MS Sans Serif", sans-serif; 27 27 font-size: 12px; 28 28 margin: 12px; 29 + overflow: hidden; 29 30 } 30 31 31 32 #webamp { 32 33 isolation: isolate; 33 34 } 34 35 36 + main > section { 37 + inset: 0; 38 + position: absolute; 39 + } 40 + 35 41 /*********************************** 36 42 * Desktop 37 43 ***********************************/ ··· 39 45 display: flex; 40 46 flex-wrap: wrap; 41 47 gap: 12px; 48 + inset: 12px; 42 49 } 43 50 44 51 .desktop__item { ··· 73 80 } 74 81 } 75 82 } 83 + 84 + /*********************************** 85 + * Windows 86 + ***********************************/ 87 + 88 + .windows dtw-window { 89 + left: 12px; 90 + position: absolute; 91 + top: 12px; 92 + z-index: 999; 93 + 94 + /* Waiting on https://developer.mozilla.org/en-US/docs/Web/CSS/sibling-index#browser_compatibility */ 95 + &:nth-child(1) { 96 + left: 24px; 97 + top: 24px; 98 + } 99 + 100 + &:nth-child(2) { 101 + left: 36px; 102 + top: 36px; 103 + } 104 + 105 + &:nth-child(3) { 106 + left: 48px; 107 + top: 48px; 108 + } 109 + 110 + &:nth-child(4) { 111 + left: 60px; 112 + top: 60px; 113 + } 114 + 115 + &:nth-child(5) { 116 + left: 72px; 117 + top: 72px; 118 + } 119 + 120 + &:nth-child(6) { 121 + left: 84px; 122 + top: 84px; 123 + } 124 + 125 + &:nth-child(7) { 126 + left: 96px; 127 + top: 96px; 128 + } 129 + 130 + &:nth-child(8) { 131 + left: 108px; 132 + top: 108px; 133 + } 134 + 135 + &:nth-child(9) { 136 + left: 120px; 137 + top: 120px; 138 + } 139 + } 140 + 141 + .windows section { 142 + z-index: 999; 143 + }
+16 -37
src/theme/webamp/index.js
··· 1 - import Webamp from "webamp/lazy"; 2 1 import deepDiff from "@fry69/deep-diff"; 3 2 4 3 // import "@component/orchestrator/process-tracks/element.js"; ··· 14 13 15 14 import "./window/element.js"; 16 15 import "./window-manager/element.js"; 16 + import WebampElement from "./webamp.js"; 17 17 18 18 /** 19 19 * @import {URLTrack} from "webamp" ··· 27 27 globalThis.queue = queue; 28 28 29 29 //////////////////////////////////////////// 30 - // ⚡ 30 + // 📡 31 31 //////////////////////////////////////////// 32 32 33 - /** @type {import("webamp/lazy").default} */ 34 - const amp = new /** @type {any} */ (Webamp)({ 35 - enableMediaSession: true, 36 - initialTracks: [], 33 + const $currTrack = signal(/** @type {null | number} */ (null)); 34 + const $playlist = signal(/** @type {Item[]} */ ([])); 35 + 36 + //////////////////////////////////////////// 37 + // ⚡️ 38 + //////////////////////////////////////////// 39 + 40 + const ampElement = document.querySelector("dtw-webamp"); 41 + if (ampElement instanceof WebampElement === false) { 42 + throw new Error("Missing webamp element"); 43 + } 37 44 38 - /** */ 39 - handleLoadListEvent: async () => { 40 - // TODO 41 - return [ 42 - /* Array of Tracks */ 43 - ]; 44 - }, 45 + const amp = ampElement.amp; 45 46 46 - /** 47 - * @param {any} tracks 48 - */ 49 - handleSaveListEvent: (tracks) => { 50 - // TODO 51 - }, 52 - }); 47 + // TODO: Handle minimize 48 + amp.onMinimize(() => {}); 53 49 54 50 // Override track loader 55 51 const loadFromUrl = amp.media.loadFromUrl.bind(amp.media); ··· 65 61 } 66 62 67 63 amp.media.loadFromUrl = loadOverride.bind(amp.media); 68 - 69 - // TODO: Handle minimize 70 - amp.onMinimize(() => {}); 71 - 72 - // Render 73 - const ampNode = document.createElement("div"); 74 - ampNode.style = 75 - "height: 100vh; left: 0; position: absolute; top: 0; width: 100%; z-index: -1000;"; 76 - document.body.appendChild(ampNode); 77 - amp.renderWhenReady(ampNode); 78 - 79 - //////////////////////////////////////////// 80 - // 🌊 81 - //////////////////////////////////////////// 82 - 83 - const $currTrack = signal(/** @type {null | number} */ (null)); 84 - const $playlist = signal(/** @type {Item[]} */ ([])); 85 64 86 65 /** 87 66 * Observe changes in Webamp's internal store.
+1
src/theme/webamp/index.vto
··· 42 42 </dtw-window> 43 43 </dtw-window-manager> 44 44 </section> 45 + <dtw-webamp></dtw-webamp> 45 46 </main> 46 47 47 48 <!--
+62
src/theme/webamp/webamp.js
··· 1 + import Webamp from "webamp/lazy"; 2 + 3 + class WebampElement extends HTMLElement { 4 + constructor() { 5 + super(); 6 + 7 + // ⚡ 8 + 9 + /** @type {import("webamp/lazy").default} */ 10 + this.amp = new /** @type {any} */ (Webamp)({ 11 + enableMediaSession: true, 12 + initialTracks: [], 13 + zIndex: 99, 14 + 15 + /** */ 16 + handleLoadListEvent: async () => { 17 + // TODO 18 + return [ 19 + /* Array of Tracks */ 20 + ]; 21 + }, 22 + 23 + /** 24 + * @param {any} tracks 25 + */ 26 + handleSaveListEvent: (tracks) => { 27 + // TODO 28 + }, 29 + }); 30 + } 31 + 32 + connectedCallback() { 33 + this.attachShadow({ mode: "open" }); 34 + 35 + // Custom webamp rendering 36 + this.renderWebamp(); 37 + } 38 + 39 + async renderWebamp() { 40 + // Ideally this would render in the shadow root, 41 + // but sadly it does not. 42 + 43 + const ampNode = document.createElement("main"); 44 + ampNode.style = 45 + "height: 100vh; left: 0; position: absolute; top: 0; width: 100vw; z-index: -1000;"; 46 + 47 + this.shadowRoot?.appendChild(ampNode); 48 + 49 + return await this.amp.renderWhenReady(ampNode); 50 + } 51 + } 52 + 53 + export default WebampElement; 54 + 55 + //////////////////////////////////////////// 56 + // REGISTER 57 + //////////////////////////////////////////// 58 + 59 + export const CLASS = WebampElement; 60 + export const NAME = "dtw-webamp"; 61 + 62 + customElements.define(NAME, WebampElement);
+86 -9
src/theme/webamp/window-manager/element.js
··· 1 1 import { DiffuseElement } from "@common/element.js"; 2 2 import { signal } from "@common/signal.js"; 3 + import { debounceMicrotask } from "@vicary/debounce-microtask"; 3 4 4 5 /** 5 6 * @import {RenderArg} from "@common/element.d.ts" ··· 19 20 // SIGNALS 20 21 21 22 $activeWindow = signal(/** @type {string | null} */ (null)); 23 + #lastZindex = 1000; 22 24 23 25 // LIFECYCLE 24 26 ··· 28 30 connectedCallback() { 29 31 super.connectedCallback(); 30 32 31 - this.addEventListener("click", this.setActiveWindow); 33 + // Events 34 + this.addEventListener("mousedown", this.focusOnWindow); 35 + this.addEventListener("dtw-window-start-move", this.windowMoveStart); 36 + 37 + // Webamp stuff 38 + document.body.addEventListener( 39 + "mousedown", 40 + this.bringWebampToFront.bind(this), 41 + ); 32 42 43 + // React to active window changing 33 44 this.effect(() => { 34 45 const activeId = this.$activeWindow.value; 35 46 this.setWindowStatuses(activeId); ··· 41 52 */ 42 53 disconnectedCallback() { 43 54 super.disconnectedCallback(); 44 - this.removeEventListener("click", this.setActiveWindow); 55 + 56 + this.removeEventListener("mousedown", this.focusOnWindow); 57 + this.removeEventListener("dtw-window-start-move", this.windowMoveStart); 58 + 59 + document.body.removeEventListener( 60 + "mousedown", 61 + this.bringWebampToFront.bind(this), 62 + ); 63 + } 64 + 65 + /** 66 + * @param {MouseEvent} event 67 + */ 68 + bringWebampToFront(event) { 69 + if (event.target instanceof HTMLElement) { 70 + const webamp = event.target?.closest("#webamp"); 71 + if (webamp instanceof HTMLElement) { 72 + this.#lastZindex++; 73 + webamp.style.zIndex = this.#lastZindex.toString(); 74 + } 75 + } 76 + } 77 + 78 + /** 79 + * @param {Event} event 80 + */ 81 + focusOnWindow(event) { 82 + if (event.target instanceof HTMLElement) { 83 + const win = event.target?.closest("dtw-window"); 84 + if (win instanceof HTMLElement === false) return; 85 + if (win.id) this.$activeWindow.value = win.id; 86 + 87 + this.#lastZindex++; 88 + win.style.zIndex = this.#lastZindex.toString(); 89 + } 45 90 } 46 91 47 92 /** ··· 64 109 } 65 110 66 111 /** 67 - * @param {Event} event 112 + * @param {any} ogEvent 68 113 */ 69 - setActiveWindow(event) { 70 - if (event.target instanceof HTMLElement) { 71 - const window = event.target?.closest("dtw-window"); 72 - if (!window) return; 73 - if (window.id) this.$activeWindow.value = window.id; 74 - } 114 + windowMoveStart(ogEvent) { 115 + /** 116 + * @param {Event} event 117 + */ 118 + const moveFn = debounceMicrotask((event) => { 119 + if (event instanceof MouseEvent) { 120 + const x = event.x - ogEvent.detail.xElement; 121 + const y = event.y - ogEvent.detail.yElement; 122 + const target = ogEvent.target; 123 + 124 + if (target) { 125 + target.style.left = `${x}px`; 126 + target.style.top = `${y}px`; 127 + } 128 + } 129 + }, { 130 + updateArguments: true, 131 + }); 132 + 133 + const stopMove = () => { 134 + this.removeEventListener("mousemove", moveFn); 135 + this.removeEventListener("dtw-window-end-move", stopMove); 136 + 137 + document.removeEventListener("mouseup", stopMove); 138 + document.removeEventListener("mouseleave", stopMove); 139 + }; 140 + 141 + this.addEventListener("mousemove", moveFn); 142 + this.addEventListener("dtw-window-end-move", stopMove); 143 + 144 + document.addEventListener("mouseup", stopMove); 145 + document.addEventListener("mouseleave", stopMove); 75 146 } 76 147 77 148 // RENDER ··· 81 152 */ 82 153 render({ html }) { 83 154 return html` 155 + <style> 156 + :host { 157 + user-select: none; 158 + } 159 + </style> 160 + 84 161 <slot></slot> 85 162 `; 86 163 }
+48 -9
src/theme/webamp/window/element.js
··· 23 23 */ 24 24 connectedCallback() { 25 25 super.connectedCallback(); 26 - 27 - const x = Math.floor( 28 - Math.random() * (document.body.clientWidth - 300), 29 - ); 30 - 31 - this.style.position = "relative"; 32 - this.style.left = `${x}px`; 33 26 } 34 27 35 28 /** ··· 64 57 border: 0; 65 58 padding: 0; 66 59 } 60 + 61 + .title-bar { 62 + user-select: none; 63 + } 67 64 </style> 68 65 69 66 <dialog open> 70 67 <div class="window" style="width: 300px"> 71 - <div class="title-bar"> 72 - <div class="title-bar-text"> 68 + <div 69 + class="title-bar" 70 + @mousedown="${this.titleBarMouseDown}" 71 + @mouseup="${this.titleBarMouseUp}" 72 + > 73 + <div class="title-bar-text" draggable="false"> 73 74 <slot name="title"></slot> 74 75 </div> 75 76 <div class="title-bar-controls"> ··· 84 85 </div> 85 86 </dialog> 86 87 `; 88 + } 89 + 90 + // EVENTS 91 + 92 + /** 93 + * @param {MouseEvent} mouse 94 + */ 95 + titleBarMouseDown(mouse) { 96 + const event = new CustomEvent("dtw-window-start-move", { 97 + bubbles: true, 98 + composed: true, 99 + detail: { 100 + x: mouse.x, 101 + xElement: mouse.layerX, 102 + y: mouse.y, 103 + yElement: mouse.layerY, 104 + }, 105 + }); 106 + 107 + this.dispatchEvent(event); 108 + } 109 + 110 + /** 111 + * @param {MouseEvent} mouse 112 + */ 113 + titleBarMouseUp(mouse) { 114 + const event = new CustomEvent("dtw-window-end-move", { 115 + bubbles: true, 116 + composed: true, 117 + detail: { 118 + x: mouse.x, 119 + xElement: mouse.layerX, 120 + y: mouse.y, 121 + yElement: mouse.layerY, 122 + }, 123 + }); 124 + 125 + this.dispatchEvent(event); 87 126 } 88 127 } 89 128