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

feat: output configurator

+203 -129
+7 -2
src/common/element.js
··· 61 61 } 62 62 63 63 /** */ 64 - nameWithGroup() { 64 + get label() { 65 + return this.getAttribute("label") ?? this.id ?? this.localName; 66 + } 67 + 68 + /** */ 69 + get nameWithGroup() { 65 70 return `${this.constructor.prototype.constructor.NAME}/${this.group}`; 66 71 } 67 72 ··· 131 136 ); 132 137 133 138 // Setup worker 134 - const name = this.nameWithGroup(); 139 + const name = this.nameWithGroup; 135 140 const url = import.meta.resolve("./" + WORKER_URL) + `?${query}`; 136 141 137 142 let worker;
+19 -2
src/components/configurator/output/element.js
··· 118 118 119 119 // ADDITIONAL ACTIONS 120 120 121 - async deselectOutput() { 121 + async deselect() { 122 122 localStorage.removeItem(`${STORAGE_PREFIX}/selected/id`); 123 123 this.#selectedOutput.value = await this.#findSelectedOutput(); 124 124 } 125 125 126 + async options() { 127 + const deps = this.dependencies(); 128 + const entries = Object.entries(deps); 129 + 130 + await Promise.all( 131 + entries.map(([_k, v]) => customElements.whenDefined(v.localName)), 132 + ); 133 + 134 + return entries.map(([k, v]) => { 135 + return { 136 + id: k, 137 + label: v.label, 138 + element: v, 139 + }; 140 + }); 141 + } 142 + 126 143 /** 127 144 * @param {string} id 128 145 */ 129 - async selectOutput(id) { 146 + async select(id) { 130 147 localStorage.setItem(`${STORAGE_PREFIX}/selected/id`, id); 131 148 this.#selectedOutput.value = await this.#findSelectedOutput(); 132 149 }
+2 -2
src/components/engine/audio/element.js
··· 51 51 // Setup broadcasting if part of group 52 52 if (this.hasAttribute("group")) { 53 53 const actions = this.broadcast( 54 - this.nameWithGroup(), 54 + this.nameWithGroup, 55 55 { 56 56 adjustVolume: { strategy: "replicate", fn: this.adjustVolume }, 57 57 pause: { strategy: "leaderOnly", fn: this.pause }, ··· 405 405 // Setup broadcasting if part of group 406 406 if (this.hasAttribute("group")) { 407 407 const actions = this.broadcast( 408 - this.nameWithGroup(), 408 + this.nameWithGroup, 409 409 { 410 410 getDuration: { strategy: "leaderOnly", fn: this.$state.duration.get }, 411 411 getHasEnded: { strategy: "leaderOnly", fn: this.$state.hasEnded.get },
+1 -1
src/components/orchestrator/queue-tracks/element.js
··· 45 45 async connectedCallback() { 46 46 // Broadcast if needed 47 47 if (this.hasAttribute("group")) { 48 - this.broadcast(this.nameWithGroup(), {}); 48 + this.broadcast(this.nameWithGroup, {}); 49 49 } 50 50 51 51 // Super
+4 -2
src/components/output/polymorphic/indexed-db/element.js
··· 38 38 39 39 /** @param {string} name */ 40 40 #cat(name) { 41 - const key = this.hasAttribute("key") ? this.getAttribute("key") + "/" : ""; 42 - return `${key}${name}`; 41 + const namespace = this.hasAttribute("namespace") 42 + ? this.getAttribute("namespace") + "/" 43 + : ""; 44 + return `${namespace}${name}`; 43 45 } 44 46 } 45 47
+2 -3
src/index.vto
··· 23 23 configurators: 24 24 - url: "components/configurator/input/element.js" 25 25 title: "Input" 26 - desc: "Add multiple inputs." 26 + desc: "Allows for multiple inputs to be used at once." 27 27 - url: "components/configurator/output/element.js" 28 28 title: "Output" 29 - desc: "Allows the user to configure a specific output." 30 - todo: true 29 + desc: "Enables the user to configure a specific output. If no default output is set, it creates a temporary session by storing everything in memory." 31 30 - url: "components/configurator/scrobbles/element.js" 32 31 title: "Scrobbles" 33 32 desc: "Configure multiple scrobblers (music trackers)."
+4 -1
src/themes/webamp/browser/element.js
··· 98 98 ***********************************/ 99 99 100 100 .sunken-panel { 101 - content-visibility: auto; 102 101 height: 30dvh; 103 102 min-height: 80px; 104 103 resize: both; ··· 116 115 &:first-child { 117 116 width: 40%; 118 117 } 118 + } 119 + 120 + table tbody tr { 121 + content-visibility: auto; 119 122 } 120 123 121 124 table td {
-61
src/themes/webamp/index.css
··· 82 82 } 83 83 } 84 84 } 85 - 86 - /*********************************** 87 - * Windows 88 - ***********************************/ 89 - 90 - .windows dtw-window { 91 - left: 12px; 92 - position: absolute; 93 - top: 12px; 94 - z-index: 999; 95 - 96 - /* Waiting on https://developer.mozilla.org/en-US/docs/Web/CSS/sibling-index#browser_compatibility */ 97 - &:nth-child(1) { 98 - left: 24px; 99 - top: 24px; 100 - } 101 - 102 - &:nth-child(2) { 103 - left: 36px; 104 - top: 36px; 105 - } 106 - 107 - &:nth-child(3) { 108 - left: 48px; 109 - top: 48px; 110 - } 111 - 112 - &:nth-child(4) { 113 - left: 60px; 114 - top: 60px; 115 - } 116 - 117 - &:nth-child(5) { 118 - left: 72px; 119 - top: 72px; 120 - } 121 - 122 - &:nth-child(6) { 123 - left: 84px; 124 - top: 84px; 125 - } 126 - 127 - &:nth-child(7) { 128 - left: 96px; 129 - top: 96px; 130 - } 131 - 132 - &:nth-child(8) { 133 - left: 108px; 134 - top: 108px; 135 - } 136 - 137 - &:nth-child(9) { 138 - left: 120px; 139 - top: 120px; 140 - } 141 - } 142 - 143 - .windows section { 144 - z-index: 999; 145 - }
+13 -5
src/themes/webamp/index.js
··· 1 1 import "@components/configurator/output/element.js"; 2 2 import "@components/input/opensubsonic/element.js"; 3 3 import "@components/input/s3/element.js"; 4 - import "@components/orchestrator/process-tracks/element.js"; 4 + // import "@components/orchestrator/process-tracks/element.js"; 5 5 import "@components/orchestrator/queue-tracks/element.js"; 6 6 import "@components/output/polymorphic/indexed-db/element.js"; 7 7 import "@components/processor/metadata/element.js"; ··· 16 16 17 17 import "./browser/element.js"; 18 18 import "./window/element.js"; 19 - import "./window-manager/element.js"; 19 + import WindowManager from "./window-manager/element.js"; 20 20 import WebampElement from "./webamp/element.js"; 21 21 22 22 const input = component(Input); ··· 157 157 if (element instanceof HTMLElement) { 158 158 element.addEventListener("dblclick", () => { 159 159 const f = element.querySelector("label")?.getAttribute("for"); 160 - if (f) { 161 - document.body.querySelector(`dtw-window#${f}`)?.toggleAttribute("open"); 162 - } 160 + if (f) windowManager()?.toggleWindow(f); 163 161 }); 164 162 } 165 163 }); ··· 182 180 183 181 // TODO: 184 182 // amp.onMinimize(() => amp.close()); 183 + 184 + //////////////////////////////////////////// 185 + // 🛠️ 186 + //////////////////////////////////////////// 187 + 188 + function windowManager() { 189 + const w = document.body.querySelector("dtw-window-manager"); 190 + if (w instanceof WindowManager) return w; 191 + return null; 192 + }
+12 -28
src/themes/webamp/index.vto
··· 10 10 <body> 11 11 <!-- 12 12 13 - UI 13 + ################################### 14 + # UI 15 + ################################### 14 16 15 17 --> 16 18 <main> 19 + <!-- 🪟 --> 17 20 <section class="windows"> 18 - <dtw-window-manager> 19 - <!-- INPUT --> 20 - <dtw-window id="input-window"> 21 - <span slot="title-icon"><img src="../../images/icons/windows_98/cd_audio_cd_a-0.png" height="14" /></span> 22 - <span slot="title">Manage audio inputs</span> 23 - <p>👀</p> 24 - </dtw-window> 21 + <dtw-window-manager></dtw-window-manager> 22 + </section> 25 23 26 - <!-- OUTPUT --> 27 - <dtw-window id="output-window"> 28 - <span slot="title-icon"><img src="../../images/icons/windows_98/computer_user_pencil-0.png" height="14" /></span> 29 - <span slot="title">Manage user data</span> 30 - <p>👀</p> 31 - </dtw-window> 32 24 33 - <!-- BROWSER --> 34 - <dtw-window id="browser-window" open> 35 - <span slot="title-icon"><img src="../../images/icons/windows_98/directory_explorer-4.png" height="14" /></span> 36 - <span slot="title">Browse collection</span> 37 - <dtw-browser 38 - input-selector="#input" 39 - output-selector="#output" 40 - queue-engine-selector="de-queue" 41 - ></dtw-browser> 42 - </dtw-window> 43 - </dtw-window-manager> 44 - </section> 25 + <!-- 🛋️ --> 45 26 <section class="desktop"> 46 27 <!-- WINAMP --> 47 28 <a class="button desktop__item" id="desktop-winamp"> ··· 67 48 <label for="browser-window">Browse collection</label> 68 49 </a> 69 50 </section> 51 + <!-- ⚡️ --> 70 52 <dtw-webamp></dtw-webamp> 71 53 </main> 72 54 73 55 <!-- 74 56 75 - COMPONENTS 57 + ################################### 58 + # COMPONENTS 59 + ################################### 76 60 77 61 --> 78 62 <de-queue></de-queue> ··· 87 71 </dc-input> 88 72 89 73 <!-- Output --> 90 - <dop-indexed-db id="idb-json-output" key="json"></dop-indexed-db> 74 + <dop-indexed-db id="idb-json-output" namespace="json"></dop-indexed-db> 91 75 92 76 <dc-output default="idb-json"> 93 77 <dtos-json id="idb-json" output-selector="#idb-json-output"></dtos-json>
+137 -21
src/themes/webamp/window-manager/element.js
··· 2 2 import { signal } from "@common/signal.js"; 3 3 import { debounceMicrotask } from "@vicary/debounce-microtask"; 4 4 5 + import WindowElement from "../window/element.js" 6 + 5 7 /** 6 8 * @import {RenderArg} from "@common/element.d.ts" 7 - * @import WindowElement from "../window/element.js"; 8 9 */ 9 10 10 11 //////////////////////////////////////////// ··· 15 16 constructor() { 16 17 super(); 17 18 this.attachShadow({ mode: "open" }); 19 + 20 + this.focusOnWindow = this.focusOnWindow.bind(this) 21 + this.windowMoveStart = this.windowMoveStart.bind(this) 18 22 } 19 23 20 24 // SIGNALS ··· 27 31 /** 28 32 * @override 29 33 */ 30 - connectedCallback() { 34 + async connectedCallback() { 31 35 super.connectedCallback(); 32 36 33 37 // Events ··· 85 89 if (win.id) this.$activeWindow.value = win.id; 86 90 87 91 this.#lastZindex++; 88 - win.style.zIndex = this.#lastZindex.toString(); 92 + this.setWindowZindex(win.id, this.#lastZindex) 89 93 } 90 94 } 91 95 ··· 94 98 */ 95 99 async setWindowStatuses(activeId) { 96 100 await customElements.whenDefined("dtw-window"); 97 - 98 - this.querySelectorAll("dtw-window").forEach( 99 - (window) => { 100 - const win = /** @type {WindowElement} */ (window); 101 - 102 - if (activeId && window.id === activeId) { 103 - win.activate(); 104 - } else { 105 - win.deactivate(); 106 - } 107 - }, 108 - ); 101 + this.activateWindow(activeId) 109 102 } 110 103 111 104 /** ··· 119 112 if (event instanceof MouseEvent) { 120 113 const x = event.x - ogEvent.detail.xElement; 121 114 const y = event.y - ogEvent.detail.yElement; 122 - const target = ogEvent.target; 115 + const target = ogEvent.detail.element; 123 116 124 117 if (target) { 125 118 target.style.left = `${x}px`; ··· 131 124 }); 132 125 133 126 const stopMove = () => { 134 - this.removeEventListener("mousemove", moveFn); 135 - 127 + document.removeEventListener("mousemove", moveFn); 136 128 document.removeEventListener("mouseup", stopMove); 137 129 document.removeEventListener("mouseleave", stopMove); 138 130 }; 139 131 140 - this.addEventListener("mousemove", moveFn); 141 - 132 + document.addEventListener("mousemove", moveFn); 142 133 document.addEventListener("mouseup", stopMove); 143 134 document.addEventListener("mouseleave", stopMove); 144 135 } 145 136 137 + // ACTIONS 138 + 139 + /** 140 + * @param {string} id 141 + */ 142 + activateWindow(id) { 143 + this.querySelectorAll("dtw-window").forEach(w => { 144 + if (w instanceof WindowElement === false) return 145 + 146 + if (activeId && w.id === activeId) { 147 + w.activate(); 148 + } else { 149 + w.deactivate(); 150 + } 151 + }) 152 + } 153 + 154 + /** 155 + * @param {string} id 156 + * @param {number} index 157 + */ 158 + setWindowZindex(id, index) { 159 + const w = this.root().querySelector(`dtw-window#${id}`) 160 + w.style.zIndex = index.toString(); 161 + } 162 + 163 + /** 164 + * @param {string} id 165 + */ 166 + toggleWindow(id) { 167 + const w = this.root().querySelector(`dtw-window#${id}`) 168 + if (w instanceof WindowElement === false) return 169 + 170 + w.toggleAttribute("open") 171 + 172 + if (w.hasAttribute("open")) { 173 + this.activateWindow(id) 174 + } 175 + } 176 + 146 177 // RENDER 147 178 148 179 /** ··· 150 181 */ 151 182 render({ html }) { 152 183 return html` 184 + <link rel="stylesheet" href="../../styles/vendor/98.css" /> 185 + 153 186 <style> 154 187 :host { 155 188 user-select: none; 156 189 } 190 + 191 + dtw-window { 192 + left: 12px; 193 + position: absolute; 194 + top: 12px; 195 + z-index: 999; 196 + 197 + /* Waiting on https://developer.mozilla.org/en-US/docs/Web/CSS/sibling-index#browser_compatibility */ 198 + &:nth-child(1) { 199 + left: 24px; 200 + top: 24px; 201 + } 202 + 203 + &:nth-child(2) { 204 + left: 36px; 205 + top: 36px; 206 + } 207 + 208 + &:nth-child(3) { 209 + left: 48px; 210 + top: 48px; 211 + } 212 + 213 + &:nth-child(4) { 214 + left: 60px; 215 + top: 60px; 216 + } 217 + 218 + &:nth-child(5) { 219 + left: 72px; 220 + top: 72px; 221 + } 222 + 223 + &:nth-child(6) { 224 + left: 84px; 225 + top: 84px; 226 + } 227 + 228 + &:nth-child(7) { 229 + left: 96px; 230 + top: 96px; 231 + } 232 + 233 + &:nth-child(8) { 234 + left: 108px; 235 + top: 108px; 236 + } 237 + 238 + &:nth-child(9) { 239 + left: 120px; 240 + top: 120px; 241 + } 242 + } 157 243 </style> 158 244 159 - <slot></slot> 245 + <!-- INPUT --> 246 + <dtw-window id="input-window"> 247 + <span slot="title-icon"><img src="../../images/icons/windows_98/cd_audio_cd_a-0.png" height="14" /></span> 248 + <span slot="title">Manage audio inputs</span> 249 + <p>👀</p> 250 + </dtw-window> 251 + 252 + <!-- OUTPUT --> 253 + <dtw-window id="output-window"> 254 + <span slot="title-icon"><img src="../../images/icons/windows_98/computer_user_pencil-0.png" height="14" /></span> 255 + <span slot="title">Manage user data</span> 256 + 257 + <form> 258 + <p>Where do you want to keep your data?</p> 259 + <div class="field-row"> 260 + <input id="idb-json" type="radio" checked /> 261 + <label for="idb-json">Local only</label> 262 + </div> 263 + </form> 264 + </dtw-window> 265 + 266 + <!-- BROWSER --> 267 + <dtw-window id="browser-window" open> 268 + <span slot="title-icon"><img src="../../images/icons/windows_98/directory_explorer-4.png" height="14" /></span> 269 + <span slot="title">Browse collection</span> 270 + <dtw-browser 271 + input-selector="#input" 272 + output-selector="#output" 273 + queue-engine-selector="de-queue" 274 + ></dtw-browser> 275 + </dtw-window> 160 276 `; 161 277 } 162 278 }
+2 -1
src/themes/webamp/window/element.js
··· 66 66 <div class="window"> 67 67 <div 68 68 class="title-bar" 69 - @mousedown="${this.titleBarMouseDown}" 69 + @mousedown="${this.titleBarMouseDown.bind(this)}" 70 70 > 71 71 <div class="title-bar-icon"> 72 72 <slot name="title-icon"></slot> ··· 99 99 bubbles: true, 100 100 composed: true, 101 101 detail: { 102 + element: this, 102 103 x: mouse.x, 103 104 xElement: mouse.layerX, 104 105 y: mouse.y,