From 85b7838db58713ebb2d3005767a679bd175b53df Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Sat, 29 Nov 2025 19:13:44 +0100 Subject: [PATCH] feat: initial work for artwork-controller --- deno.jsonc | 3 + deno.lock | 16 + src/_includes/layouts/diffuse.vto | 27 +- src/common/element.js | 11 +- src/common/index.js | 10 +- src/components/engine/audio/element.js | 26 +- .../orchestrator/queue-tracks/element.js | 17 +- src/components/output/types.d.ts | 3 +- src/components/processor/artwork/worker.js | 12 + src/index.css | 3 +- src/index.vto | 4 +- src/styles/diffuse/colors.css | 3 +- .../blur/artwork-controller/_applet.astro | 791 ------------------ .../blur/artwork-controller/element.css | 299 +++++++ src/themes/blur/artwork-controller/element.js | 266 ++++++ src/themes/blur/artwork-controller/index.vto | 53 ++ src/themes/blur/index.js | 7 +- src/themes/webamp/browser/element.js | 33 +- src/themes/webamp/index.vto | 2 +- 19 files changed, 734 insertions(+), 852 deletions(-) delete mode 100644 src/themes/blur/artwork-controller/_applet.astro create mode 100644 src/themes/blur/artwork-controller/element.css create mode 100644 src/themes/blur/artwork-controller/element.js create mode 100644 src/themes/blur/artwork-controller/index.vto diff --git a/deno.jsonc b/deno.jsonc index 526697cc..2d510095 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -8,6 +8,7 @@ "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2", "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.9.4", "@fry69/deep-diff": "jsr:@fry69/deep-diff@^0.1.10", + "@js-temporal/polyfill": "npm:@js-temporal/polyfill@^0.5.1", "@kunkun/kkrpc": "jsr:@kunkun/kkrpc@^0.6.0", "@mary/ds-queue": "jsr:@mary/ds-queue@^0.1.3", "@mys/m-rpc": "jsr:@mys/m-rpc@^0.12.2", @@ -17,6 +18,7 @@ "@vicary/debounce-microtask": "jsr:@vicary/debounce-microtask@^0.1.8", "alien-signals": "npm:alien-signals@^3.0.0", "esbuild-plugins-node-modules-polyfill": "npm:esbuild-plugins-node-modules-polyfill@^1.7.1", + "fast-average-color": "npm:fast-average-color@^9.5.0", "idb-keyval": "npm:idb-keyval@^6.2.2", "lit-html": "npm:lit-html@^3.3.1", "morphdom": "npm:morphdom@^2.7.7/dist/morphdom.js", @@ -40,6 +42,7 @@ "@common/": "./src/common/", "@components/": "./src/components/", "@definitions/": "./src/definitions/", + "@styles/": "./src/styles/", // Build "@std/fs": "jsr:@std/fs@^1.0.19", diff --git a/deno.lock b/deno.lock index a0b3bc3c..13ecad42 100644 --- a/deno.lock +++ b/deno.lock @@ -42,10 +42,12 @@ "npm:@atcute/lex-cli@*": "2.3.1", "npm:@atcute/lex-cli@^2.3.1": "2.3.1", "npm:@atcute/lexicons@^1.2.2": "1.2.2", + "npm:@js-temporal/polyfill@~0.5.1": "0.5.1", "npm:@tauri-apps/plugin-shell@^2.2.0": "2.3.3", "npm:alien-signals@3": "3.0.3", "npm:autoprefixer@10.4.21": "10.4.21_postcss@8.5.6", "npm:esbuild-plugins-node-modules-polyfill@^1.7.1": "1.7.1_esbuild@0.25.12", + "npm:fast-average-color@^9.5.0": "9.5.0", "npm:idb-keyval@^6.2.2": "6.2.2", "npm:lightningcss-wasm@1.30.1": "1.30.1", "npm:lit-html@^3.3.1": "3.3.1", @@ -361,6 +363,12 @@ "os": ["win32"], "cpu": ["x64"] }, + "@js-temporal/polyfill@0.5.1": { + "integrity": "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==", + "dependencies": [ + "jsbi" + ] + }, "@jspm/core@2.1.0": { "integrity": "sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==" }, @@ -753,6 +761,9 @@ "exsolve@1.0.8": { "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==" }, + "fast-average-color@9.5.0": { + "integrity": "sha512-nC6x2YIlJ9xxgkMFMd1BNoM1ctMjNoRKfRliPmiEWW3S6rLTHiQcy9g3pt/xiKv/D0NAAkhb9VyV+WJFvTqMGg==" + }, "fflate@0.8.2": { "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" }, @@ -877,6 +888,9 @@ "js-tokens@4.0.0": { "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "jsbi@4.3.2": { + "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==" + }, "jszip@3.10.1": { "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dependencies": [ @@ -1790,8 +1804,10 @@ "npm:98.css@~0.1.21", "npm:@atcute/lex-cli@^2.3.1", "npm:@atcute/lexicons@^1.2.2", + "npm:@js-temporal/polyfill@~0.5.1", "npm:alien-signals@3", "npm:esbuild-plugins-node-modules-polyfill@^1.7.1", + "npm:fast-average-color@^9.5.0", "npm:idb-keyval@^6.2.2", "npm:lit-html@^3.3.1", "npm:morphdom@^2.7.7", diff --git a/src/_includes/layouts/diffuse.vto b/src/_includes/layouts/diffuse.vto index c7f790b0..df219717 100644 --- a/src/_includes/layouts/diffuse.vto +++ b/src/_includes/layouts/diffuse.vto @@ -1,5 +1,9 @@ --- title: "Diffuse" + +base: "./" +scripts: [] +styles: [] --- @@ -13,20 +17,29 @@ title: "Diffuse" {{title}} - - - - + + + + + + {{ for url of styles }} + + {{ /for }} + + + {{ for url of scripts }} + + {{ /for }} {{ content }} diff --git a/src/common/element.js b/src/common/element.js index c588e631..5fd4e9c6 100644 --- a/src/common/element.js +++ b/src/common/element.js @@ -46,7 +46,7 @@ export class DiffuseElement extends HTMLElement { } /** - * Effect helper that automatically disposes it + * Effect helper that automatically is disposes * when this element is removed from the DOM. * * @param {() => void} fn @@ -265,6 +265,15 @@ export class BroadcastableDiffuseElement extends DiffuseElement { return /** @type {ProxiedActions} */ (actions); } + async isLeader() { + if (this.broadcasted) { + const status = await this.broadcastingStatus(); + return status.leader; + } else { + return true; + } + } + // LIFECYCLE /** diff --git a/src/common/index.js b/src/common/index.js index f6232f33..dfec745c 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -1,4 +1,4 @@ -// import * as Uint8 from "uint8arrays"; +import * as Uint8 from "uint8arrays"; import { xxh32r } from "xxh32/dist/raw.js"; /** @@ -92,9 +92,7 @@ export function jsonEncode(a) { * @returns {Promise} */ export async function trackArtworkCacheId(track) { - // TODO: - return ""; - // return await crypto.subtle - // .digest("SHA-256", new TextEncoder().encode(track.uri)) - // .then((a) => Uint8.toString(new Uint8Array(a), "base64url")); + return await crypto.subtle + .digest("SHA-256", new TextEncoder().encode(track.uri)) + .then((a) => Uint8.toString(new Uint8Array(a), "base64url")); } diff --git a/src/components/engine/audio/element.js b/src/components/engine/audio/element.js index 60b0cac9..511e91bd 100644 --- a/src/components/engine/audio/element.js +++ b/src/components/engine/audio/element.js @@ -20,6 +20,8 @@ const _SILENT_MP3 = * @implements {Actions} */ class AudioEngine extends BroadcastableDiffuseElement { + static NAME = "diffuse/engine/audio"; + // SIGNALS #items = signal(/** @type {Audio[]} */ ([])); @@ -43,15 +45,18 @@ class AudioEngine extends BroadcastableDiffuseElement { connectedCallback() { // Setup leader election if shared if (this.hasAttribute("group")) { - const actions = this.broadcast(`diffuse/engine/audio/${this.group}`, { - adjustVolume: { strategy: "leaderOnly", fn: this.adjustVolume }, - pause: { strategy: "leaderOnly", fn: this.pause }, - play: { strategy: "leaderOnly", fn: this.play }, - seek: { strategy: "leaderOnly", fn: this.seek }, - supply: { strategy: "replicate", fn: this.supply }, - - setIsPlaying: { strategy: "replicate", fn: this.$isPlaying.set }, - }); + const actions = this.broadcast( + `${this.constructor.prototype.constructor.NAME}/${this.group}`, + { + adjustVolume: { strategy: "leaderOnly", fn: this.adjustVolume }, + pause: { strategy: "leaderOnly", fn: this.pause }, + play: { strategy: "leaderOnly", fn: this.play }, + seek: { strategy: "leaderOnly", fn: this.seek }, + supply: { strategy: "replicate", fn: this.supply }, + + setIsPlaying: { strategy: "replicate", fn: this.$isPlaying.set }, + }, + ); if (actions) { this.adjustVolume = actions.adjustVolume; @@ -68,7 +73,8 @@ class AudioEngine extends BroadcastableDiffuseElement { super.connectedCallback(); // Get volume from previous session if possible - const VOLUME_KEY = `diffuse/engine/audio/${this.group}/volume`; + const VOLUME_KEY = + `${this.constructor.prototype.constructor.NAME}/${this.group}/volume`; const volume = localStorage.getItem(VOLUME_KEY); if (volume != undefined) { diff --git a/src/components/orchestrator/queue-tracks/element.js b/src/components/orchestrator/queue-tracks/element.js index 44d03221..3ea0b2bd 100644 --- a/src/components/orchestrator/queue-tracks/element.js +++ b/src/components/orchestrator/queue-tracks/element.js @@ -1,11 +1,10 @@ import { + BroadcastableDiffuseElement, callWorkerWithProvisions, - DiffuseElement, query, terminateProvisions, whenElementsDefined, workerProxy, - workerTunnel, } from "@common/element.js"; import { untracked } from "@common/signal.js"; @@ -28,7 +27,7 @@ import { untracked } from "@common/signal.js"; * tracks have been loaded, * or the tracks collection changes. */ -class QueueTracksOrchestrator extends DiffuseElement { +class QueueTracksOrchestrator extends BroadcastableDiffuseElement { static NAME = "diffuse/orchestrator/queue-tracks"; static WORKER_URL = "components/orchestrator/queue-tracks/worker.js"; @@ -77,11 +76,15 @@ class QueueTracksOrchestrator extends DiffuseElement { // Watch tracks collection this.effect(() => { - const tracks = output.tracks.collection().filter((t) => - t.kind !== "placeholder" - ); + const tracks = output.tracks.collection(); - untracked(() => this.poolAvailable(tracks)); + this.isLeader().then((isLeader) => { + if (!isLeader) return; + + untracked(() => + this.poolAvailable(tracks.filter((t) => t.kind !== "placeholder")) + ); + }); }); } diff --git a/src/components/output/types.d.ts b/src/components/output/types.d.ts index eda2ea3e..d46bb8ae 100644 --- a/src/components/output/types.d.ts +++ b/src/components/output/types.d.ts @@ -1,7 +1,8 @@ import type { SignalReader } from "@common/signal.d.ts"; import type { Track } from "@definitions/types.d.ts"; +import type { DiffuseElement } from "@common/element.js"; -export type OutputElement = HTMLElement & OutputManager; +export type OutputElement = DiffuseElement & OutputManager; export type OutputManager = { tracks: { diff --git a/src/components/processor/artwork/worker.js b/src/components/processor/artwork/worker.js index 9ec9089c..29eff824 100644 --- a/src/components/processor/artwork/worker.js +++ b/src/components/processor/artwork/worker.js @@ -2,6 +2,7 @@ import * as IDB from "idb-keyval"; import { IDB_ARTWORK_PREFIX } from "./constants.js"; import { musicMetadataTags } from "../metadata/common.js"; +import { ostiary, rpc } from "@common/worker.js"; /** * @import {IPicture} from "music-metadata" @@ -35,6 +36,17 @@ export function supply(items) { if (exe) shiftQueue(); } +//////////////////////////////////////////// +// ⚡️ +//////////////////////////////////////////// + +ostiary((context) => { + rpc(context, { + artwork, + supply, + }); +}); + //////////////////////////////////////////// // 🛠️ //////////////////////////////////////////// diff --git a/src/index.css b/src/index.css index 11ab6907..5c1377db 100644 --- a/src/index.css +++ b/src/index.css @@ -80,9 +80,10 @@ small { } .construct { - color: oklch(1 0 0 / 0.75); + color: oklch(from currentColor l c h / 0.65); font-size: var(--fs-2xl); font-weight: 900; + letter-spacing: -0.0125em; line-height: 0.775em; line-height: 1.05cap; margin-bottom: var(--space-md); diff --git a/src/index.vto b/src/index.vto index 1f5d5140..3f4975e8 100644 --- a/src/index.vto +++ b/src/index.vto @@ -1,6 +1,9 @@ --- layout: layouts/diffuse.vto +styles: + - index.css + # THEMES themes: @@ -116,7 +119,6 @@ definitions: url: "definitions/output/tracks.json" --- -

