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;
|
||||
// How long a dropped cursor waits for the player to report it got there.
|
||||
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 els = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
||||
@@ -58,6 +60,7 @@ const ui = {
|
||||
inputIcon: el('[data-role="input-icon"]'),
|
||||
sheet: el('[data-role="sheet"]'),
|
||||
options: el('[data-role="options"]'),
|
||||
app: el('.app'),
|
||||
avr: el('.card.avr'),
|
||||
joined: el('[data-role="joined"]'),
|
||||
avrMedia: el('[data-role="avr-media"]'),
|
||||
@@ -91,6 +94,10 @@ const avrTrack = {
|
||||
avrTrack.cover.addEventListener('error', () => { avrTrack.cover.hidden = true; });
|
||||
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 = {};
|
||||
let inputs = [];
|
||||
let currentInput = null;
|
||||
@@ -124,7 +131,6 @@ els('.room').forEach((node) => {
|
||||
const room = {
|
||||
key,
|
||||
node,
|
||||
slot: el(`.room-slot[data-slot="${key}"]`),
|
||||
level: el('[data-role="level"]', node),
|
||||
meter: el('[data-role="meter"]', 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,
|
||||
// where the card sits already says so, so the button says what it does.
|
||||
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);
|
||||
placeCards();
|
||||
}
|
||||
|
||||
/* 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
|
||||
means: one group, playing one thing. Leaving puts the card back in its
|
||||
own slot, which is why the slots exist. */
|
||||
means: one group, playing one thing. Leaving puts it back in the column,
|
||||
ahead of the rooms declared after it, so the cards keep the order they are
|
||||
declared in however they were grouped. */
|
||||
function placeRoom(room) {
|
||||
const target = room.grouped ? ui.joined : room.slot;
|
||||
if (room.node.parentElement !== target) target.appendChild(room.node);
|
||||
// Left empty, a slot would still take a gap of its own and double the space.
|
||||
room.slot.hidden = room.grouped;
|
||||
if (room.grouped) {
|
||||
if (room.node.parentElement !== ui.joined) ui.joined.appendChild(room.node);
|
||||
} else if (room.node.parentElement !== ui.app) {
|
||||
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() {
|
||||
@@ -451,7 +467,36 @@ function paintGrouping() {
|
||||
const playing = inside.some((node) => node.classList.contains('playing'))
|
||||
|| avrNowPlaying?.play_state === 'play';
|
||||
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. */
|
||||
|
||||
+10
-3
@@ -89,7 +89,14 @@ svg { width: 22px; height: 22px; fill: currentColor; }
|
||||
|
||||
/* 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 {
|
||||
background: var(--card);
|
||||
@@ -321,8 +328,8 @@ button.sources-btn.loading .icon { display: none; }
|
||||
|
||||
/* Footer */
|
||||
|
||||
.foot { 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 { margin: var(--space-xs) var(--space-xs) 0; min-height: 18px; font-size: 12px; color: var(--muted); text-align: center; }
|
||||
footer.bad { color: var(--warn); }
|
||||
|
||||
/* Toast */
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-step="{{ step }}" data-host="{{ host.label }}" data-base="{{ url_for('index') }}">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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="stylesheet" href="{{ url_for('static', filename='panel.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<body>
|
||||
{% macro host_toggle() -%}
|
||||
<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>
|
||||
@@ -52,7 +53,6 @@
|
||||
</header>
|
||||
|
||||
{% for room in rooms %}
|
||||
<div class="room-slot" data-slot="{{ room.key }}">
|
||||
<section class="card room" data-room="{{ room.key }}">
|
||||
<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>
|
||||
@@ -105,7 +105,6 @@
|
||||
|
||||
{{ control_row(room.key, spotify_accounts) }}
|
||||
</section>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<section class="card avr">
|
||||
@@ -142,7 +141,7 @@
|
||||
<div class="joined" data-role="joined" hidden></div>
|
||||
</section>
|
||||
|
||||
<p class="foot" data-role="foot"></p>
|
||||
<footer data-role="foot"></footer>
|
||||
</div>
|
||||
|
||||
<div class="sheet-wrap" data-role="sheet" hidden>
|
||||
|
||||
Reference in new issue
Block a user