diff --git a/README.md b/README.md index f4e8904..93725be 100644 --- a/README.md +++ b/README.md @@ -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 — so the levels stay round. Hold to keep moving. - **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 - **Change the AVR's input**, listed under the names you gave them, minus the sources you deleted in the AVR's setup menu diff --git a/static/app.js b/static/app.js index 766cc26..45c7f4e 100644 --- a/static/app.js +++ b/static/app.js @@ -3,6 +3,7 @@ network round trip before it looks like it did anything feels broken. */ const STEP = Number(document.documentElement.dataset.step) || 2; +const HOST_LABEL = document.documentElement.dataset.host || 'the AVR'; const POLL_MS = 5000; const el = (sel, root = document) => root.querySelector(sel); @@ -18,6 +19,8 @@ const ui = { inputName: el('[data-role="input-name"]'), sheet: el('[data-role="sheet"]'), options: el('[data-role="options"]'), + joined: el('[data-role="joined"]'), + joinedRooms: el('[data-role="joined-rooms"]'), }; const rooms = {}; @@ -50,6 +53,7 @@ els('.room').forEach((node) => { const room = { key, node, + slot: el(`.room-slot[data-slot="${key}"]`), level: el('[data-role="level"]', node), bar: el('[data-role="bar"]', node), toggle: el('[data-role="group"]', node), @@ -81,7 +85,30 @@ function paintRoom(room) { room.toggle.disabled = !room.available; room.toggle.classList.toggle('busy', room.busy); 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. */ @@ -154,6 +181,7 @@ async function setGrouped(key, joined) { room.busy = true; room.grouped = joined; // show the new state while the speakers catch up paintRoom(room); + paintGrouping(); try { const data = await api('/api/group', { target: key, joined }); applyJoined(data.joined || []); @@ -170,12 +198,13 @@ function applyJoined(joined) { room.grouped = joined.includes(room.key); paintRoom(room); }); + paintGrouping(); } ui.splitAll.addEventListener('click', async () => { try { 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 || []); } catch (error) { toast(error.message); @@ -237,6 +266,7 @@ function render(state) { if (!room.taps && !room.inflight) room.volume = incoming.volume; paintRoom(room); }); + paintGrouping(); const avr = state.avr || {}; inputs = avr.inputs || []; @@ -283,4 +313,5 @@ document.addEventListener('visibilitychange', () => { }); Object.values(rooms).forEach(paintRoom); +paintGrouping(); refresh(); diff --git a/static/panel.css b/static/panel.css index db568dc..c9380b3 100644 --- a/static/panel.css +++ b/static/panel.css @@ -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 .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 ------------------------------------------------------------- */ .wide { width: 100%; height: 50px; border-radius: 16px; font-size: 15px; } .ghost { background: transparent; border: 1px solid var(--edge); color: var(--muted); } diff --git a/templates/index.html b/templates/index.html index 3d47875..0d6aea6 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,5 +1,5 @@ - + @@ -37,9 +37,15 @@ + + {% for room in rooms %} +

{{ room.label }}

@@ -57,9 +63,10 @@ Separate
+
{% endfor %} - +