diff --git a/src/styles/diffuse/colors.css b/src/styles/diffuse/colors.css index 182d2fbb..0e298114 100644 --- a/src/styles/diffuse/colors.css +++ b/src/styles/diffuse/colors.css @@ -3,7 +3,8 @@ --color-1: oklch(4.1308% 0.25306 109.22); --color-2: oklch(98.369% 0.01834 67.664); --color-3: oklch(26.787% 0.00168 186.65); - --accent: oklch(86.947% 0.25527 28.789); + /*--accent: oklch(86.947% 0.25527 28.789);*/ + --accent: hsl(80, 60.5%, 34.7%); --bg-color: var(--color-2); --text-color: var(--color-1); diff --git a/src/themes/blur/artwork-controller/_applet.astro b/src/themes/blur/artwork-controller/_applet.astro deleted file mode 100644 index 9b1493d3..00000000 --- a/src/themes/blur/artwork-controller/_applet.astro +++ /dev/null @@ -1,791 +0,0 @@ ---- -import "@styles/reset.css"; -import "@styles/variables.css"; -import "@styles/fonts.css"; -import "@styles/animations.css"; -import "@styles/icons/phosphor.css"; - -import "@styles/diffuse/colors.css"; -import "@styles/diffuse/fonts.css"; ---- - -
-
- -
-
-
-
-
-
-
-
-
-
-
- -
- - -
-
-
- - - - - - diff --git a/src/themes/blur/artwork-controller/element.css b/src/themes/blur/artwork-controller/element.css new file mode 100644 index 00000000..fbf90832 --- /dev/null +++ b/src/themes/blur/artwork-controller/element.css @@ -0,0 +1,299 @@ +@import "../../../styles/reset.css"; +@import "../../../styles/variables.css"; +@import "../../../styles/animations.css"; +/*@import "../../../styles/icons/phosphor.css";*/ + +@import "../../../styles/diffuse/colors.css"; +@import "../../../styles/diffuse/fonts.css"; + +:root { + --transition-durition: 500ms; +} + +main { + background: var(--color-3); + color: white; + display: flex; + flex-direction: column; + font-size: var(--fs-sm); + height: 100dvh; + overflow: hidden; + position: relative; + transition: + background-color var(--transition-durition), + color var(--transition-durition); +} + +/* Artwork */ + +.artwork { + app-region: drag; + flex: 1; + position: relative; + user-select: none; +} + +.artwork img { + height: 100%; + left: 0; + object-fit: cover; + opacity: 0; + position: absolute; + top: 0; + transition-duration: var(--transition-durition); + transition-property: opacity; + width: 100%; + z-index: 0; +} + +.artwork label { + background: oklch(0 0 0); + border-radius: var(--radius-sm); + box-shadow: var(--box-shadow-lg); + font-size: var(--fs-2xs); + font-weight: 600; + left: var(--space-xs); + letter-spacing: var(--tracking-wide); + line-height: 1; + padding: 7px 6px 6px; + position: absolute; + text-transform: uppercase; + top: var(--space-xs); + transition: + background-color var(--transition-durition), + color var(--transition-durition); + z-index: 10; +} + +/* Progress bars */ + +progress { + appearance: none; + border: 0; + display: block; + height: 4px; + width: 100%; +} + +progress, +progress::-webkit-progress-bar { + background-color: color-mix(in oklch, currentColor 40%, transparent); + overflow: hidden; + border-radius: 4px; +} + +progress[value]::-webkit-progress-value { + border-radius: 4px; + background-color: color-mix(in oklch, currentColor 90%, transparent); +} + +progress[value]::-moz-progress-bar { + border-radius: 4px; + background-color: color-mix(in oklch, currentColor 50%, transparent); +} + +/* Controller */ + +.controller { + flex-shrink: 0; + padding: 0 var(--space-md) var(--space-md); + position: relative; +} + +.controller__background { + inset: 0; + opacity: 0.5; + position: absolute; + transition: background-color var(--transition-durition); + z-index: 1; +} + +.controller__inner { + position: relative; + transition-duration: var(--transition-durition); + transition-property: color; + z-index: 10; +} + +.controller__inner.controller__inner--light-mode { + color: rgba(0, 0, 0, 0.6); +} + +/* Now playing */ + +cite { + display: block; + font-style: normal; + text-shadow: var(--text-shadow-sm); +} + +.controller__inner--light-mode cite { + text-shadow: none; +} + +/* Progress */ + +.progress { + cursor: pointer; + margin: var(--space-xs) 0; + padding-top: var(--space-2xs); +} + +.timestamps { + display: flex; + font-size: var(--fs-2xs); + font-weight: 500; + justify-content: space-between; + margin-top: var(--space-3xs); + opacity: 0.4; + text-shadow: var(--text-shadow-xs); +} + +.controller__inner--light-mode .timestamps { + text-shadow: none; +} + +/* Controls */ + +.controller menu { + align-items: center; + display: flex; + font-size: var(--fs-lg); + gap: var(--space-lg); + justify-content: center; + margin: var(--space-md) 0; + padding: 0; + text-align: center; +} + +.controller .menu__loader { + line-height: 0; + transform-origin: center; +} + +.controller command { + cursor: pointer; + line-height: 0; + transition-duration: var(--transition-durition); + transition-property: opacity; +} + +.controller .ph-pause, +.controller .ph-play, +.controller .menu__loader { + font-size: var(--fs-xl); +} + +/* Volume */ + +footer { + align-items: center; + display: flex; + font-size: var(--fs-xs); + gap: var(--space-2xs); + justify-content: space-between; +} + +footer .progress-bar { + cursor: pointer; + flex: 1; + padding: var(--space-2xs) 0; +} + +footer i { + cursor: pointer; +} + +/* Gradient blur */ + +.gradient-blur { + bottom: 0; + height: 150%; + left: 0; + pointer-events: none; + position: absolute; + right: 0; + z-index: 2; +} + +.gradient-blur > div { + position: absolute; + inset: 0; +} + +.gradient-blur > div:nth-of-type(1) { + backdrop-filter: blur(1px); + mask: linear-gradient( + to bottom, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 1) 4.166666665%, + rgba(0, 0, 0, 1) 8.333333332%, + rgba(0, 0, 0, 0) 12.499999999% + ); + z-index: 1; +} + +.gradient-blur > div:nth-of-type(2) { + backdrop-filter: blur(2px); + mask: linear-gradient( + to bottom, + rgba(0, 0, 0, 0) 4.166666665%, + rgba(0, 0, 0, 1) 8.333333332%, + rgba(0, 0, 0, 1) 12.499999999%, + rgba(0, 0, 0, 0) 16.666666666% + ); + z-index: 2; +} + +.gradient-blur > div:nth-of-type(3) { + backdrop-filter: blur(4px); + mask: linear-gradient( + to bottom, + rgba(0, 0, 0, 0) 8.333333332%, + rgba(0, 0, 0, 1) 12.499999999%, + rgba(0, 0, 0, 1) 16.666666666%, + rgba(0, 0, 0, 0) 20.833333333% + ); + z-index: 3; +} + +.gradient-blur > div:nth-of-type(4) { + backdrop-filter: blur(8px); + mask: linear-gradient( + to bottom, + rgba(0, 0, 0, 0) 12.499999999%, + rgba(0, 0, 0, 1) 16.666666666%, + rgba(0, 0, 0, 1) 20.833333333%, + rgba(0, 0, 0, 0) 25% + ); + z-index: 4; +} + +.gradient-blur > div:nth-of-type(5) { + backdrop-filter: blur(16px); + mask: linear-gradient( + to bottom, + rgba(0, 0, 0, 0) 16.666666666%, + rgba(0, 0, 0, 1) 20.833333333%, + rgba(0, 0, 0, 1) 25%, + rgba(0, 0, 0, 0) 100% + ); + z-index: 5; +} + +.gradient-blur > div:nth-of-type(6) { + backdrop-filter: blur(32px); + mask: linear-gradient( + to bottom, + rgba(0, 0, 0, 0) 20.833333333%, + rgba(0, 0, 0, 1) 25%, + rgba(0, 0, 0, 1) 100% + ); + z-index: 6; +} + +.gradient-blur > div:nth-of-type(7) { + backdrop-filter: blur(64px); + mask: linear-gradient(to bottom, rgba(0, 0, 0, 0) 25%, rgba(0, 0, 0, 1) 100%); + z-index: 7; +} diff --git a/src/themes/blur/artwork-controller/element.js b/src/themes/blur/artwork-controller/element.js new file mode 100644 index 00000000..62eb22a7 --- /dev/null +++ b/src/themes/blur/artwork-controller/element.js @@ -0,0 +1,266 @@ +import { FastAverageColor } from "fast-average-color"; +import { Temporal } from "@js-temporal/polyfill"; +import { xxh32r } from "xxh32/dist/raw.js"; +import { debounce } from "throttle-debounce"; + +import { DiffuseElement, query, whenElementsDefined } from "@common/element.js"; +import { trackArtworkCacheId } from "@common/index.js"; +import { signal } from "@common/signal.js"; + +/** + * @import {RenderArg} from "@common/element.d.ts" + * @import {Track} from "@definitions/types.d.ts" + * @import {InputElement} from "@components/input/types.d.ts" + * @import {OutputElement} from "@components/output/types.d.ts" + * @import {Artwork} from "@components/processor/artwork/types.d.ts" + */ + +class ArtworkController extends DiffuseElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + } + + // SIGNALS + + // activeTrack = signal(/** @type {Track | undefined} */ (undefined)); + #artwork = signal(/** @type {Artwork[]} */ ([])); + #artworkColor = signal(/** @type {string | undefined} */ (undefined)); + #artworkLightMode = signal(false); + #duration = signal("0:00"); + // isLoading = signal(true); + // isPlaying = signal(false); + // progress = signal(0); + #time = signal("0:00"); + // volume = signal(0); + + // LIFECYCLE + + /** + * @override + */ + connectedCallback() { + super.connectedCallback(); + + /** @type {import("@components/processor/artwork/element.js").CLASS} */ + const artwork = query(this, "artwork-processor-selector"); + + /** @type {import("@components/engine/audio/element.js").CLASS} */ + const audio = query(this, "audio-engine-selector"); + + /** @type {InputElement} */ + const input = query(this, "input-selector"); + + /** @type {import("@components/engine/queue/element.js").CLASS} */ + const queue = query(this, "queue-engine-selector"); + + this.artwork = artwork; + this.audio = audio; + this.input = input; + this.queue = queue; + + whenElementsDefined({ audio, artwork, input, queue }).then(() => { + // Changed artwork based on active queue item. + const debouncedChangeArtwork = debounce( + 1000, + this.#changeArtwork.bind(this), + ); + + this.effect(() => { + debouncedChangeArtwork(queue.now()); + }); + + this.effect(() => { + const trigger = queue.now(); + const _other_trigger = queue.poolHash(); + + queue.fill({ amount: 10, shuffled: true }); + if (!trigger) queue.shift(); + }); + + // Force render when elements are defined + + // this.effect(() => { + // this.forceRender(); + // }); + }); + + this.#artworkEffects(); + } + + // EFFECTS + + /** + * @param {Track | null} track + */ + async #changeArtwork(track) { + if (!track) { + this.#artwork.value = []; + return; + } + + const cacheId = await trackArtworkCacheId(track); + + const resGet = await this.input?.resolve({ method: "GET", uri: track.uri }); + const resHead = await this.input?.resolve({ + method: "HEAD", + uri: track.uri, + }); + + if (!resGet) return; + + const request = "stream" in resGet + ? { + cacheId, + stream: resGet.stream, + tags: track.tags, + } + : { + cacheId, + tags: track.tags, + urls: { + get: resGet.url, + head: resHead && "url" in resHead ? resHead.url : resGet.url, + }, + }; + + const art = await this.artwork?.artwork(request) ?? []; + const currCacheId = track ? await trackArtworkCacheId(track) : undefined; + if (cacheId === currCacheId) this.#artwork.set(art); + } + + #artworkEffects() { + /** @type {Record>} */ + const timeouts = {}; + + this.effect(() => { + const art = this.#artwork.value; + + // No artwork, fade out existing. + if (art.length === 0) { + this.querySelectorAll(":scope .artwork img").forEach((el) => { + const element = /** @type {HTMLElement} */ (el); + element.style.opacity = "0"; + const hash = element.getAttribute("data-hash"); + if (hash) timeouts[hash] = setTimeout(() => element.remove(), 1000); + }); + return; + } + + // Determine if the current artwork needs to be replaced. + const hash = xxh32r(art[0].bytes).toString(); + + /** @type {HTMLImageElement | null} */ + const existingArtwork = this.querySelector( + `:scope .artwork img[data-hash="${hash}"]`, + ); + + // If the artwork is the same, stop here. + if (existingArtwork) { + const timeoutId = timeouts[hash]; + if (timeoutId) clearTimeout(timeoutId); + existingArtwork.style.opacity = "1"; + return; + } + + // Add new artwork + const blob = new Blob( + [/** @type {ArrayBuffer} */ (art[0].bytes.buffer)], + { type: art[0].mime }, + ); + const url = URL.createObjectURL(blob); + + /** @type {HTMLImageElement} */ + const img = document.createElement("img"); + img.setAttribute("data-hash", hash); + img.src = url; + + // Extract average color + img.onload = () => { + const fac = new FastAverageColor(); + const color = fac.getColor(img); + const rgb = color.value; + const o = Math.round( + (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000, + ); + + this.#artworkColor.value = color.rgba; + this.#artworkLightMode.value = o > 165; + + /** @type {HTMLElement | null} */ + const bg = this.querySelector(":scope .controller__background"); + if (bg) bg.style.backgroundColor = color.rgba; + + /** @type {HTMLElement | null} */ + const main = this.querySelector(":scope main"); + if (main) main.style.backgroundColor = color.rgba; + + img.style.opacity = "1"; + + this.querySelectorAll(":scope .artwork img").forEach((el) => { + if (el === img) return; + + const element = /** @type {HTMLElement} */ (el); + element.style.opacity = "0"; + timeouts[hash] = setTimeout(() => element.remove(), 1000); + }); + }; + + // Insert new artwork + this.querySelector(":scope .artwork")?.appendChild(img); + }); + + this.effect(() => { + // if (artworkLightMode()) { + // controller.classList.add("controller__inner--light-mode"); + // } else controller.classList.remove("controller__inner--light-mode"); + }); + } + + // RENDER + + /** + * @param {RenderArg} _ + */ + render({ html }) { + return html` + + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+
+
+ `; + } +} + +export default ArtworkController; + +//////////////////////////////////////////// +// REGISTER +//////////////////////////////////////////// + +export const CLASS = ArtworkController; +export const NAME = "db-artwork-controller"; + +customElements.define(NAME, CLASS); diff --git a/src/themes/blur/artwork-controller/index.vto b/src/themes/blur/artwork-controller/index.vto new file mode 100644 index 00000000..a655bf8a --- /dev/null +++ b/src/themes/blur/artwork-controller/index.vto @@ -0,0 +1,53 @@ +--- +layout: layouts/diffuse.vto + +base: "../../../" + +scripts: + - element.js +--- + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/themes/blur/index.js b/src/themes/blur/index.js index 30e36c78..23e0083e 100644 --- a/src/themes/blur/index.js +++ b/src/themes/blur/index.js @@ -57,10 +57,5 @@ effect(() => { // 🛠️ async function isLeader() { - if (audio.broadcasted) { - const status = await audio.broadcastingStatus(); - return status.leader; - } else { - return true; - } + return await audio.isLeader(); } diff --git a/src/themes/webamp/browser/element.js b/src/themes/webamp/browser/element.js index 82f0ba8d..4389d130 100644 --- a/src/themes/webamp/browser/element.js +++ b/src/themes/webamp/browser/element.js @@ -1,4 +1,4 @@ -import { DiffuseElement, query } from "@common/element.js"; +import { DiffuseElement, query, whenElementsDefined } from "@common/element.js"; /** * @import {RenderArg} from "@common/element.d.ts" @@ -10,18 +10,7 @@ import { DiffuseElement, query } from "@common/element.js"; class Browser extends DiffuseElement { constructor() { super(); - - // Enable Shadow DOM this.attachShadow({ mode: "open" }); - - /** @type {InputElement} */ - this.input = query(this, "input-selector"); - - /** @type {OutputElement} */ - this.output = query(this, "output-selector"); - - /** @type {import("@components/engine/queue/element.js").CLASS} */ - this.queue = query(this, "queue-selector"); } // LIFECYCLE @@ -32,15 +21,21 @@ class Browser extends DiffuseElement { connectedCallback() { super.connectedCallback(); - // Wait for the above dependencies to be defined, then render again. - (async () => { - await customElements.whenDefined(this.input.localName); - await customElements.whenDefined(this.output.localName); + /** @type {InputElement} */ + this.input = query(this, "input-selector"); + + /** @type {OutputElement} */ + this.output = query(this, "output-selector"); + /** @type {import("@components/engine/queue/element.js").CLASS} */ + this.queue = query(this, "queue-engine-selector"); + + // Wait for the above dependencies to be defined, then render again. + whenElementsDefined({ input: this.input, output: this.output }).then(() => { this.effect(() => { this.forceRender(); }); - })(); + }); } // EVENTS @@ -66,7 +61,7 @@ class Browser extends DiffuseElement { * @param {Track} track */ playTrack(track) { - this.queue.add({ + this.queue?.add({ inFront: true, tracks: [track], }); @@ -78,7 +73,7 @@ class Browser extends DiffuseElement { * @param {RenderArg} _ */ render({ html }) { - const tracks = this.output.tracks?.collection() || []; + const tracks = this.output?.tracks?.collection() ?? []; return html` diff --git a/src/themes/webamp/index.vto b/src/themes/webamp/index.vto index d0ba7cd6..442c3218 100644 --- a/src/themes/webamp/index.vto +++ b/src/themes/webamp/index.vto @@ -37,7 +37,7 @@ -- 2.51.2