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 --> <!-- Settings Panel -->
<div class="settings-panel" id="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="settings-content">
<div class="setting-item"><label><input type="checkbox" id="repeat-A" <div class="setting-item"><label><input type="checkbox" id="repeat-A"
onchange="toggleRepeat('A', this.checked)">Repeat Deck A</label></div> 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" <div class="setting-item"><label><input type="checkbox" id="glow-B"
onchange="updateManualGlow('B', this.checked)">Glow Deck B (Magenta)</label></div> onchange="updateManualGlow('B', this.checked)">Glow Deck B (Magenta)</label></div>
<div class="setting-item" style="flex-direction: column; align-items: flex-start;"> <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%;" <input type="range" id="glow-intensity" min="1" max="100" value="30" style="width: 100%;"
oninput="updateGlowIntensity(this.value)"> oninput="updateGlowIntensity(this.value)">
</div> </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"> <div class="setting-item">
<button class="btn-primary" onclick="openKeyboardSettings()" <button class="btn-primary" onclick="openKeyboardSettings()"
style="width: 100%; padding: 12px; margin-top: 10px;"> style="width: 100%; padding: 12px; margin-top: 10px;">

View File

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

View File

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

View File

@ -308,6 +308,15 @@ function initSocket() {
handleBroadcastOffline(); 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; return socket;
} }

View File

@ -773,6 +773,12 @@ function formatTime(seconds) {
} }
// Playback Logic // 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) { function playDeck(id) {
vibrate(15); vibrate(15);
// Server-side audio mode // Server-side audio mode
@ -782,8 +788,7 @@ function playDeck(id) {
const deckEl = document.getElementById('deck-' + id); const deckEl = document.getElementById('deck-' + id);
if (deckEl) deckEl.classList.add('playing'); if (deckEl) deckEl.classList.add('playing');
document.body.classList.add('playing-' + id); document.body.classList.add('playing-' + id);
_notifyListenerDeckGlow();
console.log(`[Deck ${id}] Play command sent to server`); console.log(`[Deck ${id}] Play command sent to server`);
return; return;
} }
@ -800,6 +805,7 @@ function playDeck(id) {
const deckEl = document.getElementById('deck-' + id); const deckEl = document.getElementById('deck-' + id);
if (deckEl) deckEl.classList.add('playing'); if (deckEl) deckEl.classList.add('playing');
document.body.classList.add('playing-' + id); document.body.classList.add('playing-' + id);
_notifyListenerDeckGlow();
if (audioCtx.state === 'suspended') { if (audioCtx.state === 'suspended') {
console.log(`[Deck ${id}] Resuming suspended AudioContext`); console.log(`[Deck ${id}] Resuming suspended AudioContext`);
@ -836,7 +842,7 @@ function pauseDeck(id) {
const deckEl = document.getElementById('deck-' + id); const deckEl = document.getElementById('deck-' + id);
if (deckEl) deckEl.classList.remove('playing'); if (deckEl) deckEl.classList.remove('playing');
document.body.classList.remove('playing-' + id); document.body.classList.remove('playing-' + id);
_notifyListenerDeckGlow();
console.log(`[Deck ${id}] Pause command sent to server`); console.log(`[Deck ${id}] Pause command sent to server`);
return; return;
} }
@ -863,6 +869,7 @@ function pauseDeck(id) {
const deckEl = document.getElementById('deck-' + id); const deckEl = document.getElementById('deck-' + id);
if (deckEl) deckEl.classList.remove('playing'); if (deckEl) deckEl.classList.remove('playing');
document.body.classList.remove('playing-' + id); document.body.classList.remove('playing-' + id);
_notifyListenerDeckGlow();
} }
@ -1774,6 +1781,12 @@ function updateGlowIntensity(val) {
document.documentElement.style.setProperty('--glow-spread', `${spread}px`); 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 // Dismiss landscape prompt
function dismissLandscapePrompt() { function dismissLandscapePrompt() {
const prompt = document.getElementById('landscape-prompt'); 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 /> <input id=\"password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" autofocus />
<button type=\"submit\">Unlock DJ Panel</button> <button type=\"submit\">Unlock DJ Panel</button>
{f"<div class='err'>{error}</div>" if error else ""} {f"<div class='err'>{error}</div>" if error else ""}
<div class=\"hint\">Set/disable this in config.json (dj_panel_password).</div>
</form> </form>
</div> </div>
</div> </div>
@ -736,6 +735,23 @@ def dj_start(data=None):
def dj_get_listener_count(): def dj_get_listener_count():
emit('listener_count', {'count': len(listener_sids)}) 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') @dj_socketio.on('stop_broadcast')
def dj_stop(): def dj_stop():
broadcast_state['active'] = False broadcast_state['active'] = False
@ -784,34 +800,49 @@ listener_socketio = SocketIO(
cors_allowed_origins=CONFIG_CORS, cors_allowed_origins=CONFIG_CORS,
async_mode='eventlet', async_mode='eventlet',
max_http_buffer_size=CONFIG_MAX_UPLOAD_MB * 1024 * 1024, max_http_buffer_size=CONFIG_MAX_UPLOAD_MB * 1024 * 1024,
ping_timeout=60, # Lower timeouts: stale connections detected in ~25s instead of ~85s
ping_interval=25, # 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, logger=CONFIG_DEBUG,
engineio_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') @listener_socketio.on('connect')
def listener_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') @listener_socketio.on('disconnect')
def listener_disconnect(): def listener_disconnect():
listener_sids.discard(request.sid) listener_sids.discard(request.sid)
count = len(listener_sids) count = _broadcast_listener_count()
print(f"REMOVED: Listener left. Total: {count}") print(f"REMOVED: Listener left {request.sid}. Total: {count}")
# Notify BOTH namespaces
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
dj_socketio.emit('listener_count', {'count': count}, namespace='/')
@listener_socketio.on('join_listener') @listener_socketio.on('join_listener')
def listener_join(): def listener_join():
if request.sid not in listener_sids: # SID already added in listener_connect(); just send stream status back
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='/')
emit('stream_status', {'active': broadcast_state['active']}) emit('stream_status', {'active': broadcast_state['active']})
@listener_socketio.on('get_listener_count') @listener_socketio.on('get_listener_count')
@ -833,12 +864,11 @@ def _transcoder_watchdog():
def _listener_count_sync_loop(): 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: 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) eventlet.sleep(5)
_broadcast_listener_count()
if __name__ == '__main__': if __name__ == '__main__':

View File

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