Follow Apple design guide lines
Deploy HEOS panel / deploy (push) Successful in 27s

This commit is contained in:
franzz committed 2026-09-21 14:44:28 +02:00
1 parent 9a3d0b8a72
commit 83a26c9476
3 files changed
+192 -69

No files matched your search

+61 -1
View File
@@ -8,12 +8,17 @@ const BASE = document.documentElement.dataset.base || '/';
const POLL_MS = 5000;
// How long a dropped cursor waits for the player to report it got there.
const SEEK_HOLD_MS = 10000;
// How far an arrow key moves the progress cursor.
const SEEK_KEY_MS = 10000;
// And how long a pressed play or pause on the AVR waits for the same. The
// Zidoo acknowledges the key press at once but takes a moment to act on it.
const PLAY_HOLD_MS = 6000;
// Motion turned down in the OS means cards change places without sliding.
const CALM = matchMedia('(prefers-reduced-motion: reduce)');
// Which way each arrow key moves a slider.
const ARROWS = { ArrowRight: 1, ArrowUp: 1, ArrowLeft: -1, ArrowDown: -1 };
const el = (sel, root = document) => root.querySelector(sel);
const els = (sel, root = document) => Array.from(root.querySelectorAll(sel));
@@ -170,6 +175,10 @@ function paintVolume(room) {
room.level.textContent = known ? room.volume : '—';
// The fill and the knob riding it both size themselves off this.
room.meter.style.setProperty('--level', known ? room.volume : 0);
// The knob is a slider to VoiceOver, which reads these rather than its digits.
if (known) room.level.setAttribute('aria-valuenow', room.volume);
else room.level.removeAttribute('aria-valuenow');
room.level.setAttribute('aria-disabled', String(!room.available));
room.steps.forEach((button) => { button.disabled = !room.available; });
}
@@ -282,6 +291,16 @@ function paintProgress(refs, known, position, duration) {
if (known) label.style.setProperty('--half', `${label.offsetWidth / 2}px`);
});
el('[data-role="progress-total"]', progress).textContent = known ? clock(total) : '';
const cursor = el('[data-role="progress-cursor"]', progress);
const seekable = progress.classList.contains('seekable');
cursor.tabIndex = seekable ? 0 : -1;
cursor.setAttribute('aria-readonly', String(!seekable));
if (known) {
cursor.setAttribute('aria-valuemax', total);
cursor.setAttribute('aria-valuenow', elapsed);
cursor.setAttribute('aria-valuetext', `${clock(elapsed)} of ${clock(total)}`);
}
}
function tickProgress(refs) {
@@ -339,6 +358,19 @@ function seekable(refs) {
};
line.addEventListener('pointerup', stop);
line.addEventListener('pointercancel', stop);
// The arrow keys, and VoiceOver's swipe up and down on a slider, move the
// cursor a step at a time, each one its own seek.
el('[data-role="progress-cursor"]', progress).addEventListener('keydown', (event) => {
const direction = ARROWS[event.key];
if (!direction || seeking(refs) || !refs.shown || !progress.classList.contains('seekable')) return;
event.preventDefault();
const { duration_ms: duration } = refs.shown;
const ms = refs.shownMs + direction * SEEK_KEY_MS;
refs.seek = { ms: Math.round(Math.max(0, Math.min(duration, ms))), stage: 'drag', song: refs.shown.song };
renderProgress(refs.shown, refs);
sendSeek(refs);
});
}
/* A seek that fails snaps the cursor back; one that works holds it where it
@@ -452,6 +484,11 @@ function holdable(node, action) {
let timer = null;
let delay = 420;
// A press by pointer acts on pointerdown and preventDefault()s, so the
// click behind it is not meant to act again. One with no pointer press
// just before it -- Enter or Space on a focused button, VoiceOver's
// double-tap -- is the only way in for those, and acts once.
let pressed = false;
const stop = () => { clearTimeout(timer); timer = null; delay = 420; };
const tick = () => {
action();
@@ -462,10 +499,15 @@ function holdable(node, action) {
node.addEventListener('pointerdown', (event) => {
if (event.button > 0 || node.disabled) return;
event.preventDefault(); // also suppresses the click that would double-fire
pressed = true;
action();
timer = setTimeout(tick, delay);
});
['pointerup', 'pointercancel', 'pointerleave'].forEach((type) => node.addEventListener(type, stop));
['pointerup', 'pointercancel'].forEach((type) => node.addEventListener(type, () => setTimeout(() => { pressed = false; }, 500)));
node.addEventListener('click', () => {
if (!pressed && !node.disabled) action();
});
}
function nextLevel(current, direction) {
@@ -552,6 +594,14 @@ function draggable(room) {
};
knob.addEventListener('pointerup', stop);
knob.addEventListener('pointercancel', stop);
// The arrow keys, and VoiceOver's swipe up and down on a slider.
knob.addEventListener('keydown', (event) => {
const direction = ARROWS[event.key];
if (!direction) return;
event.preventDefault();
nudge(room.key, direction);
});
}
async function setGrouped(key, joined) {
@@ -733,9 +783,19 @@ function openSheet() {
ui.options.appendChild(option);
});
ui.sheet.hidden = false;
// Into the sheet, on the input already selected, so a keyboard or VoiceOver
// starts there rather than on the page behind it.
(el('[aria-current="true"]', ui.options) || ui.options.firstElementChild).focus();
}
function closeSheet() { ui.sheet.hidden = true; }
function closeSheet() {
ui.sheet.hidden = true;
ui.inputButton.focus();
}
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && sheetOpen()) closeSheet();
});
async function chooseInput(source) {
closeSheet();