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: finish up new winamp theme

+289 -27
+9 -2
src/themes/winamp/facet/index.inline.js
··· 2 2 import { effect } from "~/common/signal.js"; 3 3 4 4 import WindowManager from "~/themes/winamp/window-manager/element.js"; 5 - import WinampElement from "~/themes/winamp/winamp/element.js"; 6 5 7 6 // Set doc title 8 7 foundation.setup({ title: "Winamp | Diffuse" }); ··· 25 24 await foundation.orchestrator.queueAudio(); 26 25 27 26 await import("~/themes/winamp/browser/element.js"); 28 - await import("~/themes/winamp/winamp/element.js"); 29 27 await import("~/themes/winamp/window/element.js"); 28 + 29 + const { default: WinampElement } = await import( 30 + "~/themes/winamp/winamp/element.js" 31 + ); 30 32 31 33 /** @type {OutputElement | null} */ 32 34 const output = document.querySelector("#output"); ··· 43 45 document.body.querySelectorAll(".desktop__item").forEach((element) => { 44 46 if (element instanceof HTMLElement) { 45 47 element.addEventListener("dblclick", () => { 48 + if (element.id === "desktop-winamp") { 49 + const w = document.body.querySelector("dtw-winamp"); 50 + if (w instanceof WinampElement) w.open(); 51 + return; 52 + } 46 53 const f = element.querySelector("label")?.getAttribute("for"); 47 54 if (f) return windowManager()?.toggleWindow(f); 48 55 });
+280 -25
src/themes/winamp/winamp/element.js
··· 20 20 21 21 const UI_STATE_KEY = "themes/winamp/winamp/ui"; 22 22 23 - /** @returns {{ eqOpen: boolean, playlistOpen: boolean, eqOn: boolean, eqSliders: Record<string, number> | null, mainShade: boolean, eqShade: boolean, playlistShade: boolean }} */ 23 + /** @returns {{ eqOpen: boolean, playlistOpen: boolean, milkdropOpen: boolean, eqOn: boolean, eqSliders: Record<string, number> | null, mainShade: boolean, eqShade: boolean, playlistShade: boolean, positions: Record<string, {x:number,y:number}> | null, sizes: Record<string, {width:number,height:number}> | null }} */ 24 24 function loadUiState() { 25 25 try { 26 26 return { 27 27 eqOpen: true, 28 28 playlistOpen: true, 29 + milkdropOpen: true, 29 30 eqOn: false, 30 31 eqSliders: null, 31 32 mainShade: false, 32 33 eqShade: false, 33 34 playlistShade: false, 35 + positions: null, 36 + sizes: null, 34 37 ...JSON.parse(localStorage.getItem(UI_STATE_KEY) ?? "{}"), 35 38 }; 36 39 } catch { 37 - return { eqOpen: true, playlistOpen: true, eqOn: false, eqSliders: null, mainShade: false, eqShade: false, playlistShade: false }; 40 + return { eqOpen: true, playlistOpen: true, milkdropOpen: true, eqOn: false, eqSliders: null, mainShade: false, eqShade: false, playlistShade: false, positions: null, sizes: null }; 38 41 } 39 42 } 40 43 ··· 203 206 #balance = signal(0); 204 207 #stopped = signal(false); 205 208 #seekingProgress = signal(/** @type {number | null} */ (null)); 206 - #focusedWindow = signal(/** @type {"main" | "eq" | "playlist"} */ ("main")); 209 + #focusedWindow = signal(/** @type {"main" | "eq" | "playlist" | "milkdrop"} */ ("main")); 207 210 #playlistOpen = signal(true); 211 + #milkdropOpen = signal(true); 208 212 209 213 // Window positions — plain objects (not signals) so dragging doesn't 210 214 // trigger re-renders, but the current value is always read on each render. ··· 212 216 #eqPos = { x: 0, y: 116 }; 213 217 #playlistPos = { x: 0, y: 232 }; 214 218 #playlistSize = { width: 275, height: 232 }; 219 + #milkdropPos = { x: 275, y: 0 }; 220 + #milkdropSize = { width: 275, height: 232 }; 221 + 222 + // Butterchurn 223 + /** @type {any} */ 224 + #butterchurn = null; 225 + /** @type {number | undefined} */ 226 + #butterchurnRAF = undefined; 227 + /** @type {ReturnType<typeof setInterval> | undefined} */ 228 + #butterchurnCycleInterval = undefined; 229 + /** @type {Array<any>} */ 230 + #butterchurnPresetList = []; 215 231 216 232 // SIGNALS - DEPENDENCIES 217 233 ··· 307 323 this.#eqShade.value = ui.eqShade; 308 324 this.#playlistShade.value = ui.playlistShade; 309 325 this.#playlistOpen.value = ui.playlistOpen; 326 + this.#milkdropOpen.value = ui.milkdropOpen; 310 327 if (ui.eqSliders) { 311 328 const s = ui.eqSliders; 312 329 this.#eqSliders.value = { ··· 315 332 }; 316 333 } 317 334 318 - // Center the windows on startup 319 - const totalH = 116 + 116 + this.#playlistSize.height; 320 - const cx = Math.round((window.innerWidth - 275) / 2); 321 - const cy = Math.round((window.innerHeight - totalH) / 2); 322 - this.#mainPos.x = cx; 323 - this.#mainPos.y = cy; 324 - this.#eqPos.x = cx; 325 - this.#eqPos.y = cy + 116; 326 - this.#playlistPos.x = cx; 327 - this.#playlistPos.y = cy + 232; 335 + if (ui.sizes) { 336 + if (ui.sizes.playlist) Object.assign(this.#playlistSize, ui.sizes.playlist); 337 + if (ui.sizes.milkdrop) Object.assign(this.#milkdropSize, ui.sizes.milkdrop); 338 + } 339 + 340 + if (ui.positions) { 341 + if (ui.positions.main) Object.assign(this.#mainPos, ui.positions.main); 342 + if (ui.positions.eq) Object.assign(this.#eqPos, ui.positions.eq); 343 + if (ui.positions.playlist) Object.assign(this.#playlistPos, ui.positions.playlist); 344 + if (ui.positions.milkdrop) Object.assign(this.#milkdropPos, ui.positions.milkdrop); 345 + } else { 346 + // Center the windows on startup 347 + const leftColH = 116 + 116 + this.#playlistSize.height; 348 + const milkdropOpen = this.#milkdropOpen.value; 349 + const totalW = milkdropOpen ? 275 + this.#milkdropSize.width : 275; 350 + const totalH = milkdropOpen ? Math.max(leftColH, this.#milkdropSize.height) : leftColH; 351 + const cx = Math.round((window.innerWidth - totalW) / 2); 352 + const cy = Math.round((window.innerHeight - totalH) / 2); 353 + this.#mainPos.x = cx; 354 + this.#mainPos.y = cy; 355 + this.#eqPos.x = cx; 356 + this.#eqPos.y = cy + 116; 357 + this.#playlistPos.x = cx; 358 + this.#playlistPos.y = cy + 232; 359 + this.#milkdropPos.x = cx + 275; 360 + this.#milkdropPos.y = cy; 361 + } 362 + 363 + this.effect(() => { 364 + if (!this.#milkdropOpen.value) { 365 + untracked(() => this.#stopButterchurn()); 366 + return; 367 + } 368 + untracked(() => { 369 + requestAnimationFrame(() => { 370 + const canvas = this.root().querySelector("#milkdrop-canvas"); 371 + if (!(canvas instanceof HTMLCanvasElement)) return; 372 + if (!this.#butterchurn) { 373 + this.#initButterchurn(canvas); 374 + } else { 375 + this.#startButterchurn(); 376 + } 377 + }); 378 + }); 379 + }); 328 380 329 381 this.forceRender(); 330 382 this.#marqueeScroller = this.root().querySelector("#marquee > div"); ··· 397 449 this.root().addEventListener("pointerdown", (e) => { 398 450 if (!(e.target instanceof HTMLElement)) return; 399 451 // Window focus 400 - const win = e.target.closest("#main-window, #equalizer-window, #playlist-window, #playlist-window-shade"); 452 + const win = e.target.closest("#main-window, #equalizer-window, #playlist-window, #playlist-window-shade, #milkdrop-window"); 401 453 if (win instanceof HTMLElement) { 402 454 if (win.id === "main-window") this.#focusedWindow.value = "main"; 403 455 else if (win.id === "equalizer-window") this.#focusedWindow.value = "eq"; 456 + else if (win.id === "milkdrop-window") this.#focusedWindow.value = "milkdrop"; 404 457 else this.#focusedWindow.value = "playlist"; 405 458 } 406 459 // Press feedback ··· 425 478 cancelAnimationFrame(this.#visRAF); 426 479 this.#visRAF = undefined; 427 480 } 481 + this.#stopButterchurn(); 428 482 } 429 483 430 484 // WINDOW SNAPPING — ported from webamp/js/snapUtils.ts ··· 516 570 this.#onPlaylistResizeStart(e); 517 571 return; 518 572 } 573 + if (e.target.id === "gen-resize-target") { 574 + this.#onMilkdropResizeStart(e); 575 + return; 576 + } 519 577 520 578 const draggable = e.target.closest(".draggable"); 521 579 if (!draggable) return; 522 580 const win = draggable.closest( 523 - "#main-window, #equalizer-window, #playlist-window", 581 + "#main-window, #equalizer-window, #playlist-window, #milkdrop-window", 524 582 ); 525 583 if (!(win instanceof HTMLElement)) return; 526 584 ··· 586 644 const onUp = () => { 587 645 document.removeEventListener("mousemove", onMove); 588 646 document.removeEventListener("mouseup", onUp); 647 + this.forceRender(); 648 + this.#saveLayout(); 589 649 }; 590 650 591 651 document.addEventListener("mousemove", onMove); ··· 593 653 }; 594 654 595 655 /** @param {MouseEvent} e */ 656 + #onMilkdropResizeStart = (e) => { 657 + e.preventDefault(); 658 + 659 + const milkdropEl = this.root().querySelector("#milkdrop-window"); 660 + if (!(milkdropEl instanceof HTMLElement)) return; 661 + 662 + const startMouseX = e.clientX; 663 + const startMouseY = e.clientY; 664 + const startWidth = this.#milkdropSize.width; 665 + const startHeight = this.#milkdropSize.height; 666 + const STEP_W = 25, STEP_H = 29, MIN_W = 275, MIN_H = 116; 667 + 668 + /** @param {MouseEvent} mv */ 669 + const onMove = (mv) => { 670 + const newWidth = Math.max( 671 + MIN_W, 672 + startWidth + Math.round((mv.clientX - startMouseX) / STEP_W) * STEP_W, 673 + ); 674 + const newHeight = Math.max( 675 + MIN_H, 676 + startHeight + Math.round((mv.clientY - startMouseY) / STEP_H) * STEP_H, 677 + ); 678 + this.#milkdropSize.width = newWidth; 679 + this.#milkdropSize.height = newHeight; 680 + milkdropEl.style.width = `${newWidth}px`; 681 + milkdropEl.style.height = `${newHeight}px`; 682 + const canvas = this.root().querySelector("#milkdrop-canvas"); 683 + if (canvas instanceof HTMLCanvasElement) { 684 + const cw = canvas.clientWidth; 685 + const ch = canvas.clientHeight; 686 + canvas.width = cw; 687 + canvas.height = ch; 688 + this.#butterchurn?.setRendererSize(cw, ch); 689 + } 690 + }; 691 + 692 + const onUp = () => { 693 + document.removeEventListener("mousemove", onMove); 694 + document.removeEventListener("mouseup", onUp); 695 + this.#saveLayout(); 696 + }; 697 + 698 + document.addEventListener("mousemove", onMove); 699 + document.addEventListener("mouseup", onUp); 700 + }; 701 + 596 702 #onPlaylistResizeStart = (e) => { 597 703 e.preventDefault(); 598 704 ··· 626 732 const onUp = () => { 627 733 document.removeEventListener("mousemove", onMove); 628 734 document.removeEventListener("mouseup", onUp); 735 + this.#saveLayout(); 629 736 }; 630 737 631 738 document.addEventListener("mousemove", onMove); ··· 671 778 pos: this.#playlistPos, 672 779 box: () => ({ x: this.#playlistPos.x, y: this.#playlistPos.y, width: ps.width, height: ps.height }), 673 780 }, 674 - ]; 781 + { 782 + el: /** @type {HTMLElement} */ (root.querySelector("#milkdrop-window")), 783 + pos: this.#milkdropPos, 784 + box: () => ({ x: this.#milkdropPos.x, y: this.#milkdropPos.y, width: this.#milkdropSize.width, height: this.#milkdropSize.height }), 785 + }, 786 + ].filter((e) => e.el != null); 675 787 } 676 788 677 789 // MARQUEE ··· 1087 1199 }; 1088 1200 1089 1201 #openConnect = () => { 1090 - window.open("l/?path", "_blank"); 1202 + window.open("l/?path=facets%2Fconnect%2Findex.html", "_blank"); 1091 1203 }; 1092 1204 1093 1205 #next = () => { ··· 1108 1220 if (rs) rs.setRepeat(!rs.repeat()); 1109 1221 }; 1110 1222 1223 + #centerWindows = () => { 1224 + const leftColH = 116 + 116 + this.#playlistSize.height; 1225 + const milkdropOpen = this.#milkdropOpen.value; 1226 + const totalW = milkdropOpen ? 275 + this.#milkdropSize.width : 275; 1227 + const totalH = milkdropOpen ? Math.max(leftColH, this.#milkdropSize.height) : leftColH; 1228 + const cx = Math.round((window.innerWidth - totalW) / 2); 1229 + const cy = Math.round((window.innerHeight - totalH) / 2); 1230 + this.#mainPos.x = cx; this.#mainPos.y = cy; 1231 + this.#eqPos.x = cx; this.#eqPos.y = cy + 116; 1232 + this.#playlistPos.x = cx; this.#playlistPos.y = cy + 232; 1233 + this.#milkdropPos.x = cx + 275; this.#milkdropPos.y = cy; 1234 + this.forceRender(); 1235 + this.#saveLayout(); 1236 + }; 1237 + 1238 + #saveLayout = () => { 1239 + const ui = loadUiState(); 1240 + localStorage.setItem(UI_STATE_KEY, JSON.stringify({ 1241 + ...ui, 1242 + positions: { 1243 + main: { ...this.#mainPos }, 1244 + eq: { ...this.#eqPos }, 1245 + playlist: { ...this.#playlistPos }, 1246 + milkdrop: { ...this.#milkdropPos }, 1247 + }, 1248 + sizes: { 1249 + playlist: { ...this.#playlistSize }, 1250 + milkdrop: { ...this.#milkdropSize }, 1251 + }, 1252 + })); 1253 + }; 1254 + 1255 + #toggleMilkdrop = () => { 1256 + this.#milkdropOpen.value = !this.#milkdropOpen.value; 1257 + const ui = loadUiState(); 1258 + localStorage.setItem(UI_STATE_KEY, JSON.stringify({ ...ui, milkdropOpen: this.#milkdropOpen.value })); 1259 + }; 1260 + 1261 + /** @param {HTMLCanvasElement} canvas */ 1262 + #initButterchurn = async (canvas) => { 1263 + this.#ensureAnalyser(); 1264 + if (!this.#audioCtx || !this.#analyser) return; 1265 + 1266 + const { default: butterchurn } = await import("butterchurn"); 1267 + const w = canvas.clientWidth || canvas.offsetWidth || this.#milkdropSize.width; 1268 + const h = canvas.clientHeight || canvas.offsetHeight || (this.#milkdropSize.height - 34); 1269 + canvas.width = w; 1270 + canvas.height = h; 1271 + this.#butterchurn = butterchurn.createVisualizer(this.#audioCtx, canvas, { width: w, height: h }); 1272 + this.#butterchurn.connectAudio(this.#analyser); 1273 + 1274 + const { default: raw } = await import("butterchurn-presets/dist/base.js"); 1275 + const presets = typeof raw?.default === "object" && raw.default !== null ? raw.default : raw; 1276 + this.#butterchurnPresetList = Object.values(presets ?? {}); 1277 + this.#cyclePreset(0); 1278 + 1279 + this.#butterchurnCycleInterval = setInterval(() => this.#cyclePreset(5.7), 15000); 1280 + this.#startButterchurn(); 1281 + }; 1282 + 1283 + #startButterchurn = () => { 1284 + if (this.#butterchurnRAF !== undefined) return; 1285 + const step = () => { 1286 + this.#butterchurnRAF = requestAnimationFrame(step); 1287 + if (this.isPlaying()) this.#butterchurn?.render(); 1288 + }; 1289 + step(); 1290 + if (this.#butterchurnCycleInterval === undefined && this.#butterchurnPresetList.length) { 1291 + this.#butterchurnCycleInterval = setInterval(() => this.#cyclePreset(5.7), 15000); 1292 + } 1293 + }; 1294 + 1295 + /** @param {number} transitionSecs */ 1296 + #cyclePreset = (transitionSecs) => { 1297 + const list = this.#butterchurnPresetList; 1298 + if (!list.length) return; 1299 + const preset = list[Math.floor(Math.random() * list.length)]; 1300 + this.#butterchurn?.loadPreset(preset, transitionSecs); 1301 + }; 1302 + 1303 + #stopButterchurn = () => { 1304 + if (this.#butterchurnRAF !== undefined) { 1305 + cancelAnimationFrame(this.#butterchurnRAF); 1306 + this.#butterchurnRAF = undefined; 1307 + } 1308 + if (this.#butterchurnCycleInterval !== undefined) { 1309 + clearInterval(this.#butterchurnCycleInterval); 1310 + this.#butterchurnCycleInterval = undefined; 1311 + } 1312 + }; 1313 + 1111 1314 #closeMain = () => { 1315 + const audioId = this.$queue.value?.now()?.id; 1316 + if (audioId && this.isPlaying()) this.$audio.value?.pause({ audioId }); 1112 1317 this.#mainOpen.value = false; 1113 1318 }; 1114 1319 ··· 1343 1548 return html` 1344 1549 <style> 1345 1550 @import "./themes/winamp/vendor/webamp.css"; 1551 + @import "./themes/winamp/vendor/gen-window.css"; 1346 1552 1347 1553 #webamp .playlist-track-titles > div { 1348 1554 padding-left: 3px; ··· 1381 1587 left: 72px; 1382 1588 right: auto; 1383 1589 } 1590 + #webamp .gen-top-title { 1591 + margin-top: 4px; 1592 + } 1593 + #webamp .gen-middle-left, 1594 + #webamp .gen-middle-right, 1595 + #webamp .gen-bottom { 1596 + position: relative; 1597 + } 1598 + #webamp #milkdrop-window .gen-middle-center { 1599 + overflow: hidden; 1600 + } 1384 1601 </style> 1385 1602 1386 - <div id="webamp"> 1603 + <div id="webamp" style="display: ${this.#mainOpen.value ? "block" : "none"}"> 1387 1604 <div 1388 1605 id="main-window" 1389 - class="window ${this.#stopped.value ? "stop" : this.isPlaying() ? "play" : audio ? "pause" : "stop"}${this.#mainShade.value ? " shade" : ""}" 1390 - style="position: absolute; top: ${this.#mainPos.y}px; left: ${this.#mainPos.x}px; display: ${this.#mainOpen.value ? "block" : "none"};" 1606 + class="window ${this.#stopped.value ? "stop" : this.isPlaying() ? "play" : audio ? "pause" : "stop"}${this.#mainShade.value ? " shade" : ""}${focused === "main" ? " selected" : ""}" 1607 + style="position: absolute; top: ${this.#mainPos.y}px; left: ${this.#mainPos.x}px;" 1391 1608 > 1392 - <div id="title-bar" class="${focused === "main" ? "selected " : ""}draggable" @dblclick="${this.#toggleMainShade}"> 1393 - <div id="option-context"><div id="option"></div></div> 1609 + <div id="title-bar" class="draggable" @dblclick="${this.#toggleMainShade}"> 1610 + <div id="option-context" @click="${this.#toggleMilkdrop}"><div id="option"></div></div> 1394 1611 <div id="minimize"></div> 1395 1612 <div id="shade" @click="${this.#toggleMainShade}"></div> 1396 1613 ${this.#mainShade.value ? html`<div class="mini-time${isPaused ? " blinking" : ""}">${miniTimeChars}</div>` : ""} ··· 1398 1615 </div> 1399 1616 <div class="webamp-status"> 1400 1617 <div id="clutter-bar"> 1401 - <div id="button-o"></div> 1618 + <div id="button-o" @click="${this.#centerWindows}"></div> 1402 1619 <div id="button-a"></div> 1403 1620 <div id="button-i"></div> 1404 1621 <div id="button-d"></div> ··· 1484 1701 1485 1702 <div 1486 1703 id="equalizer-window" 1487 - class="window${this.#eqShade.value ? " shade" : ""}" 1704 + class="window${this.#eqShade.value ? " shade" : ""}${focused === "eq" ? " selected" : ""}" 1488 1705 style="position: absolute; top: ${this.#eqPos.y}px; left: ${this.#eqPos.x}px; display: ${this.#eqOpen.value ? "block" : "none"};" 1489 1706 > 1490 1707 ${this.#eqShade.value ? html` ··· 1495 1712 <input type="range" id="equalizer-balance" class="${balanceClass}" min="-100" max="100" value="${balance}" @input="${this.#onBalanceInput}"> 1496 1713 </div> 1497 1714 ` : html` 1498 - <div class="equalizer-top title-bar${focused === "eq" ? " selected" : ""} draggable" @dblclick="${this.#toggleEqShade}"> 1715 + <div class="equalizer-top title-bar draggable" @dblclick="${this.#toggleEqShade}"> 1499 1716 <div id="equalizer-shade" @click="${this.#toggleEqShade}"></div> 1500 1717 <div id="equalizer-close" @click="${this.#toggleEq}"></div> 1501 1718 </div> ··· 1613 1830 </div> 1614 1831 </div> 1615 1832 </div> 1833 + 1834 + <div 1835 + id="milkdrop-window" 1836 + class="window gen-window${this.#focusedWindow.value === "milkdrop" ? " selected" : ""}" 1837 + style="position: absolute; top: ${this.#milkdropPos.y}px; left: ${this.#milkdropPos.x}px; width: ${this.#milkdropSize.width}px; height: ${this.#milkdropSize.height}px; display: ${this.#milkdropOpen.value ? "flex" : "none"};" 1838 + > 1839 + <div class="gen-top draggable"> 1840 + <div class="gen-top-left draggable"></div> 1841 + <div class="gen-top-left-fill draggable"></div> 1842 + <div class="gen-top-left-end draggable"></div> 1843 + <div class="gen-top-title draggable"> 1844 + ${"MILKDROP".split("").map((c) => html`<div class="draggable gen-text-letter gen-text-${c.toLowerCase()}"></div>`)} 1845 + </div> 1846 + <div class="gen-top-right-end draggable"></div> 1847 + <div class="gen-top-right-fill draggable"></div> 1848 + <div class="gen-top-right draggable"> 1849 + <div class="gen-close selected" @click="${this.#toggleMilkdrop}"></div> 1850 + </div> 1851 + </div> 1852 + <div class="gen-middle"> 1853 + <div class="gen-middle-left draggable"> 1854 + <div class="gen-middle-left-bottom draggable"></div> 1855 + </div> 1856 + <div class="gen-middle-center" style="background: #000;"> 1857 + <canvas id="milkdrop-canvas" style="position: absolute; inset: 0; width: 100%; height: 100%;"></canvas> 1858 + </div> 1859 + <div class="gen-middle-right draggable"> 1860 + <div class="gen-middle-right-bottom draggable"></div> 1861 + </div> 1862 + </div> 1863 + <div class="gen-bottom draggable"> 1864 + <div class="gen-bottom-left draggable"></div> 1865 + <div class="gen-bottom-right draggable"> 1866 + <div id="gen-resize-target"></div> 1867 + </div> 1868 + </div> 1869 + </div> 1870 + 1616 1871 </div> 1617 1872 `; 1618 1873 }