diff --git a/src/_data/facets.json b/src/_data/facets.json
index 90d6eb30..8b919a1e 100644
--- a/src/_data/facets.json
+++ b/src/_data/facets.json
@@ -31,7 +31,6 @@
"url": "facets/themes/blur/browser/facet/index.html",
"title": "Blur / Browser",
"category": "Browsing",
- "featured": true,
"desc": "Collection browser and search with favourite toggling, date grouping, and virtual scrolling."
},
{
@@ -168,6 +167,13 @@
"category": "Data",
"desc": "Process all your audio sources into tracks. Shows a progress bar when processing is occuring."
},
+ {
+ "url": "facets/playback/queue/index.html",
+ "title": "Queue",
+ "category": "Playback",
+ "featured": true,
+ "desc": "Manage your playback queue. Reorder upcoming tracks, clear the queue, and view playback history."
+ },
{
"url": "facets/misc/scrobble/index.html",
"title": "Scrobble",
diff --git a/src/facets/playback/queue/index.html b/src/facets/playback/queue/index.html
new file mode 100644
index 00000000..d241183f
--- /dev/null
+++ b/src/facets/playback/queue/index.html
@@ -0,0 +1,144 @@
+
+
+
+
+
+
Queue
+
+ Manage your playback queue. Reorder upcoming tracks or clear the queue. Tracks marked
+ M were manually added and survive an auto-clear.
+
+
+
+
+
+
+
+
+
+
+
+
The queue is empty.
+
+
+
+
diff --git a/src/facets/playback/queue/index.inline.js b/src/facets/playback/queue/index.inline.js
new file mode 100644
index 00000000..c191bb31
--- /dev/null
+++ b/src/facets/playback/queue/index.inline.js
@@ -0,0 +1,212 @@
+import { html, render as litRender } from "lit-html";
+import { keyed } from "~/vendor/lit-html/directives/keyed.js";
+
+import foundation from "~/common/foundation.js";
+import { effect } from "~/common/signal.js";
+
+/**
+ * @import { Track } from "~/definitions/types.d.ts"
+ */
+
+foundation.setup({ title: "Queue | Diffuse" });
+
+////////////////////////////////////////////
+// SETUP
+////////////////////////////////////////////
+
+const [queue, outputOrchestrator] = await Promise.all([
+ foundation.engine.queue(),
+ foundation.orchestrator.output(),
+]);
+
+await Promise.all([
+ customElements.whenDefined(queue.localName),
+ customElements.whenDefined(outputOrchestrator.localName),
+]);
+
+////////////////////////////////////////////
+// ELEMENTS
+////////////////////////////////////////////
+
+const nowSection =
+ /** @type {HTMLElement} */ (document.querySelector("#now-section"));
+const nowList =
+ /** @type {HTMLElement} */ (document.querySelector("#now-list"));
+const futureSection =
+ /** @type {HTMLElement} */ (document.querySelector("#future-section"));
+const futureList =
+ /** @type {HTMLElement} */ (document.querySelector("#future-list"));
+const pastSection =
+ /** @type {HTMLElement} */ (document.querySelector("#past-section"));
+const pastList =
+ /** @type {HTMLElement} */ (document.querySelector("#past-list"));
+const queueEmpty =
+ /** @type {HTMLElement} */ (document.querySelector("#queue-empty"));
+
+////////////////////////////////////////////
+// HELPERS
+////////////////////////////////////////////
+
+/**
+ * @param {string} id
+ * @param {Track[]} tracks
+ */
+const findTrack = (id, tracks) => tracks.find((t) => t.id === id);
+
+/**
+ * @param {Track | undefined} track
+ * @param {string} fallbackId
+ */
+const trackTitle = (track, fallbackId) => track?.tags?.title ?? fallbackId;
+
+/**
+ * @param {Track | undefined} track
+ */
+const trackArtist = (track) =>
+ track?.tags?.artist ?? track?.tags?.albumartist ?? null;
+
+////////////////////////////////////////////
+// RENDER
+////////////////////////////////////////////
+
+effect(() => {
+ const now = queue.now();
+ const past = queue.past();
+ const future = queue.future();
+
+ const tracksCol = outputOrchestrator.tracks.collection();
+ const tracks = tracksCol.state === "loaded" ? tracksCol.data : [];
+
+ const hasAnything = now !== null || past.length > 0 || future.length > 0;
+ queueEmpty.hidden = hasAnything;
+
+ // Now playing
+ nowSection.hidden = now === null;
+ if (now !== null) {
+ const track = findTrack(now.id, tracks);
+ litRender(
+ html`
+
+
+ ${trackTitle(
+ track,
+ now.id,
+ )}
+ ${trackArtist(track)
+ ? html`
+ ${trackArtist(track)}
+ `
+ : null}
+
+
+ `,
+ nowList,
+ );
+ }
+
+ // Up next — move() operates on the flat [past..., now, future...] list
+ futureSection.hidden = future.length === 0;
+ if (future.length > 0) {
+ const offset = past.length + (now !== null ? 1 : 0);
+
+ litRender(
+ html`
+ ${future.map((item, i) => {
+ const track = findTrack(item.id, tracks);
+ return keyed(item.id, html`
+
+
+ ${trackTitle(
+ track,
+ item.id,
+ )}
+ ${trackArtist(track)
+ ? html`
+ ${trackArtist(
+ track,
+ )}
+ `
+ : null}
+
+ ${item.manualEntry
+ ? html`
+ M
+ `
+ : null}
+
+
+
+
+
+ `);
+ })}
+ `,
+ futureList,
+ );
+ }
+
+ // History — most recent first
+ pastSection.hidden = past.length === 0;
+ if (past.length > 0) {
+ const reversed = [...past].reverse();
+ litRender(
+ html`
+ ${reversed.map((item) => {
+ const track = findTrack(item.id, tracks);
+ return keyed(item.id, html`
+
+
+ ${trackTitle(
+ track,
+ item.id,
+ )}
+ ${trackArtist(track)
+ ? html`
+ ${trackArtist(
+ track,
+ )}
+ `
+ : null}
+
+
+ `);
+ })}
+ `,
+ pastList,
+ );
+ }
+});
+
+////////////////////////////////////////////
+// ACTIONS
+////////////////////////////////////////////
+
+document.querySelector("#clear-all-btn")?.addEventListener("click", () => {
+ queue.clear({ keepManual: false });
+});
+
+document.querySelector("#clear-auto-btn")?.addEventListener("click", () => {
+ queue.clear({ keepManual: true });
+});
+
+////////////////////////////////////////////
+// 🚀
+////////////////////////////////////////////
+
+foundation.ready();
diff --git a/src/vendor/lit-html/directives/async-append.js b/src/vendor/lit-html/directives/async-append.js
new file mode 100644
index 00000000..c8381209
--- /dev/null
+++ b/src/vendor/lit-html/directives/async-append.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/async-append.js";
diff --git a/src/vendor/lit-html/directives/async-replace.js b/src/vendor/lit-html/directives/async-replace.js
new file mode 100644
index 00000000..df5023c4
--- /dev/null
+++ b/src/vendor/lit-html/directives/async-replace.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/async-replace.js";
diff --git a/src/vendor/lit-html/directives/cache.js b/src/vendor/lit-html/directives/cache.js
new file mode 100644
index 00000000..e9ecf214
--- /dev/null
+++ b/src/vendor/lit-html/directives/cache.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/cache.js";
diff --git a/src/vendor/lit-html/directives/choose.js b/src/vendor/lit-html/directives/choose.js
new file mode 100644
index 00000000..851c5593
--- /dev/null
+++ b/src/vendor/lit-html/directives/choose.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/choose.js";
diff --git a/src/vendor/lit-html/directives/class-map.js b/src/vendor/lit-html/directives/class-map.js
new file mode 100644
index 00000000..cdf94496
--- /dev/null
+++ b/src/vendor/lit-html/directives/class-map.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/class-map.js";
diff --git a/src/vendor/lit-html/directives/guard.js b/src/vendor/lit-html/directives/guard.js
new file mode 100644
index 00000000..c76f8339
--- /dev/null
+++ b/src/vendor/lit-html/directives/guard.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/guard.js";
diff --git a/src/vendor/lit-html/directives/if-defined.js b/src/vendor/lit-html/directives/if-defined.js
new file mode 100644
index 00000000..573c79d1
--- /dev/null
+++ b/src/vendor/lit-html/directives/if-defined.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/if-defined.js";
diff --git a/src/vendor/lit-html/directives/join.js b/src/vendor/lit-html/directives/join.js
new file mode 100644
index 00000000..14576649
--- /dev/null
+++ b/src/vendor/lit-html/directives/join.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/join.js";
diff --git a/src/vendor/lit-html/directives/keyed.js b/src/vendor/lit-html/directives/keyed.js
new file mode 100644
index 00000000..a12bd32e
--- /dev/null
+++ b/src/vendor/lit-html/directives/keyed.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/keyed.js";
diff --git a/src/vendor/lit-html/directives/live.js b/src/vendor/lit-html/directives/live.js
new file mode 100644
index 00000000..a9f7a00a
--- /dev/null
+++ b/src/vendor/lit-html/directives/live.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/live.js";
diff --git a/src/vendor/lit-html/directives/map.js b/src/vendor/lit-html/directives/map.js
new file mode 100644
index 00000000..741a827b
--- /dev/null
+++ b/src/vendor/lit-html/directives/map.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/map.js";
diff --git a/src/vendor/lit-html/directives/range.js b/src/vendor/lit-html/directives/range.js
new file mode 100644
index 00000000..1231a3f4
--- /dev/null
+++ b/src/vendor/lit-html/directives/range.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/range.js";
diff --git a/src/vendor/lit-html/directives/ref.js b/src/vendor/lit-html/directives/ref.js
new file mode 100644
index 00000000..010c5c03
--- /dev/null
+++ b/src/vendor/lit-html/directives/ref.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/ref.js";
diff --git a/src/vendor/lit-html/directives/repeat.js b/src/vendor/lit-html/directives/repeat.js
new file mode 100644
index 00000000..f9444dd6
--- /dev/null
+++ b/src/vendor/lit-html/directives/repeat.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/repeat.js";
diff --git a/src/vendor/lit-html/directives/style-map.js b/src/vendor/lit-html/directives/style-map.js
new file mode 100644
index 00000000..f9577281
--- /dev/null
+++ b/src/vendor/lit-html/directives/style-map.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/style-map.js";
diff --git a/src/vendor/lit-html/directives/template-content.js b/src/vendor/lit-html/directives/template-content.js
new file mode 100644
index 00000000..3a0e33c6
--- /dev/null
+++ b/src/vendor/lit-html/directives/template-content.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/template-content.js";
diff --git a/src/vendor/lit-html/directives/unsafe-html.js b/src/vendor/lit-html/directives/unsafe-html.js
new file mode 100644
index 00000000..e76e4b05
--- /dev/null
+++ b/src/vendor/lit-html/directives/unsafe-html.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/unsafe-html.js";
diff --git a/src/vendor/lit-html/directives/unsafe-mathml.js b/src/vendor/lit-html/directives/unsafe-mathml.js
new file mode 100644
index 00000000..20176fec
--- /dev/null
+++ b/src/vendor/lit-html/directives/unsafe-mathml.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/unsafe-mathml.js";
diff --git a/src/vendor/lit-html/directives/unsafe-svg.js b/src/vendor/lit-html/directives/unsafe-svg.js
new file mode 100644
index 00000000..839a9fdf
--- /dev/null
+++ b/src/vendor/lit-html/directives/unsafe-svg.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/unsafe-svg.js";
diff --git a/src/vendor/lit-html/directives/until.js b/src/vendor/lit-html/directives/until.js
new file mode 100644
index 00000000..f870b90b
--- /dev/null
+++ b/src/vendor/lit-html/directives/until.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/until.js";
diff --git a/src/vendor/lit-html/directives/when.js b/src/vendor/lit-html/directives/when.js
new file mode 100644
index 00000000..1b7d4e4a
--- /dev/null
+++ b/src/vendor/lit-html/directives/when.js
@@ -0,0 +1 @@
+export * from "lit-html/directives/when.js";