A music player that connects to your cloud/distributed storage.
1import foundation from "~/common/foundation.js";
2import { effect } from "~/common/signal.js";
3import * as Playlist from "~/common/playlist.js";
4
5// Set doc title
6document.title = "Automatic Queue | Diffuse";
7
8const ACTIVE_CLASS = "button--active";
9
10// Setup
11await foundation.orchestrator.autoQueue();
12
13const [repeatShuffle, scope, output] = await Promise.all([
14 foundation.engine.repeatShuffle(),
15 foundation.engine.scope(),
16 foundation.orchestrator.output(),
17]);
18
19// Elements
20const repeatBtn =
21 /** @type {HTMLButtonElement} */ (document.querySelector("#repeat"));
22const shuffleBtn =
23 /** @type {HTMLButtonElement} */ (document.querySelector("#shuffle"));
24const searchInput =
25 /** @type {HTMLInputElement} */ (document.querySelector("#search"));
26const playlistSelect =
27 /** @type {HTMLSelectElement} */ (document.querySelector("#playlist"));
28const sortBySelect =
29 /** @type {HTMLSelectElement} */ (document.querySelector("#sort-by"));
30const sortDirectionBtn =
31 /** @type {HTMLButtonElement} */ (document.querySelector("#sort-direction"));
32
33// Repeat & Shuffle state
34effect(() => {
35 repeatBtn.classList.toggle(ACTIVE_CLASS, repeatShuffle.repeat());
36});
37
38effect(() => {
39 shuffleBtn.classList.toggle(ACTIVE_CLASS, repeatShuffle.shuffle());
40});
41
42// Actions
43repeatBtn.onclick = () => {
44 repeatShuffle.setRepeat(!repeatShuffle.repeat());
45};
46
47shuffleBtn.onclick = () => {
48 repeatShuffle.setShuffle(!repeatShuffle.shuffle());
49};
50
51// Search state
52effect(() => {
53 searchInput.value = scope.searchTerm() ?? "";
54});
55
56searchInput.oninput = () => {
57 scope.setSearchTerm(searchInput.value.trim() || undefined);
58};
59
60// Playlist state
61effect(() => {
62 const col = output.playlistItems.collection();
63 const items = col.state === "loaded" ? col.data : [];
64 const currentPlaylist = scope.playlist();
65
66 // Group items by playlist name
67 const playlistMap = Playlist.gather(items);
68 const all = [...playlistMap.values()].sort((a, b) =>
69 a.name.localeCompare(b.name)
70 );
71
72 const ordered = all.filter((p) => !p.unordered);
73 const unordered = all.filter((p) => p.unordered);
74
75 playlistSelect.innerHTML = `<option value="">All tracks</option>`;
76
77 for (
78 const [label, group] of [
79 ["Ordered", ordered],
80 ["Unordered", unordered],
81 ]
82 ) {
83 if (group.length === 0) continue;
84
85 const optgroup = document.createElement("optgroup");
86 optgroup.label = /** @type {string} */ (label);
87
88 for (const playlist of group) {
89 if (typeof playlist === "string") continue;
90 const option = document.createElement("option");
91
92 option.value = playlist.name;
93 option.textContent = playlist.name;
94 option.selected = playlist.name === currentPlaylist;
95
96 optgroup.appendChild(option);
97 }
98
99 playlistSelect.appendChild(optgroup);
100 }
101});
102
103playlistSelect.onchange = () => {
104 scope.setPlaylist(
105 playlistSelect.value.length ? playlistSelect.value : undefined,
106 );
107};
108
109// Sort by state
110effect(() => {
111 const current = JSON.stringify(scope.sortBy());
112 for (const option of sortBySelect.options) {
113 if (JSON.stringify(JSON.parse(option.value)) === current) {
114 sortBySelect.value = option.value;
115 break;
116 }
117 }
118});
119
120sortBySelect.onchange = () => {
121 scope.setSortBy(JSON.parse(sortBySelect.value));
122};
123
124// Sort direction state
125effect(() => {
126 const dir = scope.sortDirection() ?? "desc";
127 sortDirectionBtn.textContent = dir.toUpperCase();
128});
129
130sortDirectionBtn.onclick = () => {
131 const dir = scope.sortDirection() ?? "desc";
132 scope.setSortDirection(dir === "asc" ? "desc" : "asc");
133};