From 44b36bf08d820a678d4982c7d469cb7a776d41e2 Mon Sep 17 00:00:00 2001 From: ComputerTech Date: Thu, 12 Mar 2026 16:52:21 +0000 Subject: [PATCH] Comprehensive DJ Panel Improvements: - Added toast notification system for visible feedback - Implemented settings persistence via localStorage - Added auto-crossfade logic (smooth 5s transition) - Removed ~100 lines of dead SERVER_SIDE_AUDIO code - Fixed seekTo speed bug by capturing current playbackRate - Debounced drawWaveform with requestAnimationFrame - Added 'SETTINGS' header and close button to settings panel - Wired loadFromServer errors to toast notifications - Stacked control buttons vertically to clear crossfader --- index.html | 2 + script.js | 528 ++++++++++++++++++++++++++--------------------------- style.css | 54 ++++++ 3 files changed, 313 insertions(+), 271 deletions(-) diff --git a/index.html b/index.html index c43f50e..7d63851 100644 --- a/index.html +++ b/index.html @@ -484,6 +484,8 @@ onchange="handleFileUpload(event)"> +
+ \ No newline at end of file diff --git a/script.js b/script.js index 42795a6..ad1c35c 100644 --- a/script.js +++ b/script.js @@ -2,8 +2,6 @@ // TechDJ Pro - Core DJ Logic // ========================================== -// Server-side audio mode (true = server processes audio, false = browser processes) -const SERVER_SIDE_AUDIO = false; let audioCtx; const decks = { @@ -74,6 +72,42 @@ const queues = { B: [] }; +// Toast Notification System +function showToast(message, type) { + type = type || 'info'; + const container = document.getElementById('toast-container'); + if (!container) return; + const toast = document.createElement('div'); + toast.className = 'toast toast-' + type; + toast.textContent = message; + container.appendChild(toast); + setTimeout(function() { + if (toast.parentNode) toast.parentNode.removeChild(toast); + }, 4000); +} + +// Settings Persistence +function saveSettings() { + try { + localStorage.setItem('techdj_settings', JSON.stringify(settings)); + } catch (e) { /* quota exceeded or private browsing */ } +} + +function loadSettings() { + try { + var saved = localStorage.getItem('techdj_settings'); + if (saved) { + var parsed = JSON.parse(saved); + Object.keys(parsed).forEach(function(key) { + if (settings.hasOwnProperty(key)) settings[key] = parsed[key]; + }); + } + } catch (e) { /* corrupt data, ignore */ } +} + +// Restore saved settings on load +loadSettings(); + // System Initialization function initSystem() { if (audioCtx) return; @@ -594,9 +628,21 @@ function generateWaveformData(buffer) { return filteredData; } +// Debounce guard: prevents redundant redraws within the same frame +const _waveformPending = { A: false, B: false }; + function drawWaveform(id) { + if (_waveformPending[id]) return; + _waveformPending[id] = true; + requestAnimationFrame(() => { + _waveformPending[id] = false; + _drawWaveformImmediate(id); + }); +} + +function _drawWaveformImmediate(id) { const canvas = document.getElementById('waveform-' + id); - if (!canvas) return; // Null check + if (!canvas) return; const ctx = canvas.getContext('2d'); const data = decks[id].waveformData; if (!data) return; @@ -781,19 +827,6 @@ function _notifyListenerDeckGlow() { function playDeck(id) { vibrate(15); - // Server-side audio mode - if (SERVER_SIDE_AUDIO) { - socket.emit('audio_play', { deck: id }); - decks[id].playing = true; - 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; - } - - // Browser-side audio mode (original code) if (decks[id].type === 'local' && decks[id].localBuffer) { if (decks[id].playing) return; @@ -834,20 +867,6 @@ function playDeck(id) { function pauseDeck(id) { vibrate(15); - // Server-side audio mode - if (SERVER_SIDE_AUDIO) { - if (!socket) initSocket(); - socket.emit('audio_pause', { deck: id }); - decks[id].playing = false; - 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; - } - - // Browser-side audio mode (original code) if (decks[id].type === 'local' && decks[id].localSource && decks[id].playing) { if (!audioCtx) { console.warn(`[Deck ${id}] Cannot calculate pause position - audioCtx not initialised`); @@ -877,27 +896,6 @@ function seekTo(id, time) { // Update local state and timestamp for seek protection decks[id].lastSeekTime = Date.now(); - if (SERVER_SIDE_AUDIO) { - if (!socket) initSocket(); - socket.emit('audio_seek', { deck: id, position: time }); - - // Update local state immediately for UI responsiveness - decks[id].lastAnchorPosition = time; - if (!decks[id].playing) { - decks[id].pausedAt = time; - } - - // Update UI immediately (Optimistic UI) - const progress = (time / decks[id].duration) * 100; - const playhead = document.getElementById('playhead-' + id); - if (playhead) playhead.style.left = progress + '%'; - - const timer = document.getElementById('time-current-' + id); - if (timer) timer.textContent = formatTime(time); - - return; - } - if (!decks[id].localBuffer) { console.warn(`[Deck ${id}] Cannot seek - no buffer loaded`); return; @@ -907,6 +905,8 @@ function seekTo(id, time) { if (decks[id].playing) { if (decks[id].localSource) { try { + // Capture playback rate before stopping for the new source + decks[id]._lastPlaybackRate = decks[id].localSource.playbackRate.value; decks[id].localSource.stop(); decks[id].localSource.onended = null; } catch (e) { } @@ -922,17 +922,27 @@ function seekTo(id, time) { } src.connect(decks[id].filters.low); - const speedSlider = document.querySelector(`#deck-${id} .speed-slider`); - const speed = speedSlider ? parseFloat(speedSlider.value) : 1.0; + // Read current speed from old source before it was stopped, fall back to DOM slider + let speed = 1.0; + if (decks[id]._lastPlaybackRate != null) { + speed = decks[id]._lastPlaybackRate; + } else { + const speedSlider = document.querySelector(`#deck-${id} .speed-slider`); + if (speedSlider) speed = parseFloat(speedSlider.value); + } src.playbackRate.value = speed; decks[id].localSource = src; decks[id].lastAnchorTime = audioCtx.currentTime; decks[id].lastAnchorPosition = time; - // Add error handler for the source + // Wire onended so natural playback completion always triggers auto-play src.onended = () => { - console.log(`[Deck ${id}] Playback ended naturally`); + // Guard: only act if we didn't stop it intentionally + if (decks[id].playing && !decks[id].loading) { + console.log(`[Deck ${id}] Playback ended naturally (onended)`); + handleTrackEnd(id); + } }; src.start(0, time); @@ -960,14 +970,6 @@ function seekTo(id, time) { } function changeSpeed(id, val) { - // Server-side audio mode - if (SERVER_SIDE_AUDIO) { - if (!socket) initSocket(); - socket.emit('audio_set_pitch', { deck: id, pitch: parseFloat(val) }); - return; - } - - // Browser-side audio mode if (!audioCtx || !decks[id].localSource) return; const realElapsed = audioCtx.currentTime - decks[id].lastAnchorTime; @@ -978,28 +980,12 @@ function changeSpeed(id, val) { } function changeVolume(id, val) { - // Server-side audio mode - if (SERVER_SIDE_AUDIO) { - if (!socket) initSocket(); - socket.emit('audio_set_volume', { deck: id, volume: val / 100 }); - return; - } - - // Browser-side audio mode if (decks[id].volumeGain) { decks[id].volumeGain.gain.value = val / 100; } } function changeEQ(id, band, val) { - // Server-side audio mode - if (SERVER_SIDE_AUDIO) { - if (!socket) initSocket(); - socket.emit('audio_set_eq', { deck: id, band: band, value: parseFloat(val) }); - return; - } - - // Browser-side audio mode if (decks[id].filters[band]) decks[id].filters[band].gain.value = parseFloat(val); } @@ -1269,14 +1255,6 @@ function syncDecks(id) { } function updateCrossfader(val) { - // Server-side audio mode - if (SERVER_SIDE_AUDIO) { - if (!socket) initSocket(); - socket.emit('audio_set_crossfader', { value: parseInt(val) }); - return; - } - - // Browser-side audio mode const volA = (100 - val) / 100; const volB = val / 100; if (decks.A.crossfaderGain) decks.A.crossfaderGain.gain.value = volA; @@ -1411,19 +1389,6 @@ async function loadFromServer(id, url, title) { console.log(`[Deck ${id}] Loading: ${title} from ${url}`); - // Server-side audio mode: Send command immediately but CONTINUE for local UI/waveform - if (SERVER_SIDE_AUDIO) { - // Extract filename from URL and DECODE IT (for spaces etc) - const filename = decodeURIComponent(url.split('/').pop()); - - if (!socket) initSocket(); - socket.emit('audio_load_track', { deck: id, filename: filename }); - console.log(`[Deck ${id}] Load command sent to server: ${filename}`); - - // We DON'T return here anymore. We continue below to load for the UI. - } - - // Browser-side audio mode (original code) const wasPlaying = decks[id].playing; const wasBroadcasting = isBroadcasting; @@ -1524,6 +1489,7 @@ async function loadFromServer(id, url, title) { console.error(`[Deck ${id}] Load error:`, error); d.innerText = 'LOAD ERROR'; d.classList.remove('blink'); + showToast(`Deck ${id}: Failed to load track — ${error.message}`, 'error'); setTimeout(() => { d.innerText = 'NO TRACK'; }, 3000); } } @@ -1756,11 +1722,12 @@ function toggleRepeat(id, val) { console.log(`Deck ${id} Repeat: ${settings[`repeat${id}`]}`); vibrate(10); + saveSettings(); } -function toggleAutoMix(val) { settings.autoMix = val; } -function toggleShuffle(val) { settings.shuffleMode = val; } -function toggleQuantize(val) { settings.quantize = val; } -function toggleAutoPlay(val) { settings.autoPlay = val; } +function toggleAutoMix(val) { settings.autoMix = val; saveSettings(); } +function toggleShuffle(val) { settings.shuffleMode = val; saveSettings(); } +function toggleQuantize(val) { settings.quantize = val; saveSettings(); } +function toggleAutoPlay(val) { settings.autoPlay = val; saveSettings(); } function updateManualGlow(id, val) { settings[`glow${id}`] = val; @@ -1769,6 +1736,7 @@ function updateManualGlow(id, val) { } else { document.body.classList.remove(`playing-${id}`); } + saveSettings(); } function updateGlowIntensity(val) { @@ -1776,15 +1744,16 @@ function updateGlowIntensity(val) { const opacity = settings.glowIntensity / 100; const spread = (settings.glowIntensity / 100) * 80; - // Dynamically update CSS variables for the glow document.documentElement.style.setProperty('--glow-opacity', opacity); document.documentElement.style.setProperty('--glow-spread', `${spread}px`); + saveSettings(); } function updateListenerGlow(val) { settings.listenerGlowIntensity = parseInt(val); if (!socket) initSocket(); socket.emit('listener_glow', { intensity: settings.listenerGlowIntensity }); + saveSettings(); } // Dismiss landscape prompt @@ -1805,33 +1774,38 @@ window.addEventListener('DOMContentLoaded', () => { if (prompt) prompt.classList.add('dismissed'); } - // Initialise glow intensity + // Sync all restored settings to UI controls + const syncCheckbox = (elId, val) => { const el = document.getElementById(elId); if (el) el.checked = !!val; }; + const syncRange = (elId, val) => { const el = document.getElementById(elId); if (el && val != null) el.value = val; }; + + syncCheckbox('repeat-A', settings.repeatA); + syncCheckbox('repeat-B', settings.repeatB); + syncCheckbox('auto-mix', settings.autoMix); + syncCheckbox('shuffle-mode', settings.shuffleMode); + syncCheckbox('quantize', settings.quantize); + syncCheckbox('auto-play', settings.autoPlay); + syncCheckbox('glow-A', settings.glowA); + syncCheckbox('glow-B', settings.glowB); + syncRange('glow-intensity', settings.glowIntensity); + syncRange('listener-glow-intensity', settings.listenerGlowIntensity); + + // Initialise glow intensity CSS variables updateGlowIntensity(settings.glowIntensity); - const glowAToggle = document.getElementById('glow-A'); - if (glowAToggle) glowAToggle.checked = settings.glowA; - const glowBToggle = document.getElementById('glow-B'); - if (glowBToggle) glowBToggle.checked = settings.glowB; - const intensitySlider = document.getElementById('glow-intensity'); - if (intensitySlider) intensitySlider.value = settings.glowIntensity; // Apply initial glow state updateManualGlow('A', settings.glowA); updateManualGlow('B', settings.glowB); // Set stream URL in the streaming panel - // Priority: server-configured listener_url > auto-detect > fallback const streamInput = document.getElementById('stream-url'); if (streamInput) { const _autoDetectListenerUrl = () => { const host = window.location.hostname; - // dj.techy.music → techy.music (strip leading "dj.") - // dj.anything.com → anything.com if (host.startsWith('dj.')) { return `${window.location.protocol}//${host.slice(3)}`; } return `${window.location.protocol}//${host}:5001`; }; - // Try server-configured URL first fetch('/client_config') .then(r => r.json()) .then(cfg => { @@ -2031,24 +2005,7 @@ function startBroadcast() { return; } - // Server-side audio mode - if (SERVER_SIDE_AUDIO) { - isBroadcasting = true; - document.getElementById('broadcast-btn').classList.add('active'); - document.getElementById('broadcast-text').textContent = 'STOP BROADCAST'; - document.getElementById('broadcast-status').textContent = 'LIVE'; - document.getElementById('broadcast-status').classList.add('live'); - - if (!socket) initSocket(); - const bitrateValue = document.getElementById('stream-quality').value + 'k'; - socket.emit('start_broadcast', { bitrate: bitrateValue }); - socket.emit('get_listener_count'); - - console.log('[OK] Server-side broadcast started'); - return; - } - - // Browser-side audio mode (original code) + // Browser-side audio mode // Check if any audio is playing const anyPlaying = decks.A.playing || decks.B.playing; if (!anyPlaying) { @@ -2274,47 +2231,36 @@ function startBroadcast() { function stopBroadcast() { console.log('[BROADCAST] Stopping...'); - if (SERVER_SIDE_AUDIO) { - isBroadcasting = false; - if (socket) { - socket.emit('stop_broadcast'); - } - } else { - if (streamProcessor) { - streamProcessor.stop(); - streamProcessor = null; - } + if (streamProcessor) { + streamProcessor.stop(); + streamProcessor = null; + } - if (streamDestination) { - // Disconnect from stream destination and restore normal playback - if (decks.A.crossfaderGain) { - try { - decks.A.crossfaderGain.disconnect(streamDestination); - // Ensure connection to speakers is maintained - decks.A.crossfaderGain.disconnect(); - decks.A.crossfaderGain.connect(audioCtx.destination); - } catch (e) { - console.warn('Error restoring Deck A audio:', e); - } + if (streamDestination) { + if (decks.A.crossfaderGain) { + try { + decks.A.crossfaderGain.disconnect(streamDestination); + decks.A.crossfaderGain.disconnect(); + decks.A.crossfaderGain.connect(audioCtx.destination); + } catch (e) { + console.warn('Error restoring Deck A audio:', e); } - if (decks.B.crossfaderGain) { - try { - decks.B.crossfaderGain.disconnect(streamDestination); - // Ensure connection to speakers is maintained - decks.B.crossfaderGain.disconnect(); - decks.B.crossfaderGain.connect(audioCtx.destination); - } catch (e) { - console.warn('Error restoring Deck B audio:', e); - } - } - streamDestination = null; } - isBroadcasting = false; + if (decks.B.crossfaderGain) { + try { + decks.B.crossfaderGain.disconnect(streamDestination); + decks.B.crossfaderGain.disconnect(); + decks.B.crossfaderGain.connect(audioCtx.destination); + } catch (e) { + console.warn('Error restoring Deck B audio:', e); + } + } + streamDestination = null; + } + isBroadcasting = false; - // Notify server (browser-side mode also needs to tell server to stop relaying) - if (socket) { - socket.emit('stop_broadcast'); - } + if (socket) { + socket.emit('stop_broadcast'); } // Update UI @@ -2422,91 +2368,155 @@ window.addEventListener('load', () => { } }); -// Monitoring +// Auto-crossfade: smoothly transitions from the ending deck to the other deck. +let _autoMixTimer = null; + +function startAutoMixFade(endingDeckId) { + const otherDeck = endingDeckId === 'A' ? 'B' : 'A'; + + // The other deck must have a track loaded and be playing (or about to play) + if (!decks[otherDeck].localBuffer) { + console.log(`[AutoMix] Other deck ${otherDeck} has no track loaded, skipping crossfade`); + return false; + } + + // If the other deck isn't playing, start it + if (!decks[otherDeck].playing) { + playDeck(otherDeck); + } + + // Cancel any existing auto-mix animation + if (_autoMixTimer) { + clearInterval(_autoMixTimer); + _autoMixTimer = null; + } + + const slider = document.getElementById('crossfader'); + if (!slider) return false; + + const target = endingDeckId === 'A' ? 100 : 0; // Fade toward the OTHER deck + const duration = 5000; // 5 seconds + const steps = 50; + const interval = duration / steps; + const start = parseInt(slider.value); + const delta = (target - start) / steps; + let step = 0; + + console.log(`[AutoMix] Crossfading from Deck ${endingDeckId} → Deck ${otherDeck} over ${duration / 1000}s`); + showToast(`Auto-crossfading to Deck ${otherDeck}`, 'info'); + + _autoMixTimer = setInterval(() => { + step++; + const val = Math.round(start + delta * step); + slider.value = val; + updateCrossfader(val); + + if (step >= steps) { + clearInterval(_autoMixTimer); + _autoMixTimer = null; + console.log(`[AutoMix] Crossfade complete. Now on Deck ${otherDeck}`); + + // Load next track on the ending deck so it's ready for the next crossfade + if (queues[endingDeckId] && queues[endingDeckId].length > 0) { + const next = queues[endingDeckId].shift(); + renderQueue(endingDeckId); + loadFromServer(endingDeckId, next.file, next.title); + } + } + }, interval); + + return true; +} + +// Shared handler called both by onended and the monitor poll. +// Handles repeat, auto-crossfade, auto-play-from-queue, or stop. +function handleTrackEnd(id) { + // Already being handled or loop is active — skip + if (decks[id].loading || decks[id].loopActive) return; + + if (isBroadcasting) { + console.log(`Track ending during broadcast on Deck ${id}`); + if (settings[`repeat${id}`]) { + console.log(`Repeating track on Deck ${id} (broadcast)`); + seekTo(id, 0); + return; + } + if (settings.autoPlay && queues[id] && queues[id].length > 0) { + decks[id].loading = true; + const next = queues[id].shift(); + renderQueue(id); + loadFromServer(id, next.file, next.title) + .then(() => { decks[id].loading = false; playDeck(id); }) + .catch(() => { decks[id].loading = false; }); + } + return; + } + + if (settings[`repeat${id}`]) { + console.log(`Repeating track on Deck ${id}`); + seekTo(id, 0); + } else if (settings.autoMix) { + // Auto-crossfade takes priority over simple auto-play + decks[id].loading = true; + if (!startAutoMixFade(id)) { + // Crossfade not possible (other deck empty) — fall through to normal auto-play + decks[id].loading = false; + handleAutoPlay(id); + } else { + decks[id].loading = false; + } + } else if (settings.autoPlay) { + handleAutoPlay(id); + } else { + pauseDeck(id); + decks[id].pausedAt = 0; + } +} + +function handleAutoPlay(id) { + decks[id].loading = true; + pauseDeck(id); + + if (queues[id] && queues[id].length > 0) { + console.log(`Auto-play: loading next from Queue ${id}`); + const next = queues[id].shift(); + renderQueue(id); + loadFromServer(id, next.file, next.title) + .then(() => { decks[id].loading = false; playDeck(id); }) + .catch(() => { decks[id].loading = false; }); + } else { + console.log(`Auto-play: queue empty on Deck ${id}, stopping`); + decks[id].loading = false; + decks[id].pausedAt = 0; + } +} + +// Monitoring — safety net for cases where onended doesn't fire +// (e.g. AudioContext suspended, very short buffer, scrub to near-end). function monitorTrackEnd() { setInterval(() => { - if (!audioCtx) return; // Safety check + if (!audioCtx) return; + - // In server-side mode, poll for status from server - if (SERVER_SIDE_AUDIO && socket && socket.connected) { - socket.emit('get_mixer_status'); - } ['A', 'B'].forEach(id => { - if (decks[id].playing && decks[id].localBuffer && !decks[id].loading) { - const playbackRate = decks[id].localSource ? decks[id].localSource.playbackRate.value : 1.0; - const realElapsed = audioCtx.currentTime - decks[id].lastAnchorTime; - const current = decks[id].lastAnchorPosition + (realElapsed * playbackRate); - const remaining = decks[id].duration - current; + if (!decks[id].playing || !decks[id].localBuffer || decks[id].loading) return; + if (decks[id].loopActive) return; - // If end reached (with 0.5s buffer for safety) - // Skip if a loop is active — the Web Audio API handles looping natively - // and lastAnchorPosition is not updated on each native loop repeat, so - // the monotonically-growing `current` would incorrectly trigger end-of-track. - if (remaining <= 0.5 && !decks[id].loopActive) { - // During broadcast, still handle auto-play/queue to avoid dead air - if (isBroadcasting) { - console.log(`Track ending during broadcast on Deck ${id}`); - if (settings[`repeat${id}`]) { - console.log(`LOOP Repeating track on Deck ${id}`); - seekTo(id, 0); - return; - } - // Auto-play from queue during broadcast to maintain stream - if (settings.autoPlay && queues[id] && queues[id].length > 0) { - decks[id].loading = true; - console.log(`Auto-play (broadcast): Loading next from Queue ${id}...`); - const next = queues[id].shift(); - renderQueue(id); - loadFromServer(id, next.file, next.title).then(() => { - decks[id].loading = false; - playDeck(id); - }).catch(() => { - decks[id].loading = false; - }); - return; - } - // No repeat, no queue - just let the stream continue silently - return; - } + const rate = decks[id].localSource ? decks[id].localSource.playbackRate.value : 1.0; + const elapsed = audioCtx.currentTime - decks[id].lastAnchorTime; + const current = decks[id].lastAnchorPosition + (elapsed * rate); + const remaining = decks[id].duration - current; - if (settings[`repeat${id}`]) { - // Full song repeat - console.log(`LOOP Repeating track on Deck ${id}`); - seekTo(id, 0); - } else if (settings.autoPlay) { - // Prevent race condition - decks[id].loading = true; - pauseDeck(id); - - // Check queue for auto-play - if (queues[id] && queues[id].length > 0) { - console.log(`Auto-play: Loading next from Queue ${id}...`); - const next = queues[id].shift(); - renderQueue(id); // Update queue UI - - loadFromServer(id, next.file, next.title).then(() => { - decks[id].loading = false; - playDeck(id); - }).catch(() => { - decks[id].loading = false; - }); - } else { - // No queue - just stop - console.log(`Track ended, queue empty - stopping playback`); - decks[id].loading = false; - pauseDeck(id); - decks[id].pausedAt = 0; - } - } else { - // Just stop if no auto-play - pauseDeck(id); - decks[id].pausedAt = 0; - } - } + // Threshold scales with playback rate so we don't miss fast playback. + // Use 1.5× the poll interval (0.75s) as a safe window. + const threshold = Math.max(0.75, 0.75 * rate); + if (remaining <= threshold) { + console.log(`[Monitor] Deck ${id} near end (${remaining.toFixed(2)}s left) — triggering handleTrackEnd`); + handleTrackEnd(id); } }); - }, 500); // Check every 0.5s + }, 500); } monitorTrackEnd(); @@ -2602,11 +2612,6 @@ function addToQueue(deckId, file, title) { queues[deckId].push({ file, title }); renderQueue(deckId); console.log(`Added "${title}" to Queue ${deckId} (${queues[deckId].length} tracks)`); - - // Sync with server if in server-side mode - if (SERVER_SIDE_AUDIO && socket) { - socket.emit('audio_sync_queue', { deck: deckId, tracks: queues[deckId] }); - } } // Remove track from queue @@ -2614,11 +2619,6 @@ function removeFromQueue(deckId, index) { const removed = queues[deckId].splice(index, 1)[0]; renderQueue(deckId); console.log(`Removed "${removed.title}" from Queue ${deckId}`); - - // Sync with server if in server-side mode - if (SERVER_SIDE_AUDIO && socket) { - socket.emit('audio_sync_queue', { deck: deckId, tracks: queues[deckId] }); - } } // Clear entire queue @@ -2627,11 +2627,6 @@ function clearQueue(deckId) { queues[deckId] = []; renderQueue(deckId); console.log(`Cleared Queue ${deckId} (${count} tracks removed)`); - - // Sync with server if in server-side mode - if (SERVER_SIDE_AUDIO && socket) { - socket.emit('audio_sync_queue', { deck: deckId, tracks: queues[deckId] }); - } } // Load next track from queue @@ -2646,11 +2641,6 @@ function loadNextFromQueue(deckId) { loadFromServer(deckId, next.file, next.title); renderQueue(deckId); - // Sync with server if in server-side mode (after shift) - if (SERVER_SIDE_AUDIO && socket) { - socket.emit('audio_sync_queue', { deck: deckId, tracks: queues[deckId] }); - } - return true; } @@ -2748,10 +2738,6 @@ function renderQueue(deckId) { const [movedItem] = queues[deckId].splice(fromIndex, 1); queues[deckId].splice(index, 0, movedItem); renderQueue(deckId); - - if (SERVER_SIDE_AUDIO && socket) { - socket.emit('audio_sync_queue', { deck: deckId, tracks: queues[deckId] }); - } } }; diff --git a/style.css b/style.css index 9e62b2a..61990f1 100644 --- a/style.css +++ b/style.css @@ -4782,3 +4782,57 @@ body.listening-active .landscape-prompt { background: rgba(188, 19, 254, 0.2); box-shadow: 0 0 10px rgba(188, 19, 254, 0.4); } + +/* Toast Notifications */ +.toast-container { + position: fixed; + bottom: 30px; + left: 30px; + z-index: 20000; + display: flex; + flex-direction: column; + gap: 8px; + pointer-events: none; +} + +.toast { + padding: 12px 20px; + border-radius: 6px; + font-family: 'Rajdhani', sans-serif; + font-size: 0.9rem; + font-weight: 500; + color: #fff; + pointer-events: auto; + animation: toast-in 0.3s ease-out, toast-out 0.3s ease-in 3.7s forwards; + max-width: 360px; + word-break: break-word; + border-left: 4px solid transparent; +} + +.toast-error { + background: rgba(255, 40, 40, 0.9); + border-left-color: #ff0000; + box-shadow: 0 4px 20px rgba(255, 0, 0, 0.3); +} + +.toast-success { + background: rgba(0, 200, 80, 0.9); + border-left-color: #00ff55; + box-shadow: 0 4px 20px rgba(0, 255, 85, 0.3); +} + +.toast-info { + background: rgba(0, 120, 255, 0.9); + border-left-color: var(--primary-cyan); + box-shadow: 0 4px 20px rgba(0, 243, 255, 0.3); +} + +@keyframes toast-in { + from { transform: translateX(-100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +@keyframes toast-out { + from { transform: translateX(0); opacity: 1; } + to { transform: translateX(-100%); opacity: 0; } +}