UI improvements: glow effects, deck colors, listener count, remove black bar, mobile header fix
This commit is contained in:
parent
6027f2e973
commit
2e64870daa
11
index.html
11
index.html
|
|
@ -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;">
|
||||
|
|
|
|||
43
listener.css
43
listener.css
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
19
script.js
19
script.js
|
|
@ -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');
|
||||
|
|
|
|||
70
server.py
70
server.py
|
|
@ -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__':
|
||||
|
|
|
|||
27
style.css
27
style.css
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue