A music player that connects to your cloud/distributed storage. diffuse.sh
Something went wrong. Try again.
8.4 kB · 290 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291import { defineElement, DiffuseElement, query, whenElementsDefined,} from "~/common/element.js";
import { signal } from "~/common/signal.js";
/** * @import {RenderArg} from "~/common/element.d.ts" * * @import ControllerOrchestrator from "~/components/orchestrator/controller/element.js" * @import RepeatShuffleEngine from "~/components/engine/repeat-shuffle/element.js" */
/** * Classic audio controller — a faithful recreation of the classic Diffuse * console (commit 01ea0c8472b67187d03669327b353d9625c04ef0). * * Renders a now-playing label, a thin progress bar, and a row of transport * buttons with light indicators above each one. No artwork, no volume slider, * no favourites — just the essentials, exactly like the original. */class ClassicController extends DiffuseElement { static observedAttributes = ["group-label"];
constructor() { super(); this.attachShadow({ mode: "open" }); }
// VARIABLES
/** @type {ReturnType<typeof setTimeout> | undefined} */ #isLoadingTimeout = undefined;
// SIGNALS
#audioError = signal(false); #isLoading = signal(true);
// SIGNALS - DEPENDENCIES
$controller = signal( /** @type {ControllerOrchestrator | undefined} */ (undefined), ); $repeatShuffle = signal( /** @type {RepeatShuffleEngine | undefined} */ (undefined), );
// SIGNALS - COMPUTED
audio = () => this.$controller.value?.audio(); currentTrack = () => this.$controller.value?.currentTrack(); isPlaying = () => this.$controller.value?.isPlaying();
// LIFECYCLE
/** * @override */ connectedCallback() { super.connectedCallback();
/** @type {ControllerOrchestrator} */ const controller = query(this, "controller-orchestrator-selector");
/** @type {RepeatShuffleEngine} */ const repeatShuffle = query(this, "repeat-shuffle-engine-selector");
whenElementsDefined({ controller, repeatShuffle, }) .then( () => { this.$controller.value = controller; this.$repeatShuffle.value = repeatShuffle;
this.effect(() => { const now = !!this.$controller.value?.$queue.value?.now(); const aud = this.audio()?.loadingState(); const isError = now && typeof aud === "object" && aud !== null && "error" in aud; const isLoading = now && !isError && aud !== "loaded";
this.#audioError.value = isError;
if (this.#isLoadingTimeout) { clearTimeout(this.#isLoadingTimeout); }
if (isLoading) { this.#isLoadingTimeout = setTimeout( () => this.#isLoading.value = true, 2000, ); } else { this.#isLoading.value = false; } }); }, ); }
// EVENTS
reload = () => { const audioId = this.$controller.value?.$queue.value?.now()?.id; if (audioId) { const progress = this.audio()?.progress(); this.$controller.value?.$audio.value?.reload({ audioId, play: true, progress, }); } };
next = () => { this.$controller.value?.$queue.value?.shift(); };
playPause = () => { const audioId = this.$controller.value?.$queue.value?.now()?.id;
if (this.isPlaying() && audioId) { this.$controller.value?.$audio.value?.pause({ audioId }); } else if (audioId) { this.$controller.value?.$audio.value?.play({ audioId }); } };
previous = () => { this.$controller.value?.$queue.value?.unshift(); };
/** * @param {MouseEvent} event */ seek = (event) => { const target = event.currentTarget ? /** @type {HTMLElement} */ (event.currentTarget) : null; const percentage = target ? event.offsetX / target.clientWidth : 0; const audioId = this.$controller.value?.$queue.value?.now()?.id;
if (audioId) { this.$controller.value?.$audio.value?.seek({ audioId, percentage }); } };
toggleRepeat = () => { const rs = this.$repeatShuffle.value; if (rs) rs.setRepeat(!rs.repeat()); };
toggleShuffle = () => { const rs = this.$repeatShuffle.value; if (rs) rs.setShuffle(!rs.shuffle()); };
// RENDER
/** * @param {RenderArg} _ */ render({ html }) { const track = this.currentTrack(); const isRepeat = this.$repeatShuffle.value?.repeat() ?? false; const isShuffle = this.$repeatShuffle.value?.shuffle() ?? false; const playing = this.isPlaying();
// Now playing text let nowPlayingText = "Diffuse"; if (this.#audioError.value) { nowPlayingText = "(!) An error occurred while decoding the audio"; } else if (this.#isLoading.value) { nowPlayingText = "Loading track ..."; } else if (track) { const artist = track.tags?.artist; const title = track.tags?.title ?? ""; nowPlayingText = artist ? `${artist} - ${title}` : title; }
const progress = (this.audio()?.progress() ?? 0) * 100;
return html` <link rel="stylesheet" href="vendor/@phosphor-icons/web/bold/style.css" /> <link rel="stylesheet" href="vendor/@phosphor-icons/web/fill/style.css" /> <link rel="stylesheet" href="facets/themes/blur-classic/controller/element.css" />
<main> <!-- NOW PLAYING --> <p class="now-playing" title="${nowPlayingText}">${nowPlayingText}</p>
<!-- PROGRESS BAR --> <div class="progress" @click="${this.seek}"> <div class="progress__track"> <div class="progress__fill" style="width: ${progress}%;"></div> </div> </div>
<!-- CONTROLS --> <div class="controls"> <!-- repeat --> <button class="control-btn" title="Toggle repeat" @click="${this .toggleRepeat}"> <span class="light light--small ${isRepeat ? "light--on-blue" : ""}"></span> <span class="control-btn__icon"> <i class="ph-bold ph-repeat"></i> </span> </button>
<!-- previous --> <button class="control-btn" title="Play previous track" @click="${this.previous}"> <span class="light light--placeholder"></span> <span class="control-btn__icon"> <i class="ph-fill ph-rewind"></i> </span> </button>
<!-- error --> <button class="control-btn" style="display: ${this.#audioError.value ? "flex" : "none"};" title="Reload" @click="${this.reload}" > <span class="light light--placeholder"></span> <span class="control-btn__icon"> <i class="ph-fill ph-warning-circle"></i> </span> </button>
<!-- play / pause --> <button class="control-btn" title="${playing ? "Pause" : "Play"}" @click="${this.playPause}" style="display: ${this.#audioError.value ? "none" : "flex"};" > <span class="light light--large ${playing ? "light--on-green" : ""}"></span> <span class="control-btn__icon control-btn__icon--play"> <span class="play-text">PLAY</span> </span> </button>
<!-- next --> <button class="control-btn" title="Play next track" @click="${this .next}"> <span class="light light--placeholder"></span> <span class="control-btn__icon"> <i class="ph-fill ph-fast-forward"></i> </span> </button>
<!-- shuffle --> <button class="control-btn" title="Toggle shuffle" @click="${this .toggleShuffle}"> <span class="light light--small ${isShuffle ? "light--on-blue" : ""}"></span> <span class="control-btn__icon"> <i class="ph-bold ph-shuffle"></i> </span> </button> </div> </main> `; }}
export default ClassicController;
////////////////////////////////////////////// REGISTER////////////////////////////////////////////
export const CLASS = ClassicController;export const NAME = "db-blur-classic-controller";
defineElement(NAME, CLASS);