From 4a2a789d2383f372dc19d79f2624e7b7ef88f695 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Mon, 27 Apr 2026 23:46:35 +0200 Subject: [PATCH] feat(theme/winamp): artwork window --- src/facets/themes/winamp/artwork/element.js | 171 ++++++++++++++++++ src/facets/themes/winamp/facet/index.html | 22 +++ .../themes/winamp/facet/index.inline.js | 2 + src/images/icons/windows_98/imagwmf-1.png | Bin 0 -> 503 bytes .../windows_98/wia_img_color_sound-0.png | Bin 0 -> 562 bytes 5 files changed, 195 insertions(+) create mode 100644 src/facets/themes/winamp/artwork/element.js create mode 100644 src/images/icons/windows_98/imagwmf-1.png create mode 100644 src/images/icons/windows_98/wia_img_color_sound-0.png diff --git a/src/facets/themes/winamp/artwork/element.js b/src/facets/themes/winamp/artwork/element.js new file mode 100644 index 00000000..27cff5eb --- /dev/null +++ b/src/facets/themes/winamp/artwork/element.js @@ -0,0 +1,171 @@ +import { + defineElement, + DiffuseElement, + query, + whenElementsDefined, +} from "~/common/element.js"; +import { signal, untracked } from "~/common/signal.js"; + +/** + * @import {RenderArg} from "~/common/element.d.ts" + * + * @import ArtworkOrchestrator from "~/components/orchestrator/artwork/element.js" + * @import ControllerOrchestrator from "~/components/orchestrator/controller/element.js" + */ + +class ArtworkWindowElement extends DiffuseElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + } + + // SIGNALS + + #artworkUrl = signal(/** @type {string | null} */ (null)); + + /** @type {string | null} */ + #prevUrl = null; + + // SIGNALS - DEPENDENCIES + + $artwork = signal(/** @type {ArtworkOrchestrator | undefined} */ (undefined)); + $controller = signal( + /** @type {ControllerOrchestrator | undefined} */ (undefined), + ); + + // COMPUTED + + currentTrack = () => this.$controller.value?.currentTrack(); + + // LIFECYCLE + + /** + * @override + */ + connectedCallback() { + super.connectedCallback(); + + /** @type {ArtworkOrchestrator} */ + const artwork = query(this, "artwork-selector"); + + /** @type {ControllerOrchestrator} */ + const controller = query(this, "controller-orchestrator-selector"); + + whenElementsDefined({ artwork, controller }).then(() => { + this.$artwork.value = artwork; + this.$controller.value = controller; + + this.effect(() => { + const track = this.currentTrack(); + untracked(() => this.#fetchArtwork(track)); + }); + }); + } + + /** + * @override + */ + disconnectedCallback() { + if (this.#prevUrl) { + URL.revokeObjectURL(this.#prevUrl); + this.#prevUrl = null; + } + } + + /** + * @param {ReturnType} track + */ + async #fetchArtwork(track) { + const bytes = track + ? ((await this.$artwork.value?.get(track)) ?? null) + : null; + + if (this.$controller.value?.$queue.value?.now()?.id !== track?.id) return; + + if (this.#prevUrl) { + URL.revokeObjectURL(this.#prevUrl); + this.#prevUrl = null; + } + + if (bytes) { + const mime = detectMime(bytes); + const url = URL.createObjectURL( + new Blob([/** @type {ArrayBuffer} */ (bytes.buffer)], { type: mime }), + ); + this.#prevUrl = url; + this.#artworkUrl.value = url; + } else { + this.#artworkUrl.value = null; + } + } + + /** + * @param {RenderArg} _ + */ + render({ html }) { + const url = this.#artworkUrl.value; + + return html` + + +
+ ${url + ? html`` + : html`No artwork`} +
+ `; + } +} + +export default ArtworkWindowElement; + +//////////////////////////////////////////// +// REGISTER +//////////////////////////////////////////// + +export const CLASS = ArtworkWindowElement; +export const NAME = "dtw-winamp-artwork"; + +defineElement(NAME, ArtworkWindowElement); + +/** + * @param {Uint8Array} bytes + * @returns {string} + */ +function detectMime(bytes) { + if (bytes[0] === 0xff && bytes[1] === 0xd8) return "image/jpeg"; + if (bytes[0] === 0x89 && bytes[1] === 0x50) return "image/png"; + if (bytes[0] === 0x47 && bytes[1] === 0x49) return "image/gif"; + if (bytes[0] === 0x52 && bytes[1] === 0x49) return "image/webp"; + return "image/jpeg"; +} diff --git a/src/facets/themes/winamp/facet/index.html b/src/facets/themes/winamp/facet/index.html index 8662d804..6b1c8ed9 100644 --- a/src/facets/themes/winamp/facet/index.html +++ b/src/facets/themes/winamp/facet/index.html @@ -82,6 +82,22 @@ > + + + + Artwork + + + + Browse collection + + + + + + diff --git a/src/facets/themes/winamp/facet/index.inline.js b/src/facets/themes/winamp/facet/index.inline.js index 09d38861..c1d36b6c 100644 --- a/src/facets/themes/winamp/facet/index.inline.js +++ b/src/facets/themes/winamp/facet/index.inline.js @@ -18,9 +18,11 @@ await foundation.orchestrator.sources(); await foundation.orchestrator.processTracks({ disableWhenReady: true }); await foundation.orchestrator.queueAudio(); await foundation.orchestrator.controller(); +await foundation.orchestrator.artwork(); await import("~/facets/themes/winamp/browser/element.js"); await import("~/facets/themes/winamp/window/element.js"); +await import("~/facets/themes/winamp/artwork/element.js"); const { default: WinampElement } = await import( "~/facets/themes/winamp/winamp/element.js" diff --git a/src/images/icons/windows_98/imagwmf-1.png b/src/images/icons/windows_98/imagwmf-1.png new file mode 100644 index 0000000000000000000000000000000000000000..515ee8a420ec41e117beed1ed6de9cffdc899027 GIT binary patch literal 503 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I7G?$phQ^Te;|vT8Ea{HEjtmSN`?>!lvVtU& zJ%W507^>757#dm_7=AG@Ff_biU???UV0e|lz+g3lfkC`r&aOZk1_lPX0G|+7kb;JW z|NsAk7z_;m|1%soaG;?9%wlK|5p*nMU|?V@3GxeOaCmkj4a7-E^Js@XzX?tnt6 zTF1LX|fQ7F0f)Ku(+S2s6b{|_c%kGcHKHt!eA zv`^=J6jbrz)5E`>Q`kFuYq=ORO8=NCMr=K3s#W34spx$$fm2Fw&&A|*HAlDU9dk~1 zn0~?Vfs@n&_ve53!^-CiDsarLKdBp9RNy-O2?GOzYKdz^NlIc#s#S7PYGO$$gOP!u zg|2~_u8~QIk+GGbv6X>|u7Qb_fkA7NQ7Vdt-29Zxv`X9>zTEG+$-uzC;OXk;vd$@? F2>{ULrL6z} literal 0 HcmV?d00001 diff --git a/src/images/icons/windows_98/wia_img_color_sound-0.png b/src/images/icons/windows_98/wia_img_color_sound-0.png new file mode 100644 index 0000000000000000000000000000000000000000..3f4db49fc7d1ecbf504d0a8c5da5bdbe2c1b5668 GIT binary patch literal 562 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I7G?$phQ^Te;|vT8Ea{HEjtmSN`?>!lvVtU& zJ%W507^>757#dm_7=AG@Ff_biU???UV0e|lz+g3lfkC`r&aOZk1_lQ80G|+7kb;H= z1_p-z|NkF2aDV~CZUAu@{xg7RkU#@NLsLlAH3kL-#*!evUYW?NMQuI!K4q!`Rqx_5|8XJBCX?djqeqH#VsK|z*d5tAZYVzS5smt_tbJSxtt zhYlaU>+R*`<(B{&r^Otyu-4>ZZ?A(pLG-o|3bMC2*oi z@Y&R>t3tIWt!+JUuRmf_R_Apsef{-MVzmUOGu`xVNGSd0BtN*HY+1ppQE^WGb zLQ+Ry)6%6!Z{I##{r!=vK%)uA3A2qxL1r#o3a?eW#Vr?dMM!cga!cgK?BQ79dW4m4 zN?Bu0pcB(Ah3E;4m)#OW7};_~-9s3c`7$vqZLHfk)oXh+0|SFRdP{k zVo554k%6Iwu7R1Zky(g=ft9JLm9d$wfr*uY!LPtoStuHE^HVa@DsgM*<`DwL3WKMs KpUXO@geCxqpTSH3 literal 0 HcmV?d00001 -- 2.51.2