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

Configure Feed

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

feat: artwork controller volume + fade in artwork

+126 -103
+3
_config.ts
··· 11 11 12 12 const site = lume({ 13 13 src: "./src", 14 + server: { 15 + debugBar: false, 16 + }, 14 17 }); 15 18 16 19 export default site;
+123 -103
src/themes/blur/artwork-controller/element.js
··· 3 3 import { xxh32r } from "xxh32/dist/raw.js"; 4 4 import { debounce } from "throttle-debounce"; 5 5 6 - import { DiffuseElement, query, whenElementsDefined } from "@common/element.js"; 6 + import { 7 + DiffuseElement, 8 + keyed, 9 + query, 10 + whenElementsDefined, 11 + } from "@common/element.js"; 7 12 import { trackArtworkCacheId } from "@common/index.js"; 8 13 import { computed, signal } from "@common/signal.js"; 9 14 10 15 /** 11 16 * @import {RenderArg} from "@common/element.d.ts" 12 - * @import {Signal} from "@common/signal.d.ts" 13 17 * @import {Track} from "@definitions/types.d.ts" 14 18 * 15 19 * @import {InputElement} from "@components/input/types.d.ts" ··· 22 26 class ArtworkController extends DiffuseElement { 23 27 constructor() { 24 28 super(); 29 + this.attachShadow({ mode: "open" }); 25 30 26 31 // Bind event handlers to self 32 + this.artworkLoaded = this.artworkLoaded.bind(this); 33 + this.fullVolume = this.fullVolume.bind(this); 34 + this.mute = this.mute.bind(this); 27 35 this.next = this.next.bind(this); 28 36 this.playPause = this.playPause.bind(this); 29 37 this.previous = this.previous.bind(this); 30 38 this.seek = this.seek.bind(this); 39 + this.setVolume = this.setVolume.bind(this); 31 40 } 32 41 33 42 // VARIABLES ··· 95 104 this.#setArtwork.bind(this), 96 105 ); 97 106 98 - // this.effect(() => { 99 - // debouncedChangeArtwork(queue.now()); 100 - // }); 107 + this.effect(() => { 108 + debouncedChangeArtwork(queue.now()); 109 + }); 101 110 102 - // this.effect(() => this.#changeArtworkInDOM()); 103 111 this.effect(() => this.#formatTimestamps()); 104 112 this.effect(() => this.#lightOrDark()); 105 113 ··· 129 137 // 🖼️ Artwork 130 138 //////////////////////////////////////////// 131 139 132 - /** @type {Record<string, ReturnType<typeof setTimeout>>} */ 133 - #timeouts = {}; 134 - 135 - #changeArtworkInDOM() { 136 - const art = this.#artwork.value; 137 - 138 - // No artwork, fade out existing. 139 - if (art.length === 0) { 140 - this.root().querySelectorAll(".artwork img").forEach((el) => { 141 - const element = /** @type {HTMLElement} */ (el); 142 - element.style.opacity = "0"; 143 - const hash = element.getAttribute("data-hash"); 144 - if (hash) { 145 - this.#timeouts[hash] = setTimeout(() => element.remove(), 1000); 146 - } 147 - }); 148 - return; 149 - } 150 - 151 - // Determine if the current artwork needs to be replaced. 152 - const hash = xxh32r(art[0].bytes).toString(); 153 - 154 - /** @type {HTMLImageElement | null} */ 155 - const existingArtwork = this.root().querySelector( 156 - `.artwork img[data-hash="${hash}"]`, 157 - ); 158 - 159 - // If the artwork is the same, stop here. 160 - if (existingArtwork) { 161 - const timeoutId = this.#timeouts[hash]; 162 - if (timeoutId) clearTimeout(timeoutId); 163 - existingArtwork.style.opacity = "1"; 164 - return; 165 - } 166 - 167 - // Add new artwork 168 - const blob = new Blob( 169 - [/** @type {ArrayBuffer} */ (art[0].bytes.buffer)], 170 - { type: art[0].mime }, 171 - ); 172 - const url = URL.createObjectURL(blob); 173 - 174 - /** @type {HTMLImageElement} */ 175 - const img = document.createElement("img"); 176 - img.setAttribute("data-hash", hash); 177 - img.src = url; 178 - 179 - // Extract average color 180 - img.onload = () => { 181 - const fac = new FastAverageColor(); 182 - const color = fac.getColor(img); 183 - const rgb = color.value; 184 - const o = Math.round( 185 - (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000, 186 - ); 187 - 188 - this.#artworkColor.value = color.rgba; 189 - this.#artworkLightMode.value = o > 165; 190 - 191 - /** @type {HTMLElement | null} */ 192 - const bg = this.root().querySelector(".controller__background"); 193 - if (bg) bg.style.backgroundColor = color.rgba; 194 - 195 - /** @type {HTMLElement | null} */ 196 - const main = this.root().querySelector("main"); 197 - if (main) main.style.backgroundColor = color.rgba; 198 - 199 - img.style.opacity = "1"; 200 - 201 - this.root().querySelectorAll(".artwork img").forEach((el) => { 202 - if (el === img) return; 203 - 204 - const element = /** @type {HTMLElement} */ (el); 205 - element.style.opacity = "0"; 206 - this.#timeouts[hash] = setTimeout(() => element.remove(), 1000); 207 - }); 208 - }; 209 - 210 - // Insert new artwork 211 - this.root().querySelector(".artwork")?.appendChild(img); 212 - } 213 - 214 140 #lightOrDark() { 215 141 const controller = this.root().querySelector(".controller__inner"); 216 142 if (!controller) return; ··· 259 185 }; 260 186 261 187 const art = await this.$artwork.value?.artwork(request) ?? []; 262 - 263 - console.log("ART", art); 264 - 265 188 const currCacheId = track ? await trackArtworkCacheId(track) : undefined; 266 189 if (cacheId === currCacheId) this.#artwork.set(art); 267 190 } ··· 315 238 316 239 // EVENTS 317 240 241 + /** 242 + * @param {Event} event 243 + */ 244 + artworkLoaded(event) { 245 + if (!(event.target instanceof HTMLImageElement)) return; 246 + 247 + const fac = new FastAverageColor(); 248 + const color = fac.getColor(event.target); 249 + const rgb = color.value; 250 + const o = Math.round( 251 + (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000, 252 + ); 253 + 254 + this.#artworkColor.value = color.rgba; 255 + this.#artworkLightMode.value = o > 165; 256 + 257 + event.target.style.opacity = "1"; 258 + } 259 + 260 + fullVolume() { 261 + this.$audio.value?.adjustVolume({ volume: 1 }); 262 + } 263 + 264 + mute() { 265 + this.$audio.value?.adjustVolume({ volume: 0 }); 266 + } 267 + 268 + next() { 269 + this.$queue.value?.shift(); 270 + } 271 + 318 272 playPause() { 319 273 const audioId = this.$queue.value?.now()?.id; 320 274 ··· 329 283 this.$queue.value?.unshift(); 330 284 } 331 285 332 - next() { 333 - this.$queue.value?.shift(); 334 - } 335 - 336 286 /** 337 287 * @param {MouseEvent} event 338 288 */ ··· 346 296 if (audioId) this.$audio.value?.seek({ audioId, percentage }); 347 297 } 348 298 299 + /** 300 + * @param {MouseEvent} event 301 + */ 302 + setVolume(event) { 303 + const target = event.target 304 + ? /** @type {HTMLProgressElement} */ (event.target) 305 + : null; 306 + 307 + const percentage = target ? event.offsetX / target.clientWidth : 0; 308 + this.$audio.value?.adjustVolume({ volume: percentage }); 309 + } 310 + 349 311 // RENDER 350 312 351 313 /** 352 314 * @param {RenderArg} _ 353 315 */ 354 316 render({ html }) { 317 + const activeQueueItem = this.$queue.value?.now(); 318 + 319 + // Artwork 320 + const artwork = this.#artwork.value.map((art) => { 321 + const hash = xxh32r(art.bytes).toString(); 322 + const blob = new Blob( 323 + [/** @type {ArrayBuffer} */ (art.bytes.buffer)], 324 + { type: art.mime }, 325 + ); 326 + 327 + const url = URL.createObjectURL(blob); 328 + 329 + return keyed( 330 + hash, 331 + html` 332 + <img @load="${this.artworkLoaded}" data-hash="${hash}" src="${url}" /> 333 + `, 334 + ); 335 + }); 336 + 355 337 return html` 356 338 <style> 339 + @import "../../../styles/vendor/phosphor/fill/style.css"; 357 340 @import "./element.css"; 358 341 </style> 359 342 360 - <main> 361 - <section class="artwork"></section> 343 + <main style="background-color: ${this.#artworkColor.value ?? 344 + `revert-layer`};"> 345 + <section class="artwork"> 346 + <label style="background: ${this.#artworkColor.value ?? 347 + `revert-layer`}; display: ${`block`};"> 348 + ${this.group} 349 + </label> 350 + 351 + ${artwork} 352 + </section> 362 353 363 354 <section class="controller"> 364 355 <div class="gradient-blur"> ··· 372 363 <div></div> 373 364 </div> 374 365 375 - <div class="controller__background"></div> 366 + <div 367 + class="controller__background" 368 + style="background-color: ${this.#artworkColor.value ?? 369 + `revert-layer`};" 370 + > 371 + </div> 376 372 377 373 <section class="controller__inner"> 378 - <!-- Now playing --> 374 + <!-- NOW PLAYING --> 375 + 379 376 <cite> 380 - <strong>${this.$queue.value?.now()?.tags?.title || 377 + <strong>${activeQueueItem?.tags?.title || 381 378 "Diffuse"}</strong> 382 379 <br /> 383 - <span style="font-style: italic"></span> 380 + <span style="font-style: ${activeQueueItem 381 + ? `normal` 382 + : `italic`}"> 383 + ${activeQueueItem?.tags?.artist ?? 384 + (activeQueueItem ? `Waiting on queue ...` : ``)} 385 + </span> 384 386 </cite> 385 387 386 - <!-- Progress --> 388 + <!-- PROGRESS --> 389 + 387 390 <div class="progress" @click="${this.seek}"> 388 391 <progress max="100" value="${(this.#audio()?.progress() ?? 389 392 0) * 100}"></progress> ··· 394 397 </div> 395 398 </div> 396 399 397 - <!-- Controls --> 400 + <!-- CONTROLS --> 401 + 398 402 <menu> 399 403 <!-- previous --> 400 404 <li @click="${this.previous}"> 401 405 <i class="ph-fill ph-rewind" title="Previous track"></i> 402 406 </li> 407 + 403 408 <!-- loading ... --> 404 409 <div 405 410 class="animate-bounce menu__loader" ··· 407 412 > 408 413 <i class="ph-fill ph-vinyl-record" title="Loading ..."></i> 409 414 </div> 415 + 410 416 <!-- play --> 411 417 <li 412 418 @click="${this.playPause}" ··· 417 423 > 418 424 <i class="ph-fill ph-play" title="Play"></i> 419 425 </li> 426 + 420 427 <!-- pause --> 421 428 <li 422 429 @click="${this.playPause}" ··· 426 433 > 427 434 <i class="ph-fill ph-pause" title="Pause"></i> 428 435 </li> 436 + 429 437 <!-- next --> 430 438 <li @click="${this.next}"> 431 439 <i class="ph-fill ph-fast-forward" title="Next track"></i> 432 440 </li> 433 441 </menu> 442 + 443 + <!-- VOLUME --> 444 + 445 + <footer> 446 + <i @click="${this.mute}" class="ph-fill ph-speaker-none"></i> 447 + <div @click="${this.setVolume}" class="progress-bar"> 448 + <progress max="100" value="${(this.$audio.value?.volume() ?? 449 + 0) * 100}"></progress> 450 + </div> 451 + <i @click="${this 452 + .fullVolume}" class="ph-fill ph-speaker-high"></i> 453 + </footer> 434 454 </section> 435 455 </section> 436 456 </main>