[FEAT] Menu and liquid glass #4
@@ -164,6 +164,11 @@ THEME_FONT="Helvetica, Calibri, Tahoma, Verdana, Arial, sans-serif"
|
||||
# Recolor the page background to match the current album art (auto light/dark
|
||||
# text). Needs CORS-accessible art to sample; falls back gracefully otherwise.
|
||||
THEME_DYNAMIC_BG=true
|
||||
# "Liquid Glass" refraction effect on the menu/lyrics/mute/playpause buttons,
|
||||
# replacing their flat THEME_ACCENT fill with a frosted, distorted glass look
|
||||
# (technique from https://github.com/nikdelvin/liquid-glass). Falls back to a
|
||||
# plain blur on browsers without backdrop-filter SVG-filter support.
|
||||
THEME_LIQUID_GLASS=false
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
+81
-24
@@ -87,6 +87,7 @@ a:hover, a:focus { color: var(--text); text-decoration: underline; }
|
||||
.art-ring[hidden] { display: none; }
|
||||
|
||||
.playpause {
|
||||
position: relative;
|
||||
width: 64px; height: 64px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border: none; border-radius: 50%;
|
||||
@@ -160,22 +161,11 @@ audio[controls] { width: 100%; outline: 0; }
|
||||
.waveform { width: 100%; display: block; border-radius: 8px; }
|
||||
.waveform[hidden] { display: none; } /* [hidden] must beat display:block */
|
||||
|
||||
.waveform-picker { position: absolute; top: 6px; right: 6px; z-index: 2; }
|
||||
.waveform-picker summary {
|
||||
list-style: none; padding: 4px 12px;
|
||||
background: rgba(0, 0, 0, 0.55); color: #fff;
|
||||
font-variant: small-caps; font-size: 0.8rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35); border-radius: 999px;
|
||||
cursor: pointer; backdrop-filter: blur(4px);
|
||||
transition: background .15s ease;
|
||||
}
|
||||
.waveform-picker summary::-webkit-details-marker { display: none; }
|
||||
.waveform-picker summary:hover, .waveform-picker summary:focus { background: rgba(0, 0, 0, 0.8); outline: 0; }
|
||||
.waveform-picker[open] summary { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
|
||||
|
||||
.waveform-picker[open] {
|
||||
.waveform-picker {
|
||||
position: fixed; top: 64px; right: 16px; z-index: 30;
|
||||
display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
|
||||
}
|
||||
.waveform-picker[hidden] { display: none; }
|
||||
.waveform-picker fieldset {
|
||||
margin: 0; padding: 8px 10px;
|
||||
background: rgba(0, 0, 0, 0.85); color: #fff;
|
||||
@@ -266,15 +256,36 @@ audio[controls] { width: 100%; outline: 0; }
|
||||
width: 0; height: 0; display: none;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------- history panel */
|
||||
.history-toggle {
|
||||
position: fixed; top: 16px; right: 16px; z-index: 30;
|
||||
padding: 8px 14px;
|
||||
/* ----------------------------------------------------------------- menu */
|
||||
.menu-wrap { position: fixed; top: 16px; right: 16px; z-index: 30; }
|
||||
.menu-toggle {
|
||||
position: relative;
|
||||
width: 40px; height: 40px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: var(--accent); color: var(--accent-text);
|
||||
font-variant: small-caps; border: none; border-radius: 8px; cursor: pointer;
|
||||
font-size: 1.1rem; line-height: 1;
|
||||
border: none; border-radius: 8px; cursor: pointer;
|
||||
}
|
||||
.history-toggle:hover, .history-toggle:focus { background: var(--accent-hover); outline: 0; }
|
||||
.menu-toggle:hover, .menu-toggle:focus { background: var(--accent-hover); outline: 0; }
|
||||
|
||||
.menu-list {
|
||||
position: absolute; top: calc(100% + 6px); right: 0; z-index: 30;
|
||||
min-width: 170px; margin: 0; padding: 6px;
|
||||
list-style: none;
|
||||
background: rgba(0, 0, 0, 0.85); color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35); border-radius: 10px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.menu-list[hidden] { display: none; }
|
||||
.menu-item {
|
||||
display: block; width: 100%; text-align: left;
|
||||
padding: 8px 10px; margin: 0;
|
||||
background: none; border: none; border-radius: 6px;
|
||||
color: inherit; font: inherit; font-size: 0.85rem; cursor: pointer;
|
||||
}
|
||||
.menu-item:hover, .menu-item:focus-visible { background: rgba(255, 255, 255, 0.15); outline: 0; }
|
||||
|
||||
/* ----------------------------------------------------- history panel */
|
||||
.history {
|
||||
position: fixed; top: 0; right: 0; height: 100%;
|
||||
width: min(90vw, 420px); padding: 64px 16px 16px;
|
||||
@@ -294,6 +305,52 @@ audio[controls] { width: 100%; outline: 0; }
|
||||
}
|
||||
.history-table th { opacity: 0.7; font-weight: 600; }
|
||||
|
||||
/* ------------------------------------------------------- liquid glass btns */
|
||||
/* THEME_LIQUID_GLASS=true: refraction technique ported from
|
||||
https://github.com/nikdelvin/liquid-glass (MIT) — see the liquid-glass
|
||||
block in player.js for the SVG feDisplacementMap filter it builds.
|
||||
.glass-fx gets its real backdrop-filter set there; the rule below is
|
||||
just the pre-JS / unsupported-browser fallback (plain blur). The
|
||||
[aria-pressed="true"] variant is needed to beat .lyrics-toggle's own
|
||||
higher-specificity active-state accent fill (class+attribute selector).
|
||||
position/z-index are NOT set here — .lyrics-toggle already needs its own
|
||||
position: absolute for corner placement, so each button supplies its own
|
||||
(see .playpause / .menu-toggle), keeping this class layout-neutral. */
|
||||
.glass-btn,
|
||||
.glass-btn:hover,
|
||||
.glass-btn:focus,
|
||||
.glass-btn:active,
|
||||
.glass-btn[aria-pressed="true"] {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
.glass-btn {
|
||||
isolation: isolate; /* keeps .glass-fx's negative z-index scoped to this button */
|
||||
overflow: hidden;
|
||||
transition: transform .25s ease;
|
||||
}
|
||||
.glass-btn:hover, .glass-btn:focus { outline: 0; transform: scale(1.05) rotate(-1deg); }
|
||||
.glass-fx {
|
||||
position: absolute; inset: 0; z-index: -1; /* behind the icon/text, above the page */
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
box-shadow: inset 0 0 4px rgba(255, 255, 255, 0.55);
|
||||
/* player.js sets --glass-filter/--glass-filter-hover per button (sized to
|
||||
fit); the value here is only the pre-JS / unsupported-browser fallback. */
|
||||
backdrop-filter: var(--glass-filter, blur(8px) saturate(160%) brightness(1.1));
|
||||
-webkit-backdrop-filter: var(--glass-filter, blur(8px) saturate(160%) brightness(1.1));
|
||||
transition: backdrop-filter .2s ease, -webkit-backdrop-filter .2s ease,
|
||||
background .2s ease, box-shadow .2s ease;
|
||||
}
|
||||
/* Hover: weaker refraction + lighter tint so more of what's behind shows through. */
|
||||
.glass-btn:hover .glass-fx, .glass-btn:focus .glass-fx {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: inset 0 0 3px rgba(255, 255, 255, 0.35);
|
||||
backdrop-filter: var(--glass-filter-hover, blur(4px) saturate(130%) brightness(1.05));
|
||||
-webkit-backdrop-filter: var(--glass-filter-hover, blur(4px) saturate(130%) brightness(1.05));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ listeners */
|
||||
.listeners {
|
||||
position: fixed; bottom: 12px; left: 12px;
|
||||
@@ -308,11 +365,11 @@ audio[controls] { width: 100%; outline: 0; }
|
||||
.history { width: min(96vw, 420px); }
|
||||
|
||||
/* -webkit-touch-callout is a no-op everywhere except iOS WebKit, so this
|
||||
@supports check targets iOS phones without any JS/UA sniffing. Picker is
|
||||
hidden there, so give the art a fake ambient glow (no audio analysis)
|
||||
in its place. */
|
||||
@supports check targets iOS phones without any JS/UA sniffing. The menu
|
||||
item is hidden there, so give the art a fake ambient glow (no audio
|
||||
analysis) in its place. */
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
.waveform-picker { display: none; }
|
||||
#menu-waveform { display: none; }
|
||||
.album-art { animation: art-glow-pulse 3.2s ease-in-out infinite; }
|
||||
}
|
||||
}
|
||||
|
||||
+131
-12
@@ -39,14 +39,17 @@
|
||||
lyricsLines: $('lyrics-lines'),
|
||||
lyricsSync: $('lyrics-sync'),
|
||||
lyricsOffset: $('lyrics-offset'),
|
||||
historyToggle:$('history-toggle'),
|
||||
historyBody: $('history-body'),
|
||||
history: $('history'),
|
||||
listeners: $('listeners'),
|
||||
canvas: $('tint-canvas'),
|
||||
waveform: $('waveform'),
|
||||
artRing: $('art-ring'),
|
||||
wavePicker: $('waveform-picker')
|
||||
wavePicker: $('waveform-picker'),
|
||||
menuToggle: $('menu-toggle'),
|
||||
menuList: $('menu-list'),
|
||||
menuHistory: $('menu-history'),
|
||||
menuWaveform: $('menu-waveform')
|
||||
};
|
||||
|
||||
var ICON = {
|
||||
@@ -110,7 +113,10 @@
|
||||
}
|
||||
|
||||
if (audio && el.playpause) {
|
||||
el.playpause.innerHTML = ICON.play;
|
||||
// Icon lives in its own child so it can be swapped without touching the
|
||||
// glass-fx overlay span that sits alongside it (see liquid-glass block).
|
||||
var playIcon = el.playpause.querySelector('.btn-icon') || el.playpause;
|
||||
playIcon.innerHTML = ICON.play;
|
||||
|
||||
// iOS Safari: play() must be called directly inside the tap. Calling
|
||||
// audio.load() first drops the user-gesture flag and iOS rejects playback,
|
||||
@@ -138,11 +144,11 @@
|
||||
else audio.pause();
|
||||
});
|
||||
audio.addEventListener('play', function () {
|
||||
el.playpause.innerHTML = ICON.pause;
|
||||
playIcon.innerHTML = ICON.pause;
|
||||
el.playpause.setAttribute('aria-label', 'Pause');
|
||||
});
|
||||
audio.addEventListener('pause', function () {
|
||||
el.playpause.innerHTML = ICON.play;
|
||||
playIcon.innerHTML = ICON.play;
|
||||
el.playpause.setAttribute('aria-label', 'Play');
|
||||
});
|
||||
|
||||
@@ -155,10 +161,11 @@
|
||||
});
|
||||
}
|
||||
if (el.mute) {
|
||||
el.mute.innerHTML = ICON.vol;
|
||||
var muteIcon = el.mute.querySelector('.btn-icon') || el.mute;
|
||||
muteIcon.innerHTML = ICON.vol;
|
||||
el.mute.addEventListener('click', function () {
|
||||
audio.muted = !audio.muted;
|
||||
el.mute.innerHTML = audio.muted ? ICON.mute : ICON.vol;
|
||||
muteIcon.innerHTML = audio.muted ? ICON.mute : ICON.vol;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -768,13 +775,47 @@
|
||||
while (el.historyBody.children.length > 100) el.historyBody.removeChild(el.historyBody.lastChild);
|
||||
}
|
||||
|
||||
if (el.historyToggle && el.history) {
|
||||
el.historyToggle.addEventListener('click', function () {
|
||||
var open = el.history.classList.toggle('open');
|
||||
el.historyToggle.classList.toggle('open', open);
|
||||
el.historyToggle.textContent = open ? 'Close' : 'Song list';
|
||||
/* --------------------------------------------------------- menu / popovers */
|
||||
function closeMenu() {
|
||||
if (!el.menuList) return;
|
||||
el.menuList.hidden = true;
|
||||
if (el.menuToggle) el.menuToggle.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
if (el.menuToggle && el.menuList) {
|
||||
el.menuToggle.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
var open = el.menuList.hidden;
|
||||
el.menuList.hidden = !open;
|
||||
el.menuToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
if (el.menuHistory && el.history) {
|
||||
el.menuHistory.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
el.history.classList.toggle('open');
|
||||
closeMenu();
|
||||
});
|
||||
}
|
||||
if (el.menuWaveform && el.wavePicker) {
|
||||
el.menuWaveform.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
el.wavePicker.hidden = !el.wavePicker.hidden;
|
||||
closeMenu();
|
||||
});
|
||||
}
|
||||
// Clicking anywhere outside the menu / history pane / waveform picker
|
||||
// closes whichever of them is open (the triggers above stop propagation
|
||||
// so this only fires for genuine outside clicks).
|
||||
document.addEventListener('click', function (e) {
|
||||
closeMenu();
|
||||
if (el.history && el.history.classList.contains('open') && !el.history.contains(e.target)) {
|
||||
el.history.classList.remove('open');
|
||||
}
|
||||
if (el.wavePicker && !el.wavePicker.hidden && !el.wavePicker.contains(e.target)) {
|
||||
el.wavePicker.hidden = true;
|
||||
}
|
||||
});
|
||||
|
||||
/* ----------------------------------------------------------- listeners */
|
||||
function updateListeners(data) {
|
||||
@@ -887,6 +928,84 @@
|
||||
.then(function () { setTimeout(poll, POLL_MS); });
|
||||
}
|
||||
|
||||
/* --------------------------------------------------- liquid glass buttons */
|
||||
// THEME_LIQUID_GLASS=true: replaces each .glass-btn's flat THEME_ACCENT fill
|
||||
// with a real glass refraction. Technique ported from
|
||||
// https://github.com/nikdelvin/liquid-glass (MIT) — an SVG filter chain
|
||||
// (feImage + feDisplacementMap, split per RGB channel for chromatic
|
||||
// aberration) applied via backdrop-filter: url(#filter). CSS already gives
|
||||
// every .glass-fx a plain blur as a static fallback for browsers that don't
|
||||
// support backdrop-filter url() at all (e.g. Safari).
|
||||
(function () {
|
||||
if (!CFG.liquidGlass) return;
|
||||
var buttons = document.querySelectorAll('.glass-btn');
|
||||
if (!buttons.length) return;
|
||||
|
||||
var supportsBackdropUrl = (function () {
|
||||
var t = document.createElement('div');
|
||||
try { t.style.cssText = 'backdrop-filter:url(#t)'; } catch (e) { return false; }
|
||||
return t.style.backdropFilter === 'url(#t)' || t.style.backdropFilter === 'url("#t")';
|
||||
})();
|
||||
if (!supportsBackdropUrl) return; // static CSS blur fallback is already in place
|
||||
|
||||
function displacementMap(w, h, r, depth) {
|
||||
return 'data:image/svg+xml;utf8,' + encodeURIComponent(
|
||||
'<svg height="' + h + '" width="' + w + '" viewBox="0 0 ' + w + ' ' + h + '" xmlns="http://www.w3.org/2000/svg">' +
|
||||
'<style>.mix{mix-blend-mode:screen}</style>' +
|
||||
'<defs>' +
|
||||
'<linearGradient id="Y" x1="0" x2="0" y1="' + Math.ceil(r / h * 15) + '%" y2="' + Math.floor(100 - r / h * 15) + '%">' +
|
||||
'<stop offset="0%" stop-color="#0F0"/><stop offset="100%" stop-color="#000"/></linearGradient>' +
|
||||
'<linearGradient id="X" x1="' + Math.ceil(r / w * 15) + '%" x2="' + Math.floor(100 - r / w * 15) + '%" y1="0" y2="0">' +
|
||||
'<stop offset="0%" stop-color="#F00"/><stop offset="100%" stop-color="#000"/></linearGradient>' +
|
||||
'</defs>' +
|
||||
'<rect x="0" y="0" height="' + h + '" width="' + w + '" fill="#808080"/>' +
|
||||
'<g filter="blur(2px)">' +
|
||||
'<rect x="0" y="0" height="' + h + '" width="' + w + '" fill="#000080"/>' +
|
||||
'<rect x="0" y="0" height="' + h + '" width="' + w + '" fill="url(#Y)" class="mix"/>' +
|
||||
'<rect x="0" y="0" height="' + h + '" width="' + w + '" fill="url(#X)" class="mix"/>' +
|
||||
'<rect x="' + depth + '" y="' + depth + '" height="' + (h - 2 * depth) + '" width="' + (w - 2 * depth) +
|
||||
'" fill="#808080" rx="' + r + '" ry="' + r + '" filter="blur(' + depth + 'px)"/>' +
|
||||
'</g></svg>'
|
||||
);
|
||||
}
|
||||
|
||||
function displacementFilter(w, h, r, depth, strength, cab) {
|
||||
return 'data:image/svg+xml;utf8,' + encodeURIComponent(
|
||||
'<svg height="' + h + '" width="' + w + '" viewBox="0 0 ' + w + ' ' + h + '" xmlns="http://www.w3.org/2000/svg">' +
|
||||
'<defs><filter id="displace" color-interpolation-filters="sRGB">' +
|
||||
'<feImage x="0" y="0" height="' + h + '" width="' + w + '" href="' + displacementMap(w, h, r, depth) + '" result="dm"/>' +
|
||||
'<feDisplacementMap in="SourceGraphic" in2="dm" scale="' + (strength + cab * 2) + '" xChannelSelector="R" yChannelSelector="G"/>' +
|
||||
'<feColorMatrix type="matrix" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="dR"/>' +
|
||||
'<feDisplacementMap in="SourceGraphic" in2="dm" scale="' + (strength + cab) + '" xChannelSelector="R" yChannelSelector="G"/>' +
|
||||
'<feColorMatrix type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" result="dG"/>' +
|
||||
'<feDisplacementMap in="SourceGraphic" in2="dm" scale="' + strength + '" xChannelSelector="R" yChannelSelector="G"/>' +
|
||||
'<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" result="dB"/>' +
|
||||
'<feBlend in="dR" in2="dG" mode="screen"/><feBlend in2="dB" mode="screen"/>' +
|
||||
'</filter></defs></svg>'
|
||||
) + '#displace';
|
||||
}
|
||||
|
||||
// Two filter strengths per button: full refraction at rest, a weaker/
|
||||
// clearer one on hover (see .glass-btn:hover .glass-fx in radio.css) so
|
||||
// more of what's behind the glass shows through.
|
||||
function redraw(btn, fx) {
|
||||
var w = Math.round(btn.offsetWidth), h = Math.round(btn.offsetHeight);
|
||||
if (!w || !h) return;
|
||||
var r = parseFloat(getComputedStyle(btn).borderRadius) || 0;
|
||||
var normal = 'blur(1px) url(\'' + displacementFilter(w, h, r, 6, 70, 4) + '\') blur(4px) brightness(1.1) saturate(1.6)';
|
||||
var hover = 'blur(1px) url(\'' + displacementFilter(w, h, r, 6, 32, 2) + '\') blur(2px) brightness(1.05) saturate(1.3)';
|
||||
fx.style.setProperty('--glass-filter', normal);
|
||||
fx.style.setProperty('--glass-filter-hover', hover);
|
||||
}
|
||||
|
||||
buttons.forEach(function (btn) {
|
||||
var fx = btn.querySelector('.glass-fx');
|
||||
if (!fx) return;
|
||||
redraw(btn, fx);
|
||||
if (window.ResizeObserver) new ResizeObserver(function () { redraw(btn, fx); }).observe(btn);
|
||||
});
|
||||
})();
|
||||
|
||||
setupMediaSession();
|
||||
poll();
|
||||
})();
|
||||
|
||||
@@ -213,6 +213,10 @@ if (!defined('RADIO_CONFIG_LOADED')) {
|
||||
// Recolor the page background to the album art's average color,
|
||||
// switching text light/dark for contrast.
|
||||
'dynamic_bg' => radio_env_bool('THEME_DYNAMIC_BG', true),
|
||||
// "Liquid Glass" refraction effect (menu/lyrics/mute/playpause
|
||||
// buttons) in place of a flat THEME_ACCENT fill. See
|
||||
// https://github.com/nikdelvin/liquid-glass
|
||||
'liquid_glass' => radio_env_bool('THEME_LIQUID_GLASS', false),
|
||||
],
|
||||
|
||||
// ---- Player ---------------------------------------------------
|
||||
@@ -356,6 +360,7 @@ function radio_client_config() {
|
||||
'playerStyle' => cfg('player.style'),
|
||||
'autoplay' => cfg('player.autoplay'),
|
||||
'dynamicBg' => cfg('theme.dynamic_bg'),
|
||||
'liquidGlass' => cfg('theme.liquid_glass'),
|
||||
'mediaSession' => cfg('player.mediasession'),
|
||||
'dynamicMeta' => cfg('player.dynamic_meta'),
|
||||
'siteName' => cfg('site.name'),
|
||||
|
||||
@@ -15,6 +15,9 @@ $waveActive = radio_waveform_active();
|
||||
$waveTypes = cfg('waveform.types');
|
||||
$showWavePicker = $waveActive && count($waveTypes) > 1;
|
||||
$showArtRing = $waveActive && in_array('circular', $waveTypes, true) && $rich;
|
||||
$liquidGlass = cfg('theme.liquid_glass');
|
||||
$glassBtn = $liquidGlass ? ' glass-btn' : '';
|
||||
$glassFx = $liquidGlass ? '<span class="glass-fx" aria-hidden="true"></span>' : '';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= e(cfg('site.lang')) ?>" dir="ltr" prefix="og: http://ogp.me/ns#">
|
||||
@@ -30,7 +33,7 @@ $showArtRing = $waveActive && in_array('circular', $waveTypes, true) && $rich;
|
||||
<img id="art" class="album-art" src="<?= e($fallbackArt) ?>"
|
||||
alt="Album art" title="<?= e(cfg('site.name')) ?>"/>
|
||||
<?php if ($lyricsEnabled): ?>
|
||||
<button id="lyrics-toggle" class="lyrics-toggle" type="button" hidden aria-pressed="false">🎶</button>
|
||||
<button id="lyrics-toggle" class="lyrics-toggle<?= $glassBtn ?>" type="button" hidden aria-pressed="false">🎶<?= $glassFx ?></button>
|
||||
<div id="lyrics-sync" class="lyrics-sync" hidden>
|
||||
<button type="button" data-adj="-0.5" aria-label="Lyrics earlier">−</button>
|
||||
<span id="lyrics-offset">0.0s</span>
|
||||
@@ -58,40 +61,13 @@ $showArtRing = $waveActive && in_array('circular', $waveTypes, true) && $rich;
|
||||
<?php if ($waveActive): ?>
|
||||
<div class="waveform-wrap">
|
||||
<canvas id="waveform" class="waveform" style="height:<?= (int) cfg('waveform.height') ?>px" hidden aria-hidden="true"></canvas>
|
||||
<?php if ($showWavePicker): ?>
|
||||
<details class="waveform-picker" id="waveform-picker">
|
||||
<summary>Style</summary>
|
||||
<fieldset>
|
||||
<legend>Below art</legend>
|
||||
<?php
|
||||
$stripLabels = ['none' => 'None', 'bars' => 'Bars', 'line' => 'Line', 'vu' => 'VU',
|
||||
'spectrum' => 'Spectrum', 'spectrum-mirror' => 'Spectrum (mirror)', 'blob' => 'Blob'];
|
||||
foreach ($stripLabels as $val => $label):
|
||||
if ($val !== 'none' && !in_array($val, $waveTypes, true)) continue;
|
||||
?>
|
||||
<label><input type="radio" name="wave-strip" value="<?= e($val) ?>"/> <?= e($label) ?></label>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
<?php if ($showArtRing || in_array('glow', $waveTypes, true)): ?>
|
||||
<fieldset>
|
||||
<legend>On art</legend>
|
||||
<?php if ($showArtRing): ?>
|
||||
<label><input type="checkbox" id="wave-circular"/> Ring</label>
|
||||
<?php endif; ?>
|
||||
<?php if (in_array('glow', $waveTypes, true)): ?>
|
||||
<label><input type="checkbox" id="wave-glow"/> Glow</label>
|
||||
<?php endif; ?>
|
||||
</fieldset>
|
||||
<?php endif; ?>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($rich): ?>
|
||||
<div class="controls">
|
||||
<div class="playpause-wrap">
|
||||
<button id="playpause" class="playpause" type="button" aria-label="Play"></button>
|
||||
<button id="playpause" class="playpause<?= $glassBtn ?>" type="button" aria-label="Play"><span class="btn-icon"></span><?= $glassFx ?></button>
|
||||
<?php if ($showArtRing): ?>
|
||||
<canvas id="art-ring" class="art-ring" hidden aria-hidden="true"></canvas>
|
||||
<?php endif; ?>
|
||||
@@ -104,7 +80,7 @@ $showArtRing = $waveActive && in_array('circular', $waveTypes, true) && $rich;
|
||||
<span class="live-badge" id="live-badge" hidden><span class="live-dot"></span><span class="live-text">LIVE</span></span>
|
||||
</div>
|
||||
<div class="vol-row">
|
||||
<button id="mute" class="icon-btn" type="button" aria-label="Mute"></button>
|
||||
<button id="mute" class="icon-btn" type="button" aria-label="Mute"><span class="btn-icon"></span></button>
|
||||
<input type="range" id="volume" class="range volume" min="0" max="100" value="100" aria-label="Volume"/>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
@@ -112,8 +88,48 @@ $showArtRing = $waveActive && in_array('circular', $waveTypes, true) && $rich;
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<?php if ($showHistory || $showWavePicker): ?>
|
||||
<div class="menu-wrap" id="menu-wrap">
|
||||
<button id="menu-toggle" class="menu-toggle<?= $glassBtn ?>" type="button" aria-haspopup="true" aria-expanded="false" aria-label="Menu">☰<?= $glassFx ?></button>
|
||||
<ul id="menu-list" class="menu-list" hidden>
|
||||
<?php if ($showHistory): ?>
|
||||
<li><button type="button" id="menu-history" class="menu-item">Song List</button></li>
|
||||
<?php endif; ?>
|
||||
<?php if ($showWavePicker): ?>
|
||||
<li><button type="button" id="menu-waveform" class="menu-item">Waveform Styles</button></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($showWavePicker): ?>
|
||||
<div class="waveform-picker" id="waveform-picker" hidden>
|
||||
<fieldset>
|
||||
<legend>Below art</legend>
|
||||
<?php
|
||||
$stripLabels = ['none' => 'None', 'bars' => 'Bars', 'line' => 'Line', 'vu' => 'VU',
|
||||
'spectrum' => 'Spectrum', 'spectrum-mirror' => 'Spectrum (mirror)', 'blob' => 'Blob'];
|
||||
foreach ($stripLabels as $val => $label):
|
||||
if ($val !== 'none' && !in_array($val, $waveTypes, true)) continue;
|
||||
?>
|
||||
<label><input type="radio" name="wave-strip" value="<?= e($val) ?>"/> <?= e($label) ?></label>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
<?php if ($showArtRing || in_array('glow', $waveTypes, true)): ?>
|
||||
<fieldset>
|
||||
<legend>On art</legend>
|
||||
<?php if ($showArtRing): ?>
|
||||
<label><input type="checkbox" id="wave-circular"/> Ring</label>
|
||||
<?php endif; ?>
|
||||
<?php if (in_array('glow', $waveTypes, true)): ?>
|
||||
<label><input type="checkbox" id="wave-glow"/> Glow</label>
|
||||
<?php endif; ?>
|
||||
</fieldset>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($showHistory): ?>
|
||||
<button id="history-toggle" class="history-toggle" type="button">Song list</button>
|
||||
<aside id="history" class="history">
|
||||
<table class="history-table">
|
||||
<thead><tr><th>Time</th><th>Artist</th><th>Title</th></tr></thead>
|
||||
|
||||
Reference in New Issue
Block a user