This commit is contained in:
1 parent
3565465f34
commit
0560079b16
3 files changed
+68
-17
No files matched your search
+55
-10
@@ -8,6 +8,8 @@ const BASE = document.documentElement.dataset.base || '/';
|
|||||||
const POLL_MS = 5000;
|
const POLL_MS = 5000;
|
||||||
// How long a dropped cursor waits for the player to report it got there.
|
// How long a dropped cursor waits for the player to report it got there.
|
||||||
const SEEK_HOLD_MS = 10000;
|
const SEEK_HOLD_MS = 10000;
|
||||||
|
// Motion turned down in the OS means cards change places without sliding.
|
||||||
|
const CALM = matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
|
|
||||||
const el = (sel, root = document) => root.querySelector(sel);
|
const el = (sel, root = document) => root.querySelector(sel);
|
||||||
const els = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
const els = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
||||||
@@ -58,6 +60,7 @@ const ui = {
|
|||||||
inputIcon: el('[data-role="input-icon"]'),
|
inputIcon: el('[data-role="input-icon"]'),
|
||||||
sheet: el('[data-role="sheet"]'),
|
sheet: el('[data-role="sheet"]'),
|
||||||
options: el('[data-role="options"]'),
|
options: el('[data-role="options"]'),
|
||||||
|
app: el('.app'),
|
||||||
avr: el('.card.avr'),
|
avr: el('.card.avr'),
|
||||||
joined: el('[data-role="joined"]'),
|
joined: el('[data-role="joined"]'),
|
||||||
avrMedia: el('[data-role="avr-media"]'),
|
avrMedia: el('[data-role="avr-media"]'),
|
||||||
@@ -91,6 +94,10 @@ const avrTrack = {
|
|||||||
avrTrack.cover.addEventListener('error', () => { avrTrack.cover.hidden = true; });
|
avrTrack.cover.addEventListener('error', () => { avrTrack.cover.hidden = true; });
|
||||||
seekable(avrTrack);
|
seekable(avrTrack);
|
||||||
|
|
||||||
|
// Same as a room's card: named, so a view transition can follow it when it
|
||||||
|
// moves to the top and back.
|
||||||
|
ui.avr.style.viewTransitionName = 'avr';
|
||||||
|
|
||||||
const rooms = {};
|
const rooms = {};
|
||||||
let inputs = [];
|
let inputs = [];
|
||||||
let currentInput = null;
|
let currentInput = null;
|
||||||
@@ -124,7 +131,6 @@ 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),
|
||||||
meter: el('[data-role="meter"]', node),
|
meter: el('[data-role="meter"]', node),
|
||||||
actions: el('[data-role="actions"]', node),
|
actions: el('[data-role="actions"]', node),
|
||||||
@@ -225,9 +231,8 @@ function paintRoom(room) {
|
|||||||
// Out of the group the button names where tapping it sends the room; in it,
|
// Out of the group the button names where tapping it sends the room; in it,
|
||||||
// where the card sits already says so, so the button says what it does.
|
// where the card sits already says so, so the button says what it does.
|
||||||
room.toggleLabel.textContent = room.grouped ? 'Ungroup' : 'Cinema';
|
room.toggleLabel.textContent = room.grouped ? 'Ungroup' : 'Cinema';
|
||||||
// Grouped, its slot is empty and the AVR's card is the one that moves up.
|
|
||||||
room.slot.classList.toggle('on-top', playing && !room.grouped);
|
|
||||||
placeRoom(room);
|
placeRoom(room);
|
||||||
|
placeCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Song, artist, cover and progress, each only when there is one. The
|
/* Song, artist, cover and progress, each only when there is one. The
|
||||||
@@ -427,13 +432,24 @@ function clock(seconds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* A merged room moves into the host's card, because that is what merging
|
/* 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
|
means: one group, playing one thing. Leaving puts it back in the column,
|
||||||
own slot, which is why the slots exist. */
|
ahead of the rooms declared after it, so the cards keep the order they are
|
||||||
|
declared in however they were grouped. */
|
||||||
function placeRoom(room) {
|
function placeRoom(room) {
|
||||||
const target = room.grouped ? ui.joined : room.slot;
|
if (room.grouped) {
|
||||||
if (room.node.parentElement !== target) target.appendChild(room.node);
|
if (room.node.parentElement !== ui.joined) ui.joined.appendChild(room.node);
|
||||||
// Left empty, a slot would still take a gap of its own and double the space.
|
} else if (room.node.parentElement !== ui.app) {
|
||||||
room.slot.hidden = room.grouped;
|
const order = Object.keys(rooms);
|
||||||
|
const next = order.slice(order.indexOf(room.key) + 1)
|
||||||
|
.map((key) => rooms[key].node)
|
||||||
|
.find((node) => node.parentElement === ui.app);
|
||||||
|
// Nothing left in the column below it means it goes last, before the host.
|
||||||
|
ui.app.insertBefore(room.node, next || ui.avr);
|
||||||
|
}
|
||||||
|
// A view transition only follows an element it can name, and the name has
|
||||||
|
// to be one of a kind on the page (see placeCards). Inside the host's card
|
||||||
|
// the card travels with it, so it takes no name of its own there.
|
||||||
|
room.node.style.viewTransitionName = room.grouped ? 'none' : `room-${room.key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function paintGrouping() {
|
function paintGrouping() {
|
||||||
@@ -451,7 +467,36 @@ function paintGrouping() {
|
|||||||
const playing = inside.some((node) => node.classList.contains('playing'))
|
const playing = inside.some((node) => node.classList.contains('playing'))
|
||||||
|| avrNowPlaying?.play_state === 'play';
|
|| avrNowPlaying?.play_state === 'play';
|
||||||
ui.avr.classList.toggle('playing', playing);
|
ui.avr.classList.toggle('playing', playing);
|
||||||
ui.avr.classList.toggle('on-top', playing);
|
placeCards();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Which card sits at the top of the column is CSS order (see .on-top), and
|
||||||
|
order is not a property that transitions: the cards change places between
|
||||||
|
one frame and the next. A view transition does the sliding instead -- the
|
||||||
|
browser snapshots the cards where they are, runs the swap, and animates
|
||||||
|
them to where they landed. One turn can paint several cards (a poll
|
||||||
|
touches every room and then the group), so the move is collected and run
|
||||||
|
once, and only started when a card really does change places. Without
|
||||||
|
support, or with motion turned down, the cards just change places the way
|
||||||
|
they always did. */
|
||||||
|
let placing = false;
|
||||||
|
function placeCards() {
|
||||||
|
if (placing) return;
|
||||||
|
placing = true;
|
||||||
|
queueMicrotask(() => {
|
||||||
|
placing = false;
|
||||||
|
// Grouped, a room's card has left the column and the AVR's is the one
|
||||||
|
// that moves up. The AVR's tint and its position answer the same
|
||||||
|
// question, and paintGrouping() has just settled it.
|
||||||
|
const wanted = Object.values(rooms)
|
||||||
|
.map((room) => [room.node, room.playing() && !room.grouped]);
|
||||||
|
wanted.push([ui.avr, ui.avr.classList.contains('playing')]);
|
||||||
|
|
||||||
|
const move = () => wanted.forEach(([node, top]) => node.classList.toggle('on-top', top));
|
||||||
|
const moves = wanted.some(([node, top]) => node.classList.contains('on-top') !== top);
|
||||||
|
if (!moves || CALM.matches || !document.startViewTransition) move();
|
||||||
|
else document.startViewTransition(move);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Press and hold to keep moving, accelerating as you hold. */
|
/* Press and hold to keep moving, accelerating as you hold. */
|
||||||
|
|||||||
+10
-3
@@ -89,7 +89,14 @@ svg { width: 22px; height: 22px; fill: currentColor; }
|
|||||||
|
|
||||||
/* Card */
|
/* Card */
|
||||||
|
|
||||||
.app > .on-top { order: -1; } /* A playing card goes to the top */
|
/* A playing card goes to the top. order does not transition, so the swap
|
||||||
|
rides on a view transition instead -- see placeCards() in app.js. */
|
||||||
|
.app > .on-top { order: -1; }
|
||||||
|
|
||||||
|
::view-transition-group(*) {
|
||||||
|
animation-duration: .4s;
|
||||||
|
animation-timing-function: cubic-bezier(.22, 1, .36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
@@ -321,8 +328,8 @@ button.sources-btn.loading .icon { display: none; }
|
|||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
|
|
||||||
.foot { margin: var(--space-xs) var(--space-xs) 0; min-height: 18px; font-size: 12px; color: var(--muted); text-align: center; }
|
footer { margin: var(--space-xs) var(--space-xs) 0; min-height: 18px; font-size: 12px; color: var(--muted); text-align: center; }
|
||||||
.foot.bad { color: var(--warn); }
|
footer.bad { color: var(--warn); }
|
||||||
|
|
||||||
/* Toast */
|
/* Toast */
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" data-step="{{ step }}" data-host="{{ host.label }}" data-base="{{ url_for('index') }}">
|
<html lang="en" data-step="{{ step }}" data-host="{{ host.label }}" data-base="{{ url_for('index') }}">
|
||||||
|
|
||||||
<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">
|
||||||
@@ -17,8 +18,8 @@
|
|||||||
<link rel="icon" href="{{ url_for('static', filename='logo.svg') }}">
|
<link rel="icon" href="{{ url_for('static', filename='logo.svg') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='panel.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='panel.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
|
|
||||||
|
<body>
|
||||||
{% macro host_toggle() -%}
|
{% macro host_toggle() -%}
|
||||||
<button class="sources-btn toggle" data-role="group" aria-pressed="false">
|
<button class="sources-btn toggle" data-role="group" aria-pressed="false">
|
||||||
<svg class="cinema icon" viewBox="0 0 512 512" aria-hidden="true"><path d="M256 512a256 256 0 1 0 0-512 256 256 0 1 0 0 512zM128 192c0-17.7 14.3-32 32-32l128 0c17.7 0 32 14.3 32 32l0 38.4 61-36.6c1.9-1.2 4.2-1.8 6.4-1.8 6.9 0 12.5 5.6 12.5 12.5l0 102.9c0 6.9-5.6 12.5-12.5 12.5-2.3 0-4.5-.6-6.4-1.8l-61-36.6 0 38.4c0 17.7-14.3 32-32 32l-128 0c-17.7 0-32-14.3-32-32l0-128z"/></svg>
|
<svg class="cinema icon" viewBox="0 0 512 512" aria-hidden="true"><path d="M256 512a256 256 0 1 0 0-512 256 256 0 1 0 0 512zM128 192c0-17.7 14.3-32 32-32l128 0c17.7 0 32 14.3 32 32l0 38.4 61-36.6c1.9-1.2 4.2-1.8 6.4-1.8 6.9 0 12.5 5.6 12.5 12.5l0 102.9c0 6.9-5.6 12.5-12.5 12.5-2.3 0-4.5-.6-6.4-1.8l-61-36.6 0 38.4c0 17.7-14.3 32-32 32l-128 0c-17.7 0-32-14.3-32-32l0-128z"/></svg>
|
||||||
@@ -52,7 +53,6 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{% 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><svg class="room-icon" data-icon="{{ room.kind }}" viewBox="0 0 512 512" aria-hidden="true"><path d=""/></svg><span class="room-label">{{ room.label }}</span></h2>
|
<h2><svg class="room-icon" data-icon="{{ room.kind }}" viewBox="0 0 512 512" aria-hidden="true"><path d=""/></svg><span class="room-label">{{ room.label }}</span></h2>
|
||||||
@@ -105,7 +105,6 @@
|
|||||||
|
|
||||||
{{ control_row(room.key, spotify_accounts) }}
|
{{ control_row(room.key, spotify_accounts) }}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<section class="card avr">
|
<section class="card avr">
|
||||||
@@ -142,7 +141,7 @@
|
|||||||
<div class="joined" data-role="joined" hidden></div>
|
<div class="joined" data-role="joined" hidden></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<p class="foot" data-role="foot"></p>
|
<footer data-role="foot"></footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sheet-wrap" data-role="sheet" hidden>
|
<div class="sheet-wrap" data-role="sheet" hidden>
|
||||||
|
|||||||
Reference in new issue
Block a user