+50
-3
@@ -11,6 +11,44 @@ const POLL_MS = 5000;
|
||||
const el = (sel, root = document) => root.querySelector(sel);
|
||||
const els = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
||||
|
||||
const SPEAKER = { viewbox: '0 0 384 512', path: 'M0 64C0 28.7 28.7 0 64 0L320 0c35.3 0 64 28.7 64 64l0 384c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zM304 336a112 112 0 1 0 -224 0 112 112 0 1 0 224 0zM192 272a64 64 0 1 1 0 128 64 64 0 1 1 0-128zm0-112a48 48 0 1 0 0-96 48 48 0 1 0 0 96z' };
|
||||
const SPEAKERS = { viewbox: '0 -32 448 576', path: 'M160-32C124.7-32 96-3.3 96 32l0 352c0 35.3 28.7 64 64 64l224 0c35.3 0 64-28.7 64-64l0-352c0-35.3-28.7-64-64-64L160-32zM272 184a104 104 0 1 1 0 208 104 104 0 1 1 0-208zm56 104a56 56 0 1 0 -112 0 56 56 0 1 0 112 0zM240 64a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zM48 88c0-13.3-10.7-24-24-24S0 74.7 0 88L0 480c0 35.3 28.7 64 64 64l264 0c13.3 0 24-10.7 24-24s-10.7-24-24-24L64 496c-8.8 0-16-7.2-16-16L48 88z' };
|
||||
const TV = { viewbox: '0 0 576 512', path: 'M64 96l0 240 448 0 0-240-448 0zM0 96C0 60.7 28.7 32 64 32l448 0c35.3 0 64 28.7 64 64l0 240c0 35.3-28.7 64-64 64L64 400c-35.3 0-64-28.7-64-64L0 96zM160 448l256 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-256 0c-17.7 0-32-14.3-32-32s14.3-32 32-32z' };
|
||||
|
||||
// Keyed on a room's kind (config.IN_ROOM_GROUPS, or "avr") or an input's
|
||||
// HEOS id, straight from data-icon or GET /api/avr/inputs' "code" -- not
|
||||
// something a room name or a renamed input label could change. "plug" is
|
||||
// the fallback for an input HEOS reports that none of these name.
|
||||
const ICONS = {
|
||||
avr: { viewbox: '0 0 448 512', path: 'M64 32C28.7 32 0 60.7 0 96l0 64c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-64c0-35.3-28.7-64-64-64L64 32zm216 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm56 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zM64 288c-35.3 0-64 28.7-64 64l0 64c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-64c0-35.3-28.7-64-64-64L64 288zm216 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm56 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z' },
|
||||
'none': SPEAKER,
|
||||
'stereo-pair': SPEAKERS,
|
||||
'lcr-fronts': SPEAKERS,
|
||||
'surround-sound-system': SPEAKERS,
|
||||
'subwoofer': SPEAKER,
|
||||
mediaplayer: { viewbox: '0 0 640 544', path: 'M192 64c0-35.3 28.7-64 64-64L544 0c35.3 0 64 28.7 64 64l0 120.4c-6.8-.8-13.8-.6-20.9 .6l-64.1 11.7-54.5-89.2C464.1 100.4 456.4 96 448 96s-16.1 4.4-20.5 11.5l-54 88.3-17.9-25.6c-4.5-6.4-11.8-10.2-19.7-10.2s-15.2 3.8-19.7 10.2l-56 80c-5.1 7.3-5.8 16.9-1.6 24.8S271.1 288 280 288l72 0 0 64-96 0c-35.3 0-64-28.7-64-64l0-224zM352 403.8c-15.3 3.8-29.7 10.5-42.1 19.7-8.7 6.5-16.8 14.8-23.3 24.5L64 448c-35.3 0-64-28.7-64-64L0 160c0-35.3 28.7-64 64-64l80 0 0 192c0 61.9 50.1 112 112 112l96 0 0 3.8zM320 96a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zM52 164l0 24c0 8.8 7.2 16 16 16l24 0c8.8 0 16-7.2 16-16l0-24c0-8.8-7.2-16-16-16l-24 0c-8.8 0-16 7.2-16 16zm16 80c-8.8 0-16 7.2-16 16l0 24c0 8.8 7.2 16 16 16l24 0c8.8 0 16-7.2 16-16l0-24c0-8.8-7.2-16-16-16l-24 0zm0 96c-8.8 0-16 7.2-16 16l0 24c0 8.8 7.2 16 16 16l24 0c8.8 0 16-7.2 16-16l0-24c0-8.8-7.2-16-16-16l-24 0zm556-84.1l0 208c0 23.3-18 35.4-29.1 40.5-11.9 5.4-25.4 7.5-34.9 7.5s-22.9-2.1-34.9-7.5c-11.1-5-29.1-17.2-29.1-40.5s18-35.4 29.1-40.5c11.9-5.4 25.4-7.5 34.9-7.5 4.6 0 10.1 .5 16 1.6l0-84.9-128 23.3 0 139.9c0 23.3-18 35.4-29.1 40.5-11.9 5.4-25.4 7.5-34.9 7.5s-22.9-2.1-34.9-7.5c-11.1-5-29.1-17.2-29.1-40.5s18-35.4 29.1-40.5c11.9-5.4 25.4-7.5 34.9-7.5 4.6 0 10.1 .5 16 1.6l0-161.6c0-11.6 8.3-21.5 19.7-23.6l176-32c7-1.3 14.2 .6 19.7 5.2s8.6 11.3 8.6 18.4z' },
|
||||
game: { viewbox: '0 0 640 512', path: 'M448 64c106 0 192 86 192 192S554 448 448 448l-256 0C86 448 0 362 0 256S86 64 192 64l256 0zM192 176c-13.3 0-24 10.7-24 24l0 32-32 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l32 0 0 32c0 13.3 10.7 24 24 24s24-10.7 24-24l0-32 32 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-32 0 0-32c0-13.3-10.7-24-24-24zm240 96a32 32 0 1 0 0 64 32 32 0 1 0 0-64zm64-96a32 32 0 1 0 0 64 32 32 0 1 0 0-64z' },
|
||||
tv: TV,
|
||||
tvaudio: TV,
|
||||
plug: { viewbox: '0 -32 448 544', path: 'M128-32c17.7 0 32 14.3 32 32l0 96 128 0 0-96c0-17.7 14.3-32 32-32s32 14.3 32 32l0 96 64 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l0 64c0 95.1-69.2 174.1-160 189.3l0 66.7c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-66.7C101.2 398.1 32 319.1 32 224l0-64c-17.7 0-32-14.3-32-32S14.3 96 32 96l64 0 0-96c0-17.7 14.3-32 32-32z' },
|
||||
};
|
||||
|
||||
function inputIcon(code) {
|
||||
return ICONS[(code || '').replace('inputs/', '')] || ICONS.plug;
|
||||
}
|
||||
|
||||
/* Repaints an inline <svg><path/></svg> in place -- cheaper than replacing
|
||||
the node, and there is no library here to lose track of the change. */
|
||||
function paintIcon(svg, icon) {
|
||||
svg.setAttribute('viewBox', icon.viewbox);
|
||||
svg.firstElementChild.setAttribute('d', icon.path);
|
||||
}
|
||||
|
||||
// Room and AVR cards carry their icon as a fixed kind (data-icon="avr", set
|
||||
// server-side from config -- see index.html) rather than the AVR input's
|
||||
// live one, so these paint once, not on every poll.
|
||||
els('[data-icon]').forEach((svg) => paintIcon(svg, ICONS[svg.dataset.icon]));
|
||||
|
||||
const ui = {
|
||||
foot: el('[data-role="foot"]'),
|
||||
toast: el('[data-role="toast"]'),
|
||||
@@ -19,6 +57,7 @@ const ui = {
|
||||
avrStatus: el('[data-role="avr-status"]'),
|
||||
inputButton: el('[data-role="input-button"]'),
|
||||
inputName: el('[data-role="input-name"]'),
|
||||
inputIcon: el('[data-role="input-icon"]'),
|
||||
sheet: el('[data-role="sheet"]'),
|
||||
options: el('[data-role="options"]'),
|
||||
joined: el('[data-role="joined"]'),
|
||||
@@ -445,6 +484,10 @@ el('[data-role="sheet-close"]').addEventListener('click', closeSheet);
|
||||
|
||||
function sheetOpen() { return !ui.sheet.hidden; }
|
||||
|
||||
function setInputIcon(code) {
|
||||
paintIcon(ui.inputIcon, inputIcon(code));
|
||||
}
|
||||
|
||||
function openSheet() {
|
||||
if (!inputs.length) {
|
||||
toast('No inputs reported by the AVR yet');
|
||||
@@ -455,9 +498,10 @@ function openSheet() {
|
||||
const option = document.createElement('button');
|
||||
option.className = 'option';
|
||||
option.setAttribute('aria-current', String(currentInput && currentInput.code === source.code));
|
||||
option.innerHTML = '<span></span><span class="code"></span>';
|
||||
option.firstChild.textContent = source.name;
|
||||
option.lastChild.textContent = source.code;
|
||||
option.innerHTML = '<span class="option-label"><svg viewBox="0 0 512 512" aria-hidden="true"><path d=""/></svg><span></span></span><span class="code"></span>';
|
||||
paintIcon(option.querySelector('svg'), inputIcon(source.code));
|
||||
option.querySelector('.option-label span').textContent = source.name;
|
||||
option.querySelector('.code').textContent = source.code;
|
||||
option.addEventListener('click', () => chooseInput(source));
|
||||
ui.options.appendChild(option);
|
||||
});
|
||||
@@ -470,10 +514,12 @@ async function chooseInput(source) {
|
||||
closeSheet();
|
||||
currentInput = source;
|
||||
ui.inputName.textContent = source.name;
|
||||
setInputIcon(source.code);
|
||||
try {
|
||||
const data = await api('/api/avr/input', { code: source.code });
|
||||
currentInput = data;
|
||||
ui.inputName.textContent = data.name;
|
||||
setInputIcon(data.code);
|
||||
} catch (error) {
|
||||
toast(error.message);
|
||||
refresh();
|
||||
@@ -503,6 +549,7 @@ function render(state) {
|
||||
inputs = avr.inputs || [];
|
||||
currentInput = avr.input || null;
|
||||
ui.inputName.textContent = currentInput ? currentInput.name : '—';
|
||||
setInputIcon(currentInput ? currentInput.code : null);
|
||||
ui.avrStatus.textContent = avr.connected ? 'ready' : 'offline';
|
||||
ui.avrStatus.classList.toggle('on', Boolean(avr.connected));
|
||||
renderTrack(avr.now_playing || null, avrTrack);
|
||||
|
||||
+11
-4
@@ -6,7 +6,7 @@
|
||||
--bg: #0a0d14;
|
||||
--card: #151a25;
|
||||
--edge: #232b3b;
|
||||
--raised: #1e2634;
|
||||
--raised: #29334a;
|
||||
--ink: #eef2f9;
|
||||
--muted: #8d98ad;
|
||||
--accent: #5b8def;
|
||||
@@ -93,10 +93,12 @@ svg { width: 22px; height: 22px; fill: currentColor; }
|
||||
|
||||
.card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.card-head h2 {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
margin: 0; min-width: 0;
|
||||
font-size: 17px; font-weight: 600;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.card-head h2 .room-icon { flex: 0 0 auto; height: 20px; width: auto; fill: currentColor; color: var(--muted); }
|
||||
.card-head h2 .room-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.pill {
|
||||
font-size: 11px; text-transform: uppercase; letter-spacing: .08em;
|
||||
@@ -213,7 +215,7 @@ svg { width: 22px; height: 22px; fill: currentColor; }
|
||||
/* --- join / leave the AVR, beside the room's name ---------------------- */
|
||||
.toggle {
|
||||
flex: 0 0 auto;
|
||||
height: 40px; padding: 0 14px; border-radius: 999px;
|
||||
padding: 7px 14px; border-radius: 999px;
|
||||
background: var(--raised);
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
font-size: 14px; font-weight: 550; white-space: nowrap;
|
||||
@@ -253,6 +255,7 @@ svg { width: 22px; height: 22px; fill: currentColor; }
|
||||
}
|
||||
.source-button:active { transform: scale(.985); }
|
||||
.source-button .eyebrow { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
|
||||
.source-button [data-role="input-icon"] { flex: 0 0 auto; height: 18px; width: 18px; fill: currentColor; color: var(--muted); }
|
||||
.source-button .value { flex: 1; font-size: 19px; font-weight: 600; }
|
||||
.source-button .chevron { color: var(--muted); }
|
||||
|
||||
@@ -321,7 +324,11 @@ svg { width: 22px; height: 22px; fill: currentColor; }
|
||||
}
|
||||
.option:active { transform: scale(.985); }
|
||||
.option[aria-current="true"] { background: var(--accent); color: #fff; }
|
||||
.option .code { font-size: 12px; color: var(--muted); }
|
||||
.option-label { display: flex; align-items: center; gap: 10px; min-width: 0; overflow: hidden; }
|
||||
.option-label svg { flex: 0 0 auto; height: 18px; width: 18px; fill: currentColor; color: var(--muted); }
|
||||
.option-label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.option[aria-current="true"] .option-label svg { color: rgba(255, 255, 255, .8); }
|
||||
.option .code { font-size: 12px; color: var(--muted); flex: 0 0 auto; }
|
||||
.option[aria-current="true"] .code { color: rgba(255, 255, 255, .8); }
|
||||
|
||||
/* --- toast ------------------------------------------------------------ */
|
||||
|
||||
Reference in New Issue
Block a user