// Global variables let isDragging = false; let currentStatus = null; let currentVolume = 100; let isUpdatingVolume = false; // Progress Bar Variables let localElapsed = 0; let totalDuration = 0; let isPlaying = false; // WebSocket Variables let ws = null; let wsConnectedOnce = false; // DOM Elements const statusDot = document.getElementById('status-dot'); const statusText = document.getElementById('status-text'); const voiceSelect = document.getElementById('voice-channel-select'); const textChannelSelect = document.getElementById('text-channel-select'); const btnConnect = document.getElementById('btn-connect'); const trackTitle = document.getElementById('track-title'); const trackArtist = document.getElementById('track-artist'); const trackLink = document.getElementById('track-link'); const loopBadge = document.getElementById('loop-badge'); const btnPlayPause = document.getElementById('btn-play-pause'); const playPauseIcon = document.getElementById('play-pause-icon'); const btnSkip = document.getElementById('btn-skip'); const btnStop = document.getElementById('btn-stop'); const btnLoop = document.getElementById('btn-loop'); const btnLoopPlaylist = document.getElementById('btn-loop-playlist'); const vinylRecord = document.getElementById('vinyl-record'); const musicWave = document.getElementById('music-wave'); const volumeSlider = document.getElementById('volume-slider'); const volumeIcon = document.getElementById('volume-icon'); const volumeValue = document.getElementById('volume-value'); const addForm = document.getElementById('add-track-form'); const btnAddTrack = document.getElementById('btn-add-track'); const btnAddText = document.getElementById('btn-add-text'); const btnAddSpinner = document.getElementById('btn-add-spinner'); const trackQueryInput = document.getElementById('track-query'); const queueList = document.getElementById('queue-list'); const emptyQueueMsg = document.getElementById('empty-queue-msg'); const queueCountBadge = document.getElementById('queue-count'); const toastContainer = document.getElementById('toast-container'); const fileInput = document.getElementById('file-input'); const btnSelectFile = document.getElementById('btn-select-file'); const uploadProgressContainer = document.getElementById('upload-progress-container'); const uploadProgressFill = document.getElementById('upload-progress-fill'); const uploadStatusText = document.getElementById('upload-status-text'); const uploadsList = document.getElementById('uploads-list'); const emptyUploadsMsg = document.getElementById('empty-uploads-msg'); const uploadsCountBadge = document.getElementById('uploads-count'); const historyList = document.getElementById('history-list'); const emptyHistoryMsg = document.getElementById('empty-history-msg'); const historyCountBadge = document.getElementById('history-count'); // Progress Bar DOM Elements const progressContainer = document.getElementById('progress-container'); const timeElapsed = document.getElementById('time-elapsed'); const timeTotal = document.getElementById('time-total'); const progressBarFill = document.getElementById('progress-bar-fill'); // Connect to WebSocket server for real-time events function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws`; console.log(`Próba połączenia z WebSocket: ${wsUrl}`); ws = new WebSocket(wsUrl); ws.onopen = () => { console.log('Połączono z WebSocket'); if (wsConnectedOnce) { showToast('Połączenie WebSocket przywrócone (Live)!', 'success'); } else { showToast('WebSocket zsynchronizowany (Live)!', 'success'); wsConnectedOnce = true; } }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); // Only update UI if we are not currently dragging queue items if (!isDragging) { currentStatus = data; updateUI(data); } } catch (err) { console.error('Błąd parsowania danych z WS:', err); } }; ws.onclose = () => { console.log('Połączenie WebSocket zamknięte. Próba ponownego połączenia...'); statusDot.className = 'status-indicator offline'; statusText.textContent = 'Rozłączono (brak połączenia z WS)'; // Reconnect after 3 seconds setTimeout(connectWebSocket, 3000); }; ws.onerror = (err) => { console.error('Błąd WebSocket:', err); ws.close(); }; } // Initialize document.addEventListener('DOMContentLoaded', () => { // Connect to WebSocket instead of polling connectWebSocket(); fetchUploads(); // Start Smooth Progress Tracker (every 100ms) setInterval(() => { if (isPlaying && currentStatus && currentStatus.current) { localElapsed += 0.1; if (totalDuration > 0 && localElapsed > totalDuration) { localElapsed = totalDuration; } updateProgressBar(); } }, 100); // Bind Event Listeners bindEvents(); }); // Bind UI Events function bindEvents() { // Connection Button btnConnect.addEventListener('click', toggleConnection); // Playback Controls btnPlayPause.addEventListener('click', togglePlayPause); btnSkip.addEventListener('click', skipTrack); btnStop.addEventListener('click', stopPlayback); btnLoop.addEventListener('click', toggleLoop); if (btnLoopPlaylist) { btnLoopPlaylist.addEventListener('click', toggleLoopPlaylist); } // Volume slider volumeSlider.addEventListener('input', (e) => { isUpdatingVolume = true; const val = e.target.value; volumeValue.textContent = `${val}%`; updateVolumeIcon(val); }); volumeSlider.addEventListener('change', (e) => { const val = parseInt(e.target.value); setVolume(val); setTimeout(() => { isUpdatingVolume = false; }, 500); }); // Add Song Form addForm.addEventListener('submit', addTrack); // Text Channel Select textChannelSelect.addEventListener('change', changeTextChannel); // Upload Events if (btnSelectFile && fileInput) { btnSelectFile.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', handleFileUpload); } } // Show Toast Notification function showToast(message, type = 'success') { const toast = document.createElement('div'); toast.className = `toast ${type}`; let icon = 'info'; if (type === 'success') icon = 'check-circle-2'; if (type === 'error') icon = 'alert-triangle'; toast.innerHTML = ` ${message}`; toastContainer.appendChild(toast); lucide.createIcons(); // Remove after 3.5 seconds setTimeout(() => { toast.style.transform = 'translateY(-20px)'; toast.style.opacity = '0'; setTimeout(() => { toast.remove(); }, 300); }, 3500); } // Update Volume Icon function updateVolumeIcon(vol) { vol = parseInt(vol); if (vol === 0) { volumeIcon.setAttribute('data-lucide', 'volume-x'); } else if (vol < 35) { volumeIcon.setAttribute('data-lucide', 'volume'); } else if (vol < 75) { volumeIcon.setAttribute('data-lucide', 'volume-1'); } else { volumeIcon.setAttribute('data-lucide', 'volume-2'); } lucide.createIcons(); } // API Calls async function fetchStatus() { try { const res = await fetch('/api/status'); if (!res.ok) throw new Error('API Error'); const data = await res.json(); currentStatus = data; updateUI(data); } catch (err) { console.error('Błąd pobierania statusu:', err); statusDot.className = 'status-indicator offline'; statusText.textContent = 'Błąd połączenia z API'; } } // Progress Bar Helpers function formatTime(secs) { if (secs === null || secs === undefined || isNaN(secs) || secs < 0) return "0:00"; const h = Math.floor(secs / 3600); const m = Math.floor((secs % 3600) / 60); const s = Math.floor(secs % 60); const formattedSecs = s < 10 ? `0${s}` : s; if (h > 0) { const formattedMins = m < 10 ? `0${m}` : m; return `${h}:${formattedMins}:${formattedSecs}`; } return `${m}:${formattedSecs}`; } function updateProgressBar() { timeElapsed.textContent = formatTime(localElapsed); if (totalDuration > 0) { const pct = Math.min((localElapsed / totalDuration) * 100, 100); progressBarFill.style.width = `${pct}%`; } } // Update UI States function updateUI(status) { // 1. Connection Status & Available Channels if (status.active_voice_channel) { statusDot.className = 'status-indicator online'; statusText.textContent = `Połączony: ${status.active_voice_channel.channel_name} (${status.active_voice_channel.guild_name})`; btnConnect.innerHTML = ' Odłącz'; btnConnect.className = 'btn btn-danger'; } else { statusDot.className = 'status-indicator offline'; statusText.textContent = 'Rozłączony'; btnConnect.innerHTML = ' Połącz'; btnConnect.className = 'btn btn-primary btn-glow'; } // Voice selector population (only if select doesn't have focus/interaction) if (document.activeElement !== voiceSelect) { // Keep selected value const currentSelected = voiceSelect.value; // Clear voiceSelect.innerHTML = ''; if (status.voice_channels && status.voice_channels.length > 0) { status.voice_channels.forEach(channel => { const opt = document.createElement('option'); opt.value = channel.channel_id; opt.textContent = `${channel.guild_name} - ${channel.channel_name}`; voiceSelect.appendChild(opt); }); // Re-select or select first/active if (status.active_voice_channel) { voiceSelect.value = status.active_voice_channel.channel_id; } else if (currentSelected && status.voice_channels.some(c => c.channel_id === currentSelected)) { voiceSelect.value = currentSelected; } else { voiceSelect.value = status.voice_channels[0].channel_id; } } else { const opt = document.createElement('option'); opt.value = ""; opt.textContent = "Brak dostępnych kanałów"; voiceSelect.appendChild(opt); } } // Text selector population (only if select doesn't have focus/interaction) if (document.activeElement !== textChannelSelect) { const currentSelectedText = textChannelSelect.value; textChannelSelect.innerHTML = ''; if (status.text_channels && status.text_channels.length > 0) { status.text_channels.forEach(channel => { const opt = document.createElement('option'); opt.value = channel.channel_id; opt.textContent = `${channel.guild_name} - #${channel.channel_name}`; textChannelSelect.appendChild(opt); }); if (status.active_text_channel_id) { textChannelSelect.value = status.active_text_channel_id; } else if (currentSelectedText && status.text_channels.some(c => c.channel_id === currentSelectedText)) { textChannelSelect.value = currentSelectedText; } } else { const opt = document.createElement('option'); opt.value = ""; opt.textContent = "Brak dostępnych kanałów"; textChannelSelect.appendChild(opt); } } // 2. Playback State if (status.current) { trackTitle.textContent = status.current.title; trackArtist.textContent = `Wyszukano/URL: ${status.current.url ? 'YouTube Video' : 'Nieznany'}`; trackLink.href = status.current.url || '#'; trackLink.style.display = status.current.url ? 'inline-flex' : 'none'; // Sync progress bar values totalDuration = status.current.duration || 0; localElapsed = status.current.elapsed || 0; isPlaying = status.playing; // Update playing state // Show progress bar progressContainer.style.display = 'flex'; timeElapsed.textContent = formatTime(localElapsed); if (totalDuration > 0) { timeTotal.textContent = formatTime(totalDuration); const pct = Math.min((localElapsed / totalDuration) * 100, 100); progressBarFill.style.width = `${pct}%`; } else { timeTotal.textContent = "Live"; progressBarFill.style.width = "100%"; } // Vinyl animation if (status.playing) { vinylRecord.classList.add('playing'); musicWave.classList.add('active'); } else { vinylRecord.classList.remove('playing'); musicWave.classList.remove('active'); } } else { trackTitle.textContent = 'Nic nie jest odtwarzane'; trackArtist.textContent = 'Połącz bota i dodaj utwór'; trackLink.style.display = 'none'; vinylRecord.classList.remove('playing'); musicWave.classList.remove('active'); // Hide progress bar progressContainer.style.display = 'none'; isPlaying = false; localElapsed = 0; totalDuration = 0; } // 3. Play/Pause Button State if (status.playing) { playPauseIcon.setAttribute('data-lucide', 'pause'); btnPlayPause.title = "Wstrzymaj"; } else { playPauseIcon.setAttribute('data-lucide', 'play'); btnPlayPause.title = "Odtwarzaj"; } // 4. Volume State if (!isUpdatingVolume) { volumeSlider.value = status.volume; volumeValue.textContent = `${status.volume}%`; updateVolumeIcon(status.volume); } // 5. Loop State if (status.loop_single) { loopBadge.style.display = 'flex'; btnLoop.classList.add('active'); btnLoop.style.background = 'var(--primary)'; btnLoop.style.color = 'var(--bg-card)'; } else { loopBadge.style.display = 'none'; btnLoop.classList.remove('active'); btnLoop.style.background = ''; btnLoop.style.color = ''; } if (btnLoopPlaylist) { if (status.loop_playlist) { btnLoopPlaylist.classList.add('active'); btnLoopPlaylist.style.background = 'var(--primary)'; btnLoopPlaylist.style.color = 'var(--bg-card)'; } else { btnLoopPlaylist.classList.remove('active'); btnLoopPlaylist.style.background = ''; btnLoopPlaylist.style.color = ''; } } // 6. Queue render (if not currently dragging) if (!isDragging) { renderQueue(status.queue); renderHistory(status.history || []); } // Re-trigger lucide icons update lucide.createIcons(); } // Render Queue Items list function renderQueue(queue) { // Clear list but keep empty queue message const items = queueList.querySelectorAll('.queue-item'); items.forEach(el => el.remove()); if (!queue || queue.length === 0) { emptyQueueMsg.style.display = 'flex'; queueCountBadge.textContent = '0 utworów'; return; } emptyQueueMsg.style.display = 'none'; queueCountBadge.textContent = `${queue.length} ${queue.length === 1 ? 'utwór' : queue.length < 5 ? 'utwory' : 'utworów'}`; queue.forEach((track, idx) => { const li = document.createElement('li'); li.className = 'queue-item'; li.setAttribute('draggable', 'true'); li.setAttribute('data-id', track.id); li.setAttribute('data-index', idx); li.innerHTML = `