+34
-2
@@ -60,13 +60,16 @@ els('.room').forEach((node) => {
|
||||
bar: el('[data-role="bar"]', node),
|
||||
toggle: el('[data-role="group"]', node),
|
||||
toggleLabel: el('[data-role="group-label"]', node),
|
||||
play: el('[data-role="play"]', node),
|
||||
steps: els('.step', node),
|
||||
volume: null,
|
||||
playState: null,
|
||||
grouped: false,
|
||||
available: false,
|
||||
taps: 0, // button presses not yet sent
|
||||
inflight: false,
|
||||
busy: false, // a grouping change is in flight
|
||||
playBusy: false,
|
||||
};
|
||||
rooms[key] = room;
|
||||
|
||||
@@ -76,14 +79,21 @@ els('.room').forEach((node) => {
|
||||
});
|
||||
|
||||
room.toggle.addEventListener('click', () => setGrouped(key, !room.grouped));
|
||||
room.play.addEventListener('click', () => togglePlay(key));
|
||||
});
|
||||
|
||||
function paintRoom(room) {
|
||||
const known = room.volume !== null && room.volume !== undefined;
|
||||
room.level.textContent = known ? room.volume : '—';
|
||||
room.level.textContent = known ? room.volume + '%' : '—';
|
||||
room.bar.style.width = `${known ? room.volume : 0}%`;
|
||||
room.node.classList.toggle('offline', !room.available);
|
||||
room.steps.forEach((button) => { button.disabled = !room.available; });
|
||||
const playing = room.playState === 'play';
|
||||
room.play.classList.toggle('playing', playing);
|
||||
room.play.disabled = !room.available || room.playState === null;
|
||||
room.play.setAttribute(
|
||||
'aria-label', `${room.node.querySelector('h2').textContent}: ${playing ? 'pause' : 'play'}`);
|
||||
|
||||
room.toggle.disabled = !room.available;
|
||||
room.toggle.classList.toggle('busy', room.busy);
|
||||
room.toggle.setAttribute('aria-pressed', String(room.grouped));
|
||||
@@ -195,6 +205,26 @@ async function setGrouped(key, joined) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Sends the state it wants rather than "toggle", so a stale idea of what
|
||||
the room is doing cannot flip it the wrong way. */
|
||||
async function togglePlay(key) {
|
||||
const room = rooms[key];
|
||||
if (!room.available || room.playBusy) return;
|
||||
const wanted = room.playState === 'play' ? 'pause' : 'play';
|
||||
room.playBusy = true;
|
||||
room.playState = wanted;
|
||||
paintRoom(room);
|
||||
try {
|
||||
const data = await api('/api/playback', { target: key, state: wanted });
|
||||
room.playState = data.state;
|
||||
} catch (error) {
|
||||
toast(error.message);
|
||||
} finally {
|
||||
room.playBusy = false;
|
||||
paintRoom(room);
|
||||
}
|
||||
}
|
||||
|
||||
function applyJoined(joined) {
|
||||
Object.values(rooms).forEach((room) => {
|
||||
room.grouped = joined.includes(room.key);
|
||||
@@ -266,6 +296,7 @@ function render(state) {
|
||||
room.grouped = incoming.grouped;
|
||||
// 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;
|
||||
paintRoom(room);
|
||||
});
|
||||
paintGrouping();
|
||||
@@ -297,7 +328,8 @@ async function refresh() {
|
||||
}
|
||||
|
||||
function busy() {
|
||||
return sheetOpen() || Object.values(rooms).some((r) => r.taps || r.inflight || r.busy);
|
||||
return sheetOpen()
|
||||
|| Object.values(rooms).some((r) => r.taps || r.inflight || r.busy || r.playBusy);
|
||||
}
|
||||
|
||||
ui.refresh.addEventListener('click', () => {
|
||||
|
||||
@@ -118,6 +118,32 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
|
||||
.meter { flex: 1; height: 8px; border-radius: 999px; background: #0c111b; overflow: hidden; }
|
||||
.meter i { display: block; height: 100%; width: 0; border-radius: 999px; background: var(--accent); transition: width .12s ease-out; }
|
||||
|
||||
/* --- play / pause, beside the join button ------------------------------ */
|
||||
.actions { display: flex; align-items: stretch; gap: 10px; }
|
||||
.actions .toggle { flex: 1; }
|
||||
|
||||
.transport {
|
||||
flex: 0 0 auto;
|
||||
width: 66px; height: 52px;
|
||||
border-radius: 16px;
|
||||
background: var(--raised);
|
||||
display: grid; place-items: center;
|
||||
}
|
||||
.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; }
|
||||
|
||||
/* 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,
|
||||
so svg.hidden = true sets a JS expando and styles nothing. */
|
||||
.transport .icon-pause { display: none; }
|
||||
.transport.playing .icon-play { display: none; }
|
||||
.transport.playing .icon-pause { display: block; }
|
||||
|
||||
/* --- join / leave the AVR -------------------------------------------- */
|
||||
.toggle {
|
||||
height: 52px; border-radius: 16px;
|
||||
@@ -165,6 +191,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 .transport { height: 40px; width: 58px; }
|
||||
.joined .toggle:active { background: var(--raised); color: var(--ink); }
|
||||
|
||||
/* --- misc ------------------------------------------------------------- */
|
||||
|
||||
Reference in New Issue
Block a user