This commit is contained in:
+52
-6
@@ -42,8 +42,10 @@ async function api(path, body) {
|
||||
}
|
||||
|
||||
let toastTimer;
|
||||
function toast(message) {
|
||||
/* Red by default, since most toasts report trouble; 'ok' for a confirmation. */
|
||||
function toast(message, kind = 'error') {
|
||||
ui.toast.textContent = message;
|
||||
ui.toast.classList.toggle('ok', kind === 'ok');
|
||||
ui.toast.hidden = false;
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => { ui.toast.hidden = true; }, 4000);
|
||||
@@ -62,9 +64,12 @@ els('.room').forEach((node) => {
|
||||
toggleLabel: el('[data-role="group-label"]', node),
|
||||
play: el('[data-role="play"]', node),
|
||||
next: el('[data-role="next"]', node),
|
||||
spotify: el('.spotify-row', node), // absent with no Spotify account set up
|
||||
steps: els('.step', node),
|
||||
volume: null,
|
||||
playState: null,
|
||||
onSpotify: false,
|
||||
spotifyAccount: null, // the account playing here, whose button gets a border
|
||||
grouped: false,
|
||||
available: false,
|
||||
taps: 0, // button presses not yet sent
|
||||
@@ -95,12 +100,24 @@ function paintRoom(room) {
|
||||
room.play.disabled = !room.available || room.playState === null;
|
||||
room.play.setAttribute(
|
||||
'aria-label', `${room.node.querySelector('h2').textContent}: ${playing ? 'pause' : 'play'}`);
|
||||
// Grouped, transport belongs to the AVR's card, not this one -- pressing
|
||||
// either here would still work (it shares the group's transport) but
|
||||
// only invites confusion about which card is actually in charge of it.
|
||||
room.play.hidden = room.grouped;
|
||||
room.next.hidden = !playing || room.grouped;
|
||||
// Play/pause and next only mean something for a Spotify stream: an AVR
|
||||
// input has no queue to pause or skip, and starting Spotify is what the
|
||||
// account buttons are for. Grouped, transport belongs to the AVR's card,
|
||||
// not this one -- pressing either here would still work (it shares the
|
||||
// group's transport) but only invites confusion about which card is
|
||||
// actually in charge of it.
|
||||
const transport = room.onSpotify && !room.grouped;
|
||||
room.play.hidden = !transport;
|
||||
room.next.hidden = !transport || !playing;
|
||||
room.next.disabled = !room.available;
|
||||
// Resuming stays on offer while grouped: only the speakers have Spotify
|
||||
// buttons, so hiding them here would leave none at all.
|
||||
if (room.spotify) {
|
||||
room.spotify.hidden = !room.available;
|
||||
els('.spotify', room.spotify).forEach((button) => {
|
||||
button.setAttribute('aria-pressed', String(button.dataset.account === room.spotifyAccount));
|
||||
});
|
||||
}
|
||||
|
||||
room.toggle.disabled = !room.available;
|
||||
room.toggle.classList.toggle('busy', room.busy);
|
||||
@@ -265,6 +282,33 @@ ui.splitAll.addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
/* --- Spotify: one button per account, asking the card's own Connect
|
||||
receiver to resume that account, instead of always connecting to it
|
||||
from the Spotify app -------------------------------------------------- */
|
||||
els('.spotify').forEach((button) => {
|
||||
button.addEventListener('click', () => resumeSpotify(button));
|
||||
});
|
||||
|
||||
async function resumeSpotify(button) {
|
||||
const { target, account } = button.dataset;
|
||||
const who = button.querySelector('span').textContent;
|
||||
button.disabled = true;
|
||||
try {
|
||||
const data = await api('/api/spotify/resume', { target, account });
|
||||
toast(`Resuming ${who}'s Spotify on ${data.device}`, 'ok');
|
||||
const room = rooms[target];
|
||||
room.spotifyAccount = account; // show it straight away; the refresh below confirms it
|
||||
paintRoom(room);
|
||||
// HEOS takes a moment to notice the new stream; look again once it
|
||||
// has, so play/pause turns up without waiting for the next poll.
|
||||
setTimeout(refresh, 1500);
|
||||
} catch (error) {
|
||||
toast(error.message);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- the input picker -------------------------------------------------- */
|
||||
ui.inputButton.addEventListener('click', openSheet);
|
||||
el('[data-role="scrim"]').addEventListener('click', closeSheet);
|
||||
@@ -317,6 +361,8 @@ function render(state) {
|
||||
// Do not stomp on a volume the user is in the middle of changing.
|
||||
if (!room.taps && !room.inflight) room.volume = incoming.volume;
|
||||
if (!room.playBusy) room.playState = incoming.play_state;
|
||||
room.onSpotify = Boolean(incoming.spotify);
|
||||
room.spotifyAccount = incoming.spotify_account || null;
|
||||
paintRoom(room);
|
||||
});
|
||||
paintGrouping();
|
||||
|
||||
+21
-7
@@ -45,7 +45,8 @@ button {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||
/* Every icon is a Font Awesome glyph: a solid shape, filled, never stroked. */
|
||||
svg { width: 22px; height: 22px; fill: currentColor; }
|
||||
|
||||
.app {
|
||||
max-width: 520px;
|
||||
@@ -109,9 +110,9 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
|
||||
width: 78px; height: 62px;
|
||||
border-radius: 18px;
|
||||
background: var(--raised);
|
||||
font-size: 30px; font-weight: 500; line-height: 1;
|
||||
display: grid; place-items: center;
|
||||
}
|
||||
.step svg { width: 20px; height: 20px; }
|
||||
.step:active { background: var(--accent); transform: scale(.96); }
|
||||
.step:disabled { opacity: .4; }
|
||||
|
||||
@@ -132,11 +133,6 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
|
||||
.transport:active { background: var(--accent); transform: scale(.97); }
|
||||
.transport:disabled { opacity: .5; }
|
||||
.transport svg { width: 19px; height: 19px; }
|
||||
/* A play triangle wants filling; the pause bars are drawn with the stroke
|
||||
everything else in here uses. */
|
||||
.transport .icon-play { fill: currentColor; stroke: none; }
|
||||
.transport .icon-pause { stroke-width: 2.4; }
|
||||
.transport[data-role="next"] svg { fill: currentColor; stroke: none; }
|
||||
|
||||
/* Which icon shows is a class on the button, not `hidden` on the svg:
|
||||
`hidden` is an HTMLElement property and SVGElement does not inherit it,
|
||||
@@ -160,6 +156,22 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
|
||||
.toggle:disabled { opacity: .5; }
|
||||
.toggle.busy { opacity: .6; }
|
||||
|
||||
/* --- Spotify: one resume button per account ----------------------------- */
|
||||
.spotify-row { display: flex; gap: 10px; }
|
||||
.spotify {
|
||||
flex: 1; min-width: 0;
|
||||
height: 50px; border-radius: 16px;
|
||||
background: var(--raised);
|
||||
display: flex; align-items: center; justify-content: center; gap: 9px;
|
||||
font-size: 15px; font-weight: 550;
|
||||
}
|
||||
.spotify svg { flex: 0 0 auto; color: #fff; }
|
||||
.spotify span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.spotify:active { transform: scale(.985); }
|
||||
.spotify:disabled { opacity: .5; }
|
||||
/* The account playing on this room right now. */
|
||||
.spotify[aria-pressed="true"] { border: 2px solid var(--ink); }
|
||||
|
||||
/* --- source card ------------------------------------------------------ */
|
||||
.source-button {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
@@ -192,6 +204,7 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
|
||||
background: none; border: 1px solid var(--edge); color: var(--muted);
|
||||
}
|
||||
.joined .toggle .dot { display: none; }
|
||||
.joined .spotify { height: 40px; font-size: 13px; font-weight: 500; }
|
||||
.joined .toggle:active { background: var(--raised); color: var(--ink); }
|
||||
|
||||
/* --- misc ------------------------------------------------------------- */
|
||||
@@ -242,6 +255,7 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
|
||||
background: #2b1f24; border: 1px solid #52303a; color: #ffd9d4;
|
||||
font-size: 14px; box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
|
||||
}
|
||||
.toast.ok { background: #172a21; border-color: #2a5540; color: #c9f7df; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { animation: none !important; transition: none !important; }
|
||||
|
||||
Reference in New Issue
Block a user