Show merged rooms inside the host's card
Two cards side by side said nothing about whether the rooms were playing together; now a room that joins the AVR moves into its card, under the input it is playing, and leaving puts the card back in its own slot. The button changes with it: Join Home Cinema when it stands alone, Leave when it is in the group, since the card's position already says which it is. Also fixes Separate everything, which sent a GET to a POST-only route and so failed with a 405 and snapped the rooms straight back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,8 @@ Everything it does fits on one screen:
|
|||||||
on the next multiple of `VOLUME_STEP` — from 23 it goes to 25, not 28 —
|
on the next multiple of `VOLUME_STEP` — from 23 it goes to 25, not 28 —
|
||||||
so the levels stay round. Hold to keep moving.
|
so the levels stay round. Hold to keep moving.
|
||||||
- **Group** either room with the AVR — the AVR is always the host, so its
|
- **Group** either room with the AVR — the AVR is always the host, so its
|
||||||
sound takes over whatever joins it
|
sound takes over whatever joins it. A room that joins moves *into* the
|
||||||
|
Home Cinema card, so one glance says what is playing together
|
||||||
- **Ungroup** either room again, or all of them at once
|
- **Ungroup** either room again, or all of them at once
|
||||||
- **Change the AVR's input**, listed under the names you gave them, minus
|
- **Change the AVR's input**, listed under the names you gave them, minus
|
||||||
the sources you deleted in the AVR's setup menu
|
the sources you deleted in the AVR's setup menu
|
||||||
|
|||||||
+33
-2
@@ -3,6 +3,7 @@
|
|||||||
network round trip before it looks like it did anything feels broken. */
|
network round trip before it looks like it did anything feels broken. */
|
||||||
|
|
||||||
const STEP = Number(document.documentElement.dataset.step) || 2;
|
const STEP = Number(document.documentElement.dataset.step) || 2;
|
||||||
|
const HOST_LABEL = document.documentElement.dataset.host || 'the AVR';
|
||||||
const POLL_MS = 5000;
|
const POLL_MS = 5000;
|
||||||
|
|
||||||
const el = (sel, root = document) => root.querySelector(sel);
|
const el = (sel, root = document) => root.querySelector(sel);
|
||||||
@@ -18,6 +19,8 @@ const ui = {
|
|||||||
inputName: el('[data-role="input-name"]'),
|
inputName: el('[data-role="input-name"]'),
|
||||||
sheet: el('[data-role="sheet"]'),
|
sheet: el('[data-role="sheet"]'),
|
||||||
options: el('[data-role="options"]'),
|
options: el('[data-role="options"]'),
|
||||||
|
joined: el('[data-role="joined"]'),
|
||||||
|
joinedRooms: el('[data-role="joined-rooms"]'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const rooms = {};
|
const rooms = {};
|
||||||
@@ -50,6 +53,7 @@ els('.room').forEach((node) => {
|
|||||||
const room = {
|
const room = {
|
||||||
key,
|
key,
|
||||||
node,
|
node,
|
||||||
|
slot: el(`.room-slot[data-slot="${key}"]`),
|
||||||
level: el('[data-role="level"]', node),
|
level: el('[data-role="level"]', node),
|
||||||
bar: el('[data-role="bar"]', node),
|
bar: el('[data-role="bar"]', node),
|
||||||
toggle: el('[data-role="group"]', node),
|
toggle: el('[data-role="group"]', node),
|
||||||
@@ -81,7 +85,30 @@ function paintRoom(room) {
|
|||||||
room.toggle.disabled = !room.available;
|
room.toggle.disabled = !room.available;
|
||||||
room.toggle.classList.toggle('busy', room.busy);
|
room.toggle.classList.toggle('busy', room.busy);
|
||||||
room.toggle.setAttribute('aria-pressed', String(room.grouped));
|
room.toggle.setAttribute('aria-pressed', String(room.grouped));
|
||||||
room.toggleLabel.textContent = room.grouped ? 'Grouped with AVR' : 'Separate';
|
// Where the card sits already says whether it is grouped, so the button
|
||||||
|
// says what tapping it does instead.
|
||||||
|
room.toggleLabel.textContent = room.grouped ? 'Leave' : `Join ${HOST_LABEL}`;
|
||||||
|
placeRoom(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A merged room moves into the host's card, because that is what merging
|
||||||
|
means: one group, playing one thing. Leaving puts the card back in its
|
||||||
|
own slot, which is why the slots exist. */
|
||||||
|
function placeRoom(room) {
|
||||||
|
const target = room.grouped ? ui.joinedRooms : room.slot;
|
||||||
|
if (room.node.parentElement !== target) target.appendChild(room.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintGrouping() {
|
||||||
|
const order = Object.keys(rooms);
|
||||||
|
const inside = Array.from(ui.joinedRooms.children);
|
||||||
|
// Keep them in the order the cards are declared, not the order they joined.
|
||||||
|
inside
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => order.indexOf(a.dataset.room) - order.indexOf(b.dataset.room))
|
||||||
|
.forEach((node) => ui.joinedRooms.appendChild(node));
|
||||||
|
ui.joined.hidden = inside.length === 0;
|
||||||
|
ui.splitAll.hidden = inside.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Press and hold to keep moving, accelerating as you hold. */
|
/* Press and hold to keep moving, accelerating as you hold. */
|
||||||
@@ -154,6 +181,7 @@ async function setGrouped(key, joined) {
|
|||||||
room.busy = true;
|
room.busy = true;
|
||||||
room.grouped = joined; // show the new state while the speakers catch up
|
room.grouped = joined; // show the new state while the speakers catch up
|
||||||
paintRoom(room);
|
paintRoom(room);
|
||||||
|
paintGrouping();
|
||||||
try {
|
try {
|
||||||
const data = await api('/api/group', { target: key, joined });
|
const data = await api('/api/group', { target: key, joined });
|
||||||
applyJoined(data.joined || []);
|
applyJoined(data.joined || []);
|
||||||
@@ -170,12 +198,13 @@ function applyJoined(joined) {
|
|||||||
room.grouped = joined.includes(room.key);
|
room.grouped = joined.includes(room.key);
|
||||||
paintRoom(room);
|
paintRoom(room);
|
||||||
});
|
});
|
||||||
|
paintGrouping();
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.splitAll.addEventListener('click', async () => {
|
ui.splitAll.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
applyJoined([]);
|
applyJoined([]);
|
||||||
const data = await api('/api/group/none');
|
const data = await api('/api/group/none', {}); // {} so it is a POST, as the route requires
|
||||||
applyJoined(data.joined || []);
|
applyJoined(data.joined || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast(error.message);
|
toast(error.message);
|
||||||
@@ -237,6 +266,7 @@ function render(state) {
|
|||||||
if (!room.taps && !room.inflight) room.volume = incoming.volume;
|
if (!room.taps && !room.inflight) room.volume = incoming.volume;
|
||||||
paintRoom(room);
|
paintRoom(room);
|
||||||
});
|
});
|
||||||
|
paintGrouping();
|
||||||
|
|
||||||
const avr = state.avr || {};
|
const avr = state.avr || {};
|
||||||
inputs = avr.inputs || [];
|
inputs = avr.inputs || [];
|
||||||
@@ -283,4 +313,5 @@ document.addEventListener('visibilitychange', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Object.values(rooms).forEach(paintRoom);
|
Object.values(rooms).forEach(paintRoom);
|
||||||
|
paintGrouping();
|
||||||
refresh();
|
refresh();
|
||||||
|
|||||||
@@ -147,6 +147,26 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
|
|||||||
.source-button .value { flex: 1; font-size: 19px; font-weight: 600; }
|
.source-button .value { flex: 1; font-size: 19px; font-weight: 600; }
|
||||||
.source-button .chevron { color: var(--muted); }
|
.source-button .chevron { color: var(--muted); }
|
||||||
|
|
||||||
|
/* --- rooms merged into the host's card --------------------------------- */
|
||||||
|
.joined { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
.joined-title { margin: 0 2px; font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
|
||||||
|
.joined-rooms { display: flex; flex-direction: column; gap: 14px; }
|
||||||
|
|
||||||
|
/* The card stops being a card in here: one border around the group, not
|
||||||
|
one around every room in it. */
|
||||||
|
.joined .card.room { background: none; border: 0; border-radius: 0; padding: 0; gap: 12px; }
|
||||||
|
.joined .card.room + .card.room { border-top: 1px solid var(--edge); padding-top: 14px; }
|
||||||
|
.joined .card-head h2 { font-size: 15px; font-weight: 550; color: var(--muted); }
|
||||||
|
.joined .level b { font-size: 22px; }
|
||||||
|
.joined .step { height: 54px; }
|
||||||
|
.joined .toggle,
|
||||||
|
.joined .toggle[aria-pressed="true"] {
|
||||||
|
height: 40px; font-size: 13px; font-weight: 500;
|
||||||
|
background: none; border: 1px solid var(--edge); color: var(--muted);
|
||||||
|
}
|
||||||
|
.joined .toggle .dot { display: none; }
|
||||||
|
.joined .toggle:active { background: var(--raised); color: var(--ink); }
|
||||||
|
|
||||||
/* --- misc ------------------------------------------------------------- */
|
/* --- misc ------------------------------------------------------------- */
|
||||||
.wide { width: 100%; height: 50px; border-radius: 16px; font-size: 15px; }
|
.wide { width: 100%; height: 50px; border-radius: 16px; font-size: 15px; }
|
||||||
.ghost { background: transparent; border: 1px solid var(--edge); color: var(--muted); }
|
.ghost { background: transparent; border: 1px solid var(--edge); color: var(--muted); }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" data-step="{{ step }}">
|
<html lang="en" data-step="{{ step }}" data-host="{{ host.label }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
|
||||||
@@ -37,9 +37,15 @@
|
|||||||
<span class="value" data-role="input-name">—</span>
|
<span class="value" data-role="input-name">—</span>
|
||||||
<svg class="chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>
|
<svg class="chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div class="joined" data-role="joined" hidden>
|
||||||
|
<p class="joined-title">Playing together</p>
|
||||||
|
<div class="joined-rooms" data-role="joined-rooms"></div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% for room in rooms %}
|
{% for room in rooms %}
|
||||||
|
<div class="room-slot" data-slot="{{ room.key }}">
|
||||||
<section class="card room" data-room="{{ room.key }}">
|
<section class="card room" data-room="{{ room.key }}">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h2>{{ room.label }}</h2>
|
<h2>{{ room.label }}</h2>
|
||||||
@@ -57,9 +63,10 @@
|
|||||||
<span data-role="group-label">Separate</span>
|
<span data-role="group-label">Separate</span>
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<button class="wide ghost" data-role="split-all">Separate everything</button>
|
<button class="wide ghost" data-role="split-all" hidden>Separate everything</button>
|
||||||
<p class="foot" data-role="foot"></p>
|
<p class="foot" data-role="foot"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user