UI improvements: glow effects, deck colors, listener count, remove black bar, mobile header fix

This commit is contained in:
ComputerTech 2026-03-11 19:34:32 +00:00
parent 6027f2e973
commit 2e64870daa
7 changed files with 121 additions and 60 deletions

View File

@ -424,10 +424,6 @@
<!-- Settings Panel -->
<div class="settings-panel" id="settings-panel">
<div class="settings-header">
<span>SETTINGS</span>
<button class="close-settings" onclick="toggleSettings()">X</button>
</div>
<div class="settings-content">
<div class="setting-item"><label><input type="checkbox" id="repeat-A"
onchange="toggleRepeat('A', this.checked)">Repeat Deck A</label></div>
@ -446,10 +442,15 @@
<div class="setting-item"><label><input type="checkbox" id="glow-B"
onchange="updateManualGlow('B', this.checked)">Glow Deck B (Magenta)</label></div>
<div class="setting-item" style="flex-direction: column; align-items: flex-start;">
<label>Glow Intensity</label>
<label>Glow Intensity (DJ Panel)</label>
<input type="range" id="glow-intensity" min="1" max="100" value="30" style="width: 100%;"
oninput="updateGlowIntensity(this.value)">
</div>
<div class="setting-item" style="flex-direction: column; align-items: flex-start;">
<label>Listener Page Glow</label>
<input type="range" id="listener-glow-intensity" min="0" max="100" value="30" style="width: 100%;"
oninput="updateListenerGlow(this.value)">
</div>
<div class="setting-item">
<button class="btn-primary" onclick="openKeyboardSettings()"
style="width: 100%; padding: 12px; margin-top: 10px;">

View File

@ -45,34 +45,33 @@ body::before {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1;
z-index: 999;
opacity: var(--glow-opacity, 0.3);
transition: all 1s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Listener atmospheric glow */
/* Listener atmospheric glow — static, intensity driven by --glow-opacity/--glow-spread */
body.listener-glow::before {
animation: pulse-listener 4s ease-in-out infinite;
box-shadow:
0 0 var(--glow-spread) rgba(0, 80, 255, calc(var(--glow-opacity) * 1.5)),
0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 1.5)),
inset 0 0 var(--glow-spread) rgba(0, 80, 255, calc(var(--glow-opacity) * 1)),
inset 0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 1));
}
@keyframes pulse-listener {
0%, 100% {
filter: hue-rotate(0deg) brightness(1.2);
box-shadow:
0 0 var(--glow-spread) rgba(0, 80, 255, calc(var(--glow-opacity) * 1.5)),
0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 1.5)),
inset 0 0 var(--glow-spread) rgba(0, 80, 255, calc(var(--glow-opacity) * 1)),
inset 0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 1));
}
50% {
filter: hue-rotate(15deg) brightness(1.8);
box-shadow:
0 0 calc(var(--glow-spread) * 1.5) rgba(0, 120, 255, calc(var(--glow-opacity) * 2.2)),
0 0 calc(var(--glow-spread) * 2) rgba(220, 50, 255, calc(var(--glow-opacity) * 2.2)),
0 0 calc(var(--glow-spread) * 4) rgba(0, 243, 255, calc(var(--glow-opacity) * 1)),
inset 0 0 calc(var(--glow-spread) * 1.5) rgba(0, 120, 255, calc(var(--glow-opacity) * 1.5)),
inset 0 0 calc(var(--glow-spread) * 2) rgba(220, 50, 255, calc(var(--glow-opacity) * 1.5));
}
/* Deck-driven glow — mirrors the DJ panel colours */
body.listener-glow.playing-A::before {
box-shadow: inset 0 0 var(--glow-spread) var(--primary-cyan);
}
body.listener-glow.playing-B::before {
box-shadow: inset 0 0 var(--glow-spread) var(--secondary-magenta);
}
body.listener-glow.playing-A.playing-B::before {
box-shadow:
inset 0 0 var(--glow-spread) var(--primary-cyan),
inset 0 0 calc(var(--glow-spread) * 1.5) var(--secondary-magenta);
}
/* ========== Listener Mode Layout ========== */
@ -80,7 +79,7 @@ body.listener-glow::before {
.listener-mode {
position: fixed;
inset: 0;
background: linear-gradient(135deg, #0a0a12 0%, #1a0a1a 100%);
background: transparent;
z-index: 10;
display: flex;
flex-direction: column;

View File

@ -12,7 +12,7 @@
<!-- Listener UI -->
<div class="listener-mode" id="listener-mode">
<div class="listener-header">
<h1>TECHDJ LIVE</h1>
<h1>TECHY.MUSIC</h1>
<div class="live-indicator">
<span class="pulse-dot"></span>
<span>LIVE</span>

View File

@ -308,6 +308,15 @@ function initSocket() {
handleBroadcastOffline();
});
socket.on('listener_glow', (data) => {
updateGlowIntensity(data.intensity ?? 30);
});
socket.on('deck_glow', (data) => {
document.body.classList.toggle('playing-A', !!data.A);
document.body.classList.toggle('playing-B', !!data.B);
});
return socket;
}

View File

@ -773,6 +773,12 @@ function formatTime(seconds) {
}
// Playback Logic
function _notifyListenerDeckGlow() {
// Emit the current playing state of both decks to the listener page
if (!socket) return;
socket.emit('deck_glow', { A: !!decks.A.playing, B: !!decks.B.playing });
}
function playDeck(id) {
vibrate(15);
// Server-side audio mode
@ -782,8 +788,7 @@ function playDeck(id) {
const deckEl = document.getElementById('deck-' + id);
if (deckEl) deckEl.classList.add('playing');
document.body.classList.add('playing-' + id);
_notifyListenerDeckGlow();
console.log(`[Deck ${id}] Play command sent to server`);
return;
}
@ -800,6 +805,7 @@ function playDeck(id) {
const deckEl = document.getElementById('deck-' + id);
if (deckEl) deckEl.classList.add('playing');
document.body.classList.add('playing-' + id);
_notifyListenerDeckGlow();
if (audioCtx.state === 'suspended') {
console.log(`[Deck ${id}] Resuming suspended AudioContext`);
@ -836,7 +842,7 @@ function pauseDeck(id) {
const deckEl = document.getElementById('deck-' + id);
if (deckEl) deckEl.classList.remove('playing');
document.body.classList.remove('playing-' + id);
_notifyListenerDeckGlow();
console.log(`[Deck ${id}] Pause command sent to server`);
return;
}
@ -863,6 +869,7 @@ function pauseDeck(id) {
const deckEl = document.getElementById('deck-' + id);
if (deckEl) deckEl.classList.remove('playing');
document.body.classList.remove('playing-' + id);
_notifyListenerDeckGlow();
}
@ -1774,6 +1781,12 @@ function updateGlowIntensity(val) {
document.documentElement.style.setProperty('--glow-spread', `${spread}px`);
}
function updateListenerGlow(val) {
settings.listenerGlowIntensity = parseInt(val);
if (!socket) initSocket();
socket.emit('listener_glow', { intensity: settings.listenerGlowIntensity });
}
// Dismiss landscape prompt
function dismissLandscapePrompt() {
const prompt = document.getElementById('landscape-prompt');

View File

@ -615,7 +615,6 @@ def dj_login():
<input id=\"password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" autofocus />
<button type=\"submit\">Unlock DJ Panel</button>
{f"<div class='err'>{error}</div>" if error else ""}
<div class=\"hint\">Set/disable this in config.json (dj_panel_password).</div>
</form>
</div>
</div>
@ -736,6 +735,23 @@ def dj_start(data=None):
def dj_get_listener_count():
emit('listener_count', {'count': len(listener_sids)})
@dj_socketio.on('listener_glow')
def dj_listener_glow(data):
"""DJ sets the glow intensity on the listener page."""
intensity = int(data.get('intensity', 30)) if isinstance(data, dict) else 30
intensity = max(0, min(100, intensity))
listener_socketio.emit('listener_glow', {'intensity': intensity}, namespace='/')
@dj_socketio.on('deck_glow')
def dj_deck_glow(data):
"""Relay which decks are playing so the listener page can mirror the glow colour."""
if not isinstance(data, dict):
return
listener_socketio.emit('deck_glow', {
'A': bool(data.get('A', False)),
'B': bool(data.get('B', False)),
}, namespace='/')
@dj_socketio.on('stop_broadcast')
def dj_stop():
broadcast_state['active'] = False
@ -784,34 +800,49 @@ listener_socketio = SocketIO(
cors_allowed_origins=CONFIG_CORS,
async_mode='eventlet',
max_http_buffer_size=CONFIG_MAX_UPLOAD_MB * 1024 * 1024,
ping_timeout=60,
ping_interval=25,
# Lower timeouts: stale connections detected in ~25s instead of ~85s
# ping_interval: how often to probe (seconds)
# ping_timeout: how long to wait for pong before declaring dead
ping_timeout=15,
ping_interval=10,
logger=CONFIG_DEBUG,
engineio_logger=CONFIG_DEBUG
)
def _broadcast_listener_count():
"""Compute the most accurate listener count and broadcast to both panels.
Uses the larger of:
- listener_sids: Socket.IO connections (people with the page open)
- _mp3_clients: active /stream.mp3 HTTP connections (people actually hearing audio)
Taking the max avoids undercounting when someone hasn't clicked Enable Audio
yet, and also avoids undercounting direct stream URL listeners (e.g. VLC).
"""
with _mp3_lock:
stream_count = len(_mp3_clients)
count = max(len(listener_sids), stream_count)
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
dj_socketio.emit('listener_count', {'count': count}, namespace='/')
return count
@listener_socketio.on('connect')
def listener_connect():
print(f"LISTENER: Listener Socket Connected: {request.sid}")
# Count immediately on connect — don't wait for join_listener
listener_sids.add(request.sid)
count = _broadcast_listener_count()
print(f"LISTENER: Connected {request.sid}. Total: {count}")
@listener_socketio.on('disconnect')
def listener_disconnect():
listener_sids.discard(request.sid)
count = len(listener_sids)
print(f"REMOVED: Listener left. Total: {count}")
# Notify BOTH namespaces
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
dj_socketio.emit('listener_count', {'count': count}, namespace='/')
count = _broadcast_listener_count()
print(f"REMOVED: Listener left {request.sid}. Total: {count}")
@listener_socketio.on('join_listener')
def listener_join():
if request.sid not in listener_sids:
listener_sids.add(request.sid)
count = len(listener_sids)
print(f"LISTENER: New listener joined. Total: {count}")
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
dj_socketio.emit('listener_count', {'count': count}, namespace='/')
# SID already added in listener_connect(); just send stream status back
emit('stream_status', {'active': broadcast_state['active']})
@listener_socketio.on('get_listener_count')
@ -833,12 +864,11 @@ def _transcoder_watchdog():
def _listener_count_sync_loop():
"""Periodic background sync to ensure listener count is always accurate."""
"""Periodic reconciliation — catches any edge cases where connect/disconnect
events were missed (e.g. server under load, eventlet greenlet delays)."""
while True:
count = len(listener_sids)
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
dj_socketio.emit('listener_count', {'count': count}, namespace='/')
eventlet.sleep(5)
_broadcast_listener_count()
if __name__ == '__main__':

View File

@ -24,6 +24,12 @@
html {
scroll-behavior: smooth;
overflow: hidden;
scrollbar-width: none; /* Firefox */
}
html::-webkit-scrollbar {
display: none; /* Chrome / Edge / Safari */
}
body {
@ -218,7 +224,7 @@ header h1 {
grid-template-rows: 1fr 80px;
gap: 10px;
padding: 10px;
height: calc(100vh - 60px);
height: 100vh;
/* Adjust based on header height */
min-height: 0;
overflow: hidden;
@ -1706,6 +1712,9 @@ input[type=range] {
/* Less intense on mobile */
}
header {
display: none;
}
.mobile-top-bar {
display: flex;
@ -1916,8 +1925,8 @@ input[type=range] {
/* Streaming Button */
.streaming-btn {
position: fixed;
bottom: 25px;
right: 175px;
bottom: 175px;
right: 25px;
width: 60px;
height: 60px;
border-radius: 50%;
@ -1947,8 +1956,8 @@ input[type=range] {
.upload-btn {
position: fixed;
bottom: 25px;
right: 100px;
bottom: 100px;
right: 25px;
width: 60px;
height: 60px;
border-radius: 50%;
@ -1980,7 +1989,7 @@ input[type=range] {
.streaming-panel {
position: fixed;
top: 0;
right: -400px;
right: -460px;
height: 100vh;
width: 380px;
background: rgba(10, 10, 20, 0.98);
@ -2587,8 +2596,8 @@ body.listening-active .landscape-prompt {
/* Base Settings Button Fix */
.keyboard-btn {
position: fixed;
bottom: 25px;
right: 250px;
bottom: 250px;
right: 25px;
width: 60px;
height: 60px;
border-radius: 50%;
@ -2637,7 +2646,7 @@ body.listening-active .landscape-prompt {
.settings-panel {
position: fixed;
top: 0;
right: -350px;
right: -400px;
height: 100vh;
width: 320px;
background: rgba(10, 10, 20, 0.98);