1 parent
6c76121863
commit
6bee9bd9a3
13 files changed
+230
-5
No files matched your search
@@ -127,6 +127,10 @@ els('.card[data-room]').forEach((node) => {
|
||||
artist: el('[data-role="artist"]', node),
|
||||
progress: el('[data-role="progress"]', node),
|
||||
steps: els('.step', node),
|
||||
queue: el('[data-role="queue"]', node),
|
||||
queueList: el('[data-role="queue-list"]', node),
|
||||
queueFor: null, // the song the queue on show was fetched behind
|
||||
queueLoading: false,
|
||||
volume: null,
|
||||
playState: null,
|
||||
track: null, // {song, artist, image} while something is loaded
|
||||
@@ -162,6 +166,9 @@ els('.card[data-room]').forEach((node) => {
|
||||
// the next poll does not try it again until the track changes.
|
||||
room.cover.addEventListener('error', () => { room.cover.hidden = true; });
|
||||
|
||||
// Asked for afresh on every unfold: songs queued from the Spotify app in
|
||||
// the meantime would not otherwise turn up until the track changed.
|
||||
if (room.queue) room.queue.addEventListener('toggle', () => { if (room.queue.open) loadQueue(room); });
|
||||
if (room.toggle) room.toggle.addEventListener('click', () => setGrouped(key, !room.grouped));
|
||||
if (room.prev) room.prev.addEventListener('click', () => skipTrack(key, 'previous'));
|
||||
room.play.addEventListener('click', () => togglePlay(key));
|
||||
@@ -226,9 +233,93 @@ function paintRoom(room) {
|
||||
paintVolume(room);
|
||||
paintTransport(room);
|
||||
paintSources(room);
|
||||
paintQueue(room);
|
||||
placeRoom(room);
|
||||
}
|
||||
|
||||
/* --- Spotify queue: what comes next, folded at the foot of a room's card.
|
||||
It costs a Spotify call, so it is only fetched while unfolded -- on the
|
||||
unfold, and again whenever the song changes under it. ------------------ */
|
||||
function queueKey(room) {
|
||||
return room.track ? `${room.spotifyAccount}|${room.track.song}|${room.track.artist}` : room.spotifyAccount;
|
||||
}
|
||||
|
||||
function paintQueue(room) {
|
||||
if (!room.queue) return;
|
||||
// Shown for the same stream the transport buttons are: one of your accounts'.
|
||||
const shown = room.available && room.onSpotify && Boolean(room.spotifyAccount) && !room.grouped;
|
||||
room.queue.hidden = !shown;
|
||||
if (!shown) {
|
||||
// Folded again for next time, rather than reopening on a stale list.
|
||||
room.queue.open = false;
|
||||
room.queueFor = null;
|
||||
return;
|
||||
}
|
||||
if (room.queue.open && room.queueFor !== queueKey(room)) loadQueue(room);
|
||||
}
|
||||
|
||||
async function loadQueue(room) {
|
||||
if (room.queueLoading) return;
|
||||
room.queueLoading = true;
|
||||
const wanted = queueKey(room);
|
||||
room.queueFor = wanted;
|
||||
// Only an empty box says it is loading; a list already there stays put
|
||||
// until the new one replaces it.
|
||||
if (!room.queueList.children.length) renderQueueLoading(room);
|
||||
try {
|
||||
const data = await api(`/api/spotify/queue?target=${encodeURIComponent(room.key)}`);
|
||||
renderQueue(room, data.queue || []);
|
||||
} catch (error) {
|
||||
renderQueueNote(room, error.message);
|
||||
room.queueFor = null; // so the next poll tries again
|
||||
} finally {
|
||||
room.queueLoading = false;
|
||||
}
|
||||
// The song moved on while this was in flight.
|
||||
if (room.queueFor !== null && room.queueFor !== queueKey(room)) paintQueue(room);
|
||||
}
|
||||
|
||||
function renderQueueNote(room, text) {
|
||||
const note = document.createElement('li');
|
||||
note.className = 'queue-note';
|
||||
note.textContent = text;
|
||||
room.queueList.replaceChildren(note);
|
||||
}
|
||||
|
||||
/* Font Awesome's ellipsis, one path per dot so panel.css can blink them in turn. */
|
||||
function renderQueueLoading(room) {
|
||||
const note = document.createElement('li');
|
||||
note.className = 'queue-note';
|
||||
note.setAttribute('aria-label', 'Loading');
|
||||
note.innerHTML = '<svg class="queue-loading" viewBox="0 0 448 512" aria-hidden="true">'
|
||||
+ '<path d="M0 256a56 56 0 1 1 112 0 56 56 0 1 1 -112 0z"/>'
|
||||
+ '<path d="M168 256a56 56 0 1 1 112 0 56 56 0 1 1 -112 0z"/>'
|
||||
+ '<path d="M336 256a56 56 0 1 1 112 0 56 56 0 1 1 -112 0z"/></svg>';
|
||||
room.queueList.replaceChildren(note);
|
||||
}
|
||||
|
||||
function renderQueue(room, items) {
|
||||
if (!items.length) {
|
||||
renderQueueNote(room, 'Nothing queued');
|
||||
return;
|
||||
}
|
||||
room.queueList.replaceChildren(...items.map((item) => {
|
||||
const row = document.createElement('li');
|
||||
row.className = 'queue-item';
|
||||
row.innerHTML = '<img class="cover" alt="" loading="lazy"><div class="track"><span class="song"></span><span class="artist"></span></div><span class="queue-time"></span>';
|
||||
const cover = el('.cover', row);
|
||||
if (item.image) {
|
||||
cover.addEventListener('error', () => { cover.style.visibility = 'hidden'; });
|
||||
cover.src = item.image;
|
||||
}
|
||||
el('.song', row).textContent = item.song;
|
||||
el('.artist', row).textContent = item.artist || '';
|
||||
el('.artist', row).hidden = !item.artist;
|
||||
el('.queue-time', row).textContent = item.duration_ms ? clock(Math.floor(item.duration_ms / 1000)) : '';
|
||||
return row;
|
||||
}));
|
||||
}
|
||||
|
||||
/* Song, artist, cover and progress, each only when there is one. The
|
||||
cover's src is only touched when the track changes, so a poll never makes
|
||||
it flicker. */
|
||||
|
||||
Reference in new issue
Block a user