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

Configure Feed

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

feat: queue facet

+384 -1
+7 -1
src/_data/facets.json
··· 31 31 "url": "facets/themes/blur/browser/facet/index.html", 32 32 "title": "Blur / Browser", 33 33 "category": "Browsing", 34 - "featured": true, 35 34 "desc": "Collection browser and search with favourite toggling, date grouping, and virtual scrolling." 36 35 }, 37 36 { ··· 167 166 "title": "Process Tracks", 168 167 "category": "Data", 169 168 "desc": "Process all your audio sources into tracks. Shows a progress bar when processing is occuring." 169 + }, 170 + { 171 + "url": "facets/playback/queue/index.html", 172 + "title": "Queue", 173 + "category": "Playback", 174 + "featured": true, 175 + "desc": "Manage your playback queue. Reorder upcoming tracks, clear the queue, and view playback history." 170 176 }, 171 177 { 172 178 "url": "facets/misc/scrobble/index.html",
+144
src/facets/playback/queue/index.html
··· 1 + <style> 2 + @import "./styles/base.css"; 3 + @import "./styles/diffuse/facet.css"; 4 + @import "./vendor/@phosphor-icons/web/fill/style.css"; 5 + @import "./vendor/@phosphor-icons/web/bold/style.css"; 6 + 7 + @layer base, diffuse; 8 + 9 + .queue-section { 10 + display: flex; 11 + flex-direction: column; 12 + gap: var(--space-xs); 13 + } 14 + 15 + .queue-section + .queue-section { 16 + margin-top: var(--space-lg); 17 + } 18 + 19 + .queue-section__heading { 20 + font-size: var(--fs-2xs); 21 + font-weight: 600; 22 + letter-spacing: var(--tracking-wider); 23 + opacity: 0.4; 24 + text-transform: uppercase; 25 + } 26 + 27 + .queue-list { 28 + display: flex; 29 + flex-direction: column; 30 + gap: var(--space-2xs); 31 + list-style: none; 32 + margin: 0; 33 + padding: 0; 34 + } 35 + 36 + .queue-item { 37 + align-items: center; 38 + display: flex; 39 + gap: var(--space-xs); 40 + } 41 + 42 + .queue-item--past { 43 + opacity: 0.35; 44 + } 45 + 46 + .queue-item__info { 47 + display: flex; 48 + flex-direction: column; 49 + flex: 1; 50 + gap: var(--space-3xs); 51 + min-width: 0; 52 + } 53 + 54 + .queue-item__title { 55 + font-weight: 600; 56 + overflow: hidden; 57 + text-overflow: ellipsis; 58 + white-space: nowrap; 59 + } 60 + 61 + .queue-item__title--now { 62 + color: var(--accent); 63 + } 64 + 65 + .queue-item__detail { 66 + color: oklch(from var(--text-color) l c h / 0.6); 67 + font-size: var(--fs-xs); 68 + overflow: hidden; 69 + text-overflow: ellipsis; 70 + white-space: nowrap; 71 + } 72 + 73 + .queue-item__badge { 74 + background: oklch(from var(--accent) l c h / 0.15); 75 + border-radius: var(--radius-sm); 76 + color: var(--accent); 77 + flex-shrink: 0; 78 + font-size: var(--fs-2xs); 79 + font-weight: 600; 80 + letter-spacing: var(--tracking-wider); 81 + padding: 2px var(--space-2xs); 82 + text-transform: uppercase; 83 + } 84 + 85 + .queue-item__reorder { 86 + display: flex; 87 + flex-direction: row; 88 + flex-shrink: 0; 89 + gap: 0; 90 + 91 + button { 92 + font-size: 75%; 93 + } 94 + } 95 + </style> 96 + 97 + <main> 98 + <div class="facet__left"> 99 + <div> 100 + <a href="./dashboard/" class="diffuse-logo-container"> 101 + <svg viewBox="0 0 902 134" width="160"> 102 + <title>Diffuse</title> 103 + <use 104 + xlink:href="images/diffuse-current.svg#diffuse" 105 + href="images/diffuse-current.svg#diffuse" 106 + ></use> 107 + </svg> 108 + </a> 109 + </div> 110 + <h1>Queue</h1> 111 + <p> 112 + Manage your playback queue. Reorder upcoming tracks or clear the queue. Tracks marked 113 + <strong>M</strong> were manually added and survive an auto-clear. 114 + </p> 115 + <p class="button-row" style="margin-top: var(--space-md)"> 116 + <button id="clear-all-btn"> 117 + <i class="ph-fill ph-trash"></i> 118 + Clear all 119 + </button> 120 + <button id="clear-auto-btn"> 121 + <i class="ph-fill ph-robot"></i> 122 + Clear auto 123 + </button> 124 + </p> 125 + </div> 126 + 127 + <div class="facet__right"> 128 + <div id="now-section" class="queue-section" hidden> 129 + <span class="queue-section__heading">Now Playing</span> 130 + <ul id="now-list" class="queue-list"></ul> 131 + </div> 132 + <div id="future-section" class="queue-section" hidden> 133 + <span class="queue-section__heading">Up Next</span> 134 + <ul id="future-list" class="queue-list"></ul> 135 + </div> 136 + <div id="past-section" class="queue-section" hidden> 137 + <span class="queue-section__heading">History</span> 138 + <ul id="past-list" class="queue-list"></ul> 139 + </div> 140 + <p id="queue-empty" class="caption">The queue is empty.</p> 141 + </div> 142 + </main> 143 + 144 + <script type="module" src="facets/playback/queue/index.inline.js"></script>
+212
src/facets/playback/queue/index.inline.js
··· 1 + import { html, render as litRender } from "lit-html"; 2 + import { keyed } from "~/vendor/lit-html/directives/keyed.js"; 3 + 4 + import foundation from "~/common/foundation.js"; 5 + import { effect } from "~/common/signal.js"; 6 + 7 + /** 8 + * @import { Track } from "~/definitions/types.d.ts" 9 + */ 10 + 11 + foundation.setup({ title: "Queue | Diffuse" }); 12 + 13 + //////////////////////////////////////////// 14 + // SETUP 15 + //////////////////////////////////////////// 16 + 17 + const [queue, outputOrchestrator] = await Promise.all([ 18 + foundation.engine.queue(), 19 + foundation.orchestrator.output(), 20 + ]); 21 + 22 + await Promise.all([ 23 + customElements.whenDefined(queue.localName), 24 + customElements.whenDefined(outputOrchestrator.localName), 25 + ]); 26 + 27 + //////////////////////////////////////////// 28 + // ELEMENTS 29 + //////////////////////////////////////////// 30 + 31 + const nowSection = 32 + /** @type {HTMLElement} */ (document.querySelector("#now-section")); 33 + const nowList = 34 + /** @type {HTMLElement} */ (document.querySelector("#now-list")); 35 + const futureSection = 36 + /** @type {HTMLElement} */ (document.querySelector("#future-section")); 37 + const futureList = 38 + /** @type {HTMLElement} */ (document.querySelector("#future-list")); 39 + const pastSection = 40 + /** @type {HTMLElement} */ (document.querySelector("#past-section")); 41 + const pastList = 42 + /** @type {HTMLElement} */ (document.querySelector("#past-list")); 43 + const queueEmpty = 44 + /** @type {HTMLElement} */ (document.querySelector("#queue-empty")); 45 + 46 + //////////////////////////////////////////// 47 + // HELPERS 48 + //////////////////////////////////////////// 49 + 50 + /** 51 + * @param {string} id 52 + * @param {Track[]} tracks 53 + */ 54 + const findTrack = (id, tracks) => tracks.find((t) => t.id === id); 55 + 56 + /** 57 + * @param {Track | undefined} track 58 + * @param {string} fallbackId 59 + */ 60 + const trackTitle = (track, fallbackId) => track?.tags?.title ?? fallbackId; 61 + 62 + /** 63 + * @param {Track | undefined} track 64 + */ 65 + const trackArtist = (track) => 66 + track?.tags?.artist ?? track?.tags?.albumartist ?? null; 67 + 68 + //////////////////////////////////////////// 69 + // RENDER 70 + //////////////////////////////////////////// 71 + 72 + effect(() => { 73 + const now = queue.now(); 74 + const past = queue.past(); 75 + const future = queue.future(); 76 + 77 + const tracksCol = outputOrchestrator.tracks.collection(); 78 + const tracks = tracksCol.state === "loaded" ? tracksCol.data : []; 79 + 80 + const hasAnything = now !== null || past.length > 0 || future.length > 0; 81 + queueEmpty.hidden = hasAnything; 82 + 83 + // Now playing 84 + nowSection.hidden = now === null; 85 + if (now !== null) { 86 + const track = findTrack(now.id, tracks); 87 + litRender( 88 + html` 89 + <li class="queue-item"> 90 + <div class="queue-item__info"> 91 + <span class="queue-item__title queue-item__title--now">${trackTitle( 92 + track, 93 + now.id, 94 + )}</span> 95 + ${trackArtist(track) 96 + ? html` 97 + <span class="queue-item__detail">${trackArtist(track)}</span> 98 + ` 99 + : null} 100 + </div> 101 + </li> 102 + `, 103 + nowList, 104 + ); 105 + } 106 + 107 + // Up next — move() operates on the flat [past..., now, future...] list 108 + futureSection.hidden = future.length === 0; 109 + if (future.length > 0) { 110 + const offset = past.length + (now !== null ? 1 : 0); 111 + 112 + litRender( 113 + html` 114 + ${future.map((item, i) => { 115 + const track = findTrack(item.id, tracks); 116 + return keyed(item.id, html` 117 + <li class="queue-item"> 118 + <div class="queue-item__info"> 119 + <span class="queue-item__title">${trackTitle( 120 + track, 121 + item.id, 122 + )}</span> 123 + ${trackArtist(track) 124 + ? html` 125 + <span class="queue-item__detail">${trackArtist( 126 + track, 127 + )}</span> 128 + ` 129 + : null} 130 + </div> 131 + ${item.manualEntry 132 + ? html` 133 + <span class="queue-item__badge" title="Manually queued">M</span> 134 + ` 135 + : null} 136 + <div class="queue-item__reorder"> 137 + <button 138 + class="button--plain button--icon" 139 + title="Move up" 140 + ?disabled="${i === 0}" 141 + @click="${() => 142 + queue.move({ from: offset + i, to: offset + i - 1 })}" 143 + > 144 + <i class="ph-bold ph-arrow-up"></i> 145 + </button> 146 + <button 147 + class="button--plain button--icon" 148 + title="Move down" 149 + ?disabled="${i === future.length - 1}" 150 + @click="${() => 151 + queue.move({ from: offset + i, to: offset + i + 1 })}" 152 + > 153 + <i class="ph-bold ph-arrow-down"></i> 154 + </button> 155 + </div> 156 + </li> 157 + `); 158 + })} 159 + `, 160 + futureList, 161 + ); 162 + } 163 + 164 + // History — most recent first 165 + pastSection.hidden = past.length === 0; 166 + if (past.length > 0) { 167 + const reversed = [...past].reverse(); 168 + litRender( 169 + html` 170 + ${reversed.map((item) => { 171 + const track = findTrack(item.id, tracks); 172 + return keyed(item.id, html` 173 + <li class="queue-item queue-item--past"> 174 + <div class="queue-item__info"> 175 + <span class="queue-item__title">${trackTitle( 176 + track, 177 + item.id, 178 + )}</span> 179 + ${trackArtist(track) 180 + ? html` 181 + <span class="queue-item__detail">${trackArtist( 182 + track, 183 + )}</span> 184 + ` 185 + : null} 186 + </div> 187 + </li> 188 + `); 189 + })} 190 + `, 191 + pastList, 192 + ); 193 + } 194 + }); 195 + 196 + //////////////////////////////////////////// 197 + // ACTIONS 198 + //////////////////////////////////////////// 199 + 200 + document.querySelector("#clear-all-btn")?.addEventListener("click", () => { 201 + queue.clear({ keepManual: false }); 202 + }); 203 + 204 + document.querySelector("#clear-auto-btn")?.addEventListener("click", () => { 205 + queue.clear({ keepManual: true }); 206 + }); 207 + 208 + //////////////////////////////////////////// 209 + // 🚀 210 + //////////////////////////////////////////// 211 + 212 + foundation.ready();
+1
src/vendor/lit-html/directives/async-append.js
··· 1 + export * from "lit-html/directives/async-append.js";
+1
src/vendor/lit-html/directives/async-replace.js
··· 1 + export * from "lit-html/directives/async-replace.js";
+1
src/vendor/lit-html/directives/cache.js
··· 1 + export * from "lit-html/directives/cache.js";
+1
src/vendor/lit-html/directives/choose.js
··· 1 + export * from "lit-html/directives/choose.js";
+1
src/vendor/lit-html/directives/class-map.js
··· 1 + export * from "lit-html/directives/class-map.js";
+1
src/vendor/lit-html/directives/guard.js
··· 1 + export * from "lit-html/directives/guard.js";
+1
src/vendor/lit-html/directives/if-defined.js
··· 1 + export * from "lit-html/directives/if-defined.js";
+1
src/vendor/lit-html/directives/join.js
··· 1 + export * from "lit-html/directives/join.js";
+1
src/vendor/lit-html/directives/keyed.js
··· 1 + export * from "lit-html/directives/keyed.js";
+1
src/vendor/lit-html/directives/live.js
··· 1 + export * from "lit-html/directives/live.js";
+1
src/vendor/lit-html/directives/map.js
··· 1 + export * from "lit-html/directives/map.js";
+1
src/vendor/lit-html/directives/range.js
··· 1 + export * from "lit-html/directives/range.js";
+1
src/vendor/lit-html/directives/ref.js
··· 1 + export * from "lit-html/directives/ref.js";
+1
src/vendor/lit-html/directives/repeat.js
··· 1 + export * from "lit-html/directives/repeat.js";
+1
src/vendor/lit-html/directives/style-map.js
··· 1 + export * from "lit-html/directives/style-map.js";
+1
src/vendor/lit-html/directives/template-content.js
··· 1 + export * from "lit-html/directives/template-content.js";
+1
src/vendor/lit-html/directives/unsafe-html.js
··· 1 + export * from "lit-html/directives/unsafe-html.js";
+1
src/vendor/lit-html/directives/unsafe-mathml.js
··· 1 + export * from "lit-html/directives/unsafe-mathml.js";
+1
src/vendor/lit-html/directives/unsafe-svg.js
··· 1 + export * from "lit-html/directives/unsafe-svg.js";
+1
src/vendor/lit-html/directives/until.js
··· 1 + export * from "lit-html/directives/until.js";
+1
src/vendor/lit-html/directives/when.js
··· 1 + export * from "lit-html/directives/when.js";