// 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 btnOpenSettings = document.getElementById('btn-open-settings'); const btnCloseSettings = document.getElementById('btn-close-settings'); const settingsModal = document.getElementById('settings-modal'); const settingsGuildsList = document.getElementById('settings-guilds-list'); 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 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 btnOpenAllUploads = document.getElementById('btn-open-all-uploads'); const btnAllUploadsCount = document.getElementById('btn-all-uploads-count'); const uploadsModal = document.getElementById('uploads-modal'); const btnCloseUploads = document.getElementById('btn-close-uploads'); const modalUploadsList = document.getElementById('modal-uploads-list'); const emptyModalUploadsMsg = document.getElementById('empty-modal-uploads-msg'); 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); // 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); // Settings Modal Events if (btnOpenSettings) btnOpenSettings.addEventListener('click', openSettingsModal); if (btnCloseSettings) btnCloseSettings.addEventListener('click', closeSettingsModal); if (settingsModal) { settingsModal.addEventListener('click', (e) => { if (e.target === settingsModal) closeSettingsModal(); }); } const btnSaveDefaultPlays = document.getElementById('btn-save-default-plays'); if (btnSaveDefaultPlays) { btnSaveDefaultPlays.addEventListener('click', async () => { const inputDefaultPlays = document.getElementById('setting-default-plays'); if (!inputDefaultPlays) return; const val = parseInt(inputDefaultPlays.value, 10); if (isNaN(val) || val < 1 || val > 999) { showToast('Wartość musi być z przedziału 1-999', 'error'); return; } try { const res = await fetch('/api/settings/default_plays', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ default_plays: val }) }); if (!res.ok) throw new Error('Failed to save setting'); showToast(`Domyślna liczba odtworzeń: ${val}`, 'success'); } catch (err) { showToast('Błąd zapisu ustawienia', 'error'); } }); } // Uploads Modal Events if (btnOpenAllUploads) btnOpenAllUploads.addEventListener('click', openUploadsModal); if (btnCloseUploads) btnCloseUploads.addEventListener('click', closeUploadsModal); if (uploadsModal) { uploadsModal.addEventListener('click', (e) => { if (e.target === uploadsModal) closeUploadsModal(); }); } // 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) { currentStatus = 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); } } // Settings Guilds Population if (status.guilds) { renderSettingsGuilds(status.guilds); } if (status.default_plays !== undefined) { const inputDefaultPlays = document.getElementById('setting-default-plays'); if (inputDefaultPlays && document.activeElement !== inputDefaultPlays) { inputDefaultPlays.value = status.default_plays; } } // 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 const targetIcon = status.playing ? 'pause' : 'play'; if (btnPlayPause.dataset.state !== targetIcon) { btnPlayPause.dataset.state = targetIcon; btnPlayPause.innerHTML = ``; btnPlayPause.title = status.playing ? "Wstrzymaj" : "Odtwarzaj"; if (window.lucide) { lucide.createIcons(); } } // 4. Volume State if (!isUpdatingVolume) { volumeSlider.value = status.volume; volumeValue.textContent = `${status.volume}%`; updateVolumeIcon(status.volume); } // 5. Loop State (Combined toggle: Off -> Playlist -> Single) let loopMode = 'off'; if (status.loop_single) loopMode = 'single'; else if (status.loop_playlist) loopMode = 'playlist'; if (btnLoop.dataset.loopMode !== loopMode) { btnLoop.dataset.loopMode = loopMode; if (loopMode === 'single') { btnLoop.innerHTML = ''; btnLoop.classList.add('active'); btnLoop.style.background = 'var(--primary)'; btnLoop.style.color = 'var(--bg-card)'; btnLoop.title = "Zapętlenie: jeden utwór (kliknij, aby wyłączyć)"; loopBadge.style.display = 'flex'; loopBadge.innerHTML = ' Zapętlenie utworu'; } else if (loopMode === 'playlist') { btnLoop.innerHTML = ''; btnLoop.classList.add('active'); btnLoop.style.background = 'var(--primary)'; btnLoop.style.color = 'var(--bg-card)'; btnLoop.title = "Zapętlenie: cała kolejka (kliknij, aby zapętlić jeden utwór)"; loopBadge.style.display = 'flex'; loopBadge.innerHTML = ' Zapętlenie kolejki'; } else { btnLoop.innerHTML = ''; btnLoop.classList.remove('active'); btnLoop.style.background = ''; btnLoop.style.color = ''; btnLoop.title = "Zapętlenie: wyłączone (kliknij, aby zapętlić kolejkę)"; loopBadge.style.display = 'none'; } if (window.lucide) { lucide.createIcons(); } } // 6. Queue render (if not currently dragging and not editing track plays input) if (!isDragging && (!document.activeElement || !document.activeElement.classList.contains('track-plays-input'))) { renderQueue(status.queue, status.queue_index); renderHistory(status.history || []); } // Re-trigger lucide icons update lucide.createIcons(); } // Render Queue Items list function renderQueue(queue, queueIndex = -1) { // 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'); let classes = 'queue-item'; let indexHtml = `
${idx + 1}
`; if (idx === queueIndex) { classes += ' playing-item'; indexHtml = `
`; } else if (idx < queueIndex) { classes += ' played-item'; } li.className = classes; li.setAttribute('draggable', 'true'); li.setAttribute('data-id', track.id); li.setAttribute('data-index', idx); li.innerHTML = `
${indexHtml}
${track.title}
Pozostało:
`; // Add Drag and Drop listeners li.addEventListener('dragstart', handleDragStart); li.addEventListener('dragenter', handleDragEnter); li.addEventListener('dragover', handleDragOver); li.addEventListener('dragleave', handleDragLeave); li.addEventListener('drop', handleDrop); li.addEventListener('dragend', handleDragEnd); queueList.appendChild(li); }); } // Drag & Drop Handlers let dragSrcEl = null; function handleDragStart(e) { this.classList.add('dragging'); isDragging = true; dragSrcEl = this; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', this.getAttribute('data-id')); } function handleDragEnter(e) { if (this !== dragSrcEl) { this.classList.add('drag-over'); } } function handleDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; return false; } function handleDragLeave(e) { this.classList.remove('drag-over'); } function handleDrop(e) { e.stopPropagation(); e.preventDefault(); this.classList.remove('drag-over'); if (dragSrcEl !== this) { const listItems = Array.from(queueList.querySelectorAll('.queue-item')); const fromIdx = parseInt(dragSrcEl.getAttribute('data-index')); const toIdx = parseInt(this.getAttribute('data-index')); // Local state swap for speed const srcId = dragSrcEl.getAttribute('data-id'); const targetId = this.getAttribute('data-id'); // Re-arrange DOM locally for visual feedback if (fromIdx < toIdx) { this.parentNode.insertBefore(dragSrcEl, this.nextSibling); } else { this.parentNode.insertBefore(dragSrcEl, this); } // Recalculate index numbers locally const updatedItems = Array.from(queueList.querySelectorAll('.queue-item')); const newOrderIds = updatedItems.map(item => item.getAttribute('data-id')); // Send to backend sendReorder(newOrderIds); } return false; } function handleDragEnd(e) { this.classList.remove('dragging'); const items = queueList.querySelectorAll('.queue-item'); items.forEach(item => { item.classList.remove('drag-over'); }); isDragging = false; } // Manual Up/Down Buttons function moveItemUp(idx) { if (idx === 0) return; const items = Array.from(queueList.querySelectorAll('.queue-item')); const ids = items.map(item => item.getAttribute('data-id')); // Swap elements in ids const temp = ids[idx]; ids[idx] = ids[idx - 1]; ids[idx - 1] = temp; sendReorder(ids); } function moveItemDown(idx) { const items = Array.from(queueList.querySelectorAll('.queue-item')); if (idx === items.length - 1) return; const ids = items.map(item => item.getAttribute('data-id')); // Swap elements in ids const temp = ids[idx]; ids[idx] = ids[idx + 1]; ids[idx + 1] = temp; sendReorder(ids); } async function sendReorder(queueIds) { try { const res = await fetch('/api/reorder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ queue_ids: queueIds }) }); if (!res.ok) throw new Error('Reorder failed'); showToast('Zmieniono kolejność utworów!', 'success'); } catch (err) { showToast('Nie udało się zapisać kolejności', 'error'); } } // Add Track to Queue async function addTrack(e) { e.preventDefault(); const query = trackQueryInput.value.trim(); if (!query) return; // Check if bot is connected if (!currentStatus || !currentStatus.active_voice_channel) { showToast('Bot musi być połączony z kanałem głosowym!', 'error'); return; } // Show spinner btnAddText.style.display = 'none'; btnAddSpinner.style.display = 'inline-block'; btnAddTrack.disabled = true; trackQueryInput.disabled = true; try { const res = await fetch('/api/play', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: query }) }); const data = await res.json(); if (!res.ok) { throw new Error(data.detail || 'Failed to add'); } showToast(`Dodano utwór do kolejki!`, 'success'); trackQueryInput.value = ''; } catch (err) { showToast(`Błąd dodawania: ${err.message}`, 'error'); } finally { btnAddText.style.display = 'inline-flex'; btnAddSpinner.style.display = 'none'; btnAddTrack.disabled = false; trackQueryInput.disabled = false; } } // Remove Track async function removeTrack(id) { try { const res = await fetch('/api/remove', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: id }) }); if (!res.ok) throw new Error('Remove failed'); showToast('Usunięto utwór z kolejki', 'success'); } catch (err) { showToast('Nie udało się usunąć utworu', 'error'); } } // Jump to specific track index in queue async function jumpToTrack(idx) { try { const res = await fetch('/api/jump', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ index: idx }) }); if (!res.ok) throw new Error('Jump failed'); showToast('Zmieniono odtwarzany utwór', 'info'); } catch (err) { showToast('Nie udało się zmienić utworu', 'error'); } } // Toggle Connection async function toggleConnection() { if (currentStatus && currentStatus.active_voice_channel) { // Disconnect try { const res = await fetch('/api/disconnect', { method: 'POST' }); if (!res.ok) throw new Error('Disconnect failed'); showToast('Rozłączono z kanału głosowego', 'info'); } catch (err) { showToast('Błąd rozłączania', 'error'); } } else { // Connect const channelId = voiceSelect.value; if (!channelId) { showToast('Wybierz kanał głosowy z listy!', 'error'); return; } try { const res = await fetch('/api/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel_id: channelId }) }); if (!res.ok) throw new Error('Connect failed'); showToast('Połączono z kanałem głosowym!', 'success'); } catch (err) { showToast('Nie udało się połączyć', 'error'); } } } // Toggle Play / Pause async function togglePlayPause() { if (!currentStatus) return; const endpoint = currentStatus.playing ? '/api/pause' : '/api/resume'; try { const res = await fetch(endpoint, { method: 'POST' }); if (!res.ok) throw new Error('Action failed'); showToast(currentStatus.playing ? 'Wstrzymano odtwarzanie' : 'Wznowiono odtwarzanie', 'info'); } catch (err) { showToast('Błąd sterowania odtwarzaniem', 'error'); } } // Skip Track async function skipTrack() { try { const res = await fetch('/api/skip', { method: 'POST' }); if (!res.ok) throw new Error('Skip failed'); showToast('Pominięto utwór', 'info'); } catch (err) { showToast('Błąd pomijania utworu', 'error'); } } // Stop Playback async function stopPlayback() { try { const res = await fetch('/api/stop', { method: 'POST' }); if (!res.ok) throw new Error('Stop failed'); showToast('Zatrzymano odtwarzanie i wyczyszczono kolejkę', 'info'); } catch (err) { showToast('Błąd zatrzymywania', 'error'); } } // Toggle Loop State (Cycle: Off -> Playlist -> Single -> Off) async function toggleLoop() { if (!currentStatus) return; try { const res = await fetch('/api/loop_cycle', { method: 'POST' }); if (!res.ok) throw new Error('Loop cycle failed'); const data = await res.json(); if (data.mode === 'playlist') showToast('Zapętlanie kolejki włączone', 'info'); else if (data.mode === 'single') showToast('Zapętlanie utworu włączone', 'info'); else showToast('Zapętlanie wyłączone', 'info'); } catch (err) { showToast('Błąd ustawiania pętli', 'error'); } } // Set Volume async function setVolume(vol) { try { const res = await fetch('/api/volume', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ volume: vol }) }); if (!res.ok) throw new Error('Volume failed'); } catch (err) { showToast('Nie udało się zmienić głośności', 'error'); } } // Settings Modal Functions function openSettingsModal() { if (settingsModal) settingsModal.style.display = 'flex'; } function closeSettingsModal() { if (settingsModal) settingsModal.style.display = 'none'; } function renderSettingsGuilds(guilds) { if (!settingsGuildsList) return; // Don't re-render if user is currently interacting with a select inside settings if (settingsModal && settingsModal.style.display !== 'none' && document.activeElement && document.activeElement.classList.contains('guild-log-select')) { return; } if (!guilds || guilds.length === 0) { settingsGuildsList.innerHTML = '

Brak serwerów Discord w pamięci bota.

'; lucide.createIcons(); return; } let html = ''; guilds.forEach(guild => { let optionsHtml = ''; if (guild.text_channels && guild.text_channels.length > 0) { guild.text_channels.forEach(tc => { const isSelected = guild.active_notification_channel_id === tc.channel_id ? 'selected' : ''; optionsHtml += ``; }); } else { optionsHtml = ''; } html += `
${guild.guild_name}
ID: ${guild.guild_id}
`; }); settingsGuildsList.innerHTML = html; lucide.createIcons(); } async function saveGuildLogChannel(guildId, channelId) { try { const res = await fetch('/api/settings/text_channel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ guild_id: guildId, channel_id: channelId || "" }) }); if (!res.ok) throw new Error('Settings change failed'); showToast('Zapisano kanał powiadomień dla serwera!', 'success'); } catch (err) { showToast('Nie udało się zapisać kanału powiadomień', 'error'); } } async function changeTrackPlays(trackId, delta) { if (!currentStatus || !currentStatus.queue) return; const track = currentStatus.queue.find(t => t.id === trackId); if (!track) return; const currentPlays = track.plays_left !== undefined ? track.plays_left : (currentStatus.default_plays || 5); const newPlays = Math.max(1, Math.min(999, currentPlays + delta)); if (newPlays === currentPlays) return; try { const res = await fetch('/api/queue/plays', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: trackId, plays: newPlays }) }); if (!res.ok) throw new Error('Failed to update plays'); } catch (err) { showToast('Błąd aktualizacji odtworzeń', 'error'); } } async function changeTrackPlaysDirect(trackId, val) { const newPlays = parseInt(val, 10); if (isNaN(newPlays) || newPlays < 1 || newPlays > 999) { showToast('Wartość musi być z przedziału 1-999', 'error'); if (currentStatus) renderQueue(currentStatus.queue, currentStatus.queue_index); return; } try { const res = await fetch('/api/queue/plays', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: trackId, plays: newPlays }) }); if (!res.ok) throw new Error('Failed to update plays'); } catch (err) { showToast('Błąd aktualizacji odtworzeń', 'error'); if (currentStatus) renderQueue(currentStatus.queue, currentStatus.queue_index); } } // Fetch Uploaded Files async function fetchUploads() { try { const res = await fetch('/api/uploads'); if (!res.ok) return; const data = await res.json(); renderUploads(data.files || []); } catch (err) { console.error('Błąd pobierania plików:', err); } } let allUploadedFiles = []; // Open Uploads Modal function openUploadsModal() { if (uploadsModal) { uploadsModal.style.display = 'flex'; renderModalUploads(allUploadedFiles); } } // Close Uploads Modal function closeUploadsModal() { if (uploadsModal) uploadsModal.style.display = 'none'; } // Render Modal Uploads (All files) function renderModalUploads(files) { if (!modalUploadsList) return; const items = modalUploadsList.querySelectorAll('.queue-item'); items.forEach(el => el.remove()); if (!files || files.length === 0) { if (emptyModalUploadsMsg) emptyModalUploadsMsg.style.display = 'flex'; return; } if (emptyModalUploadsMsg) emptyModalUploadsMsg.style.display = 'none'; files.forEach(file => { const li = document.createElement('li'); li.className = 'queue-item'; const safeFilename = file.filename.replace(/'/g, "\\'"); li.innerHTML = `
${file.filename}
${file.size_mb} MB
`; modalUploadsList.appendChild(li); }); lucide.createIcons(); } // Render Uploaded Files (Top 10 most recent on dashboard panel) function renderUploads(files) { allUploadedFiles = files || []; if (btnAllUploadsCount) btnAllUploadsCount.textContent = allUploadedFiles.length; if (!uploadsList) return; const items = uploadsList.querySelectorAll('.queue-item'); items.forEach(el => el.remove()); if (!files || files.length === 0) { emptyUploadsMsg.style.display = 'flex'; uploadsCountBadge.textContent = '0 plików'; if (modalUploadsList) renderModalUploads([]); return; } emptyUploadsMsg.style.display = 'none'; uploadsCountBadge.textContent = `${files.length} ${files.length === 1 ? 'plik' : files.length < 5 ? 'pliki' : 'plików'}`; // Take only top 10 most recently used/uploaded const top10 = files.slice(0, 10); top10.forEach(file => { const li = document.createElement('li'); li.className = 'queue-item'; const safeFilename = file.filename.replace(/'/g, "\\'"); li.innerHTML = `
${file.filename}
${file.size_mb} MB
`; uploadsList.appendChild(li); }); if (uploadsModal && uploadsModal.style.display !== 'none') { renderModalUploads(allUploadedFiles); } lucide.createIcons(); } // Handle File Upload async function handleFileUpload(e) { const files = e.target.files; if (!files || files.length === 0) return; uploadProgressContainer.style.display = 'block'; for (let i = 0; i < files.length; i++) { const file = files[i]; const formData = new FormData(); formData.append('file', file); uploadStatusText.textContent = `Przesyłanie: ${file.name}...`; uploadProgressFill.style.width = '50%'; try { const res = await fetch('/api/upload', { method: 'POST', body: formData }); if (!res.ok) { const errData = await res.json(); throw new Error(errData.detail || 'Błąd przesyłania'); } showToast(`Przesłano plik: ${file.name}`, 'success'); } catch (err) { showToast(`Błąd przesyłania ${file.name}: ${err.message}`, 'error'); } } uploadProgressFill.style.width = '100%'; setTimeout(() => { uploadProgressContainer.style.display = 'none'; uploadProgressFill.style.width = '0%'; fileInput.value = ''; fetchUploads(); }, 500); } // Play Uploaded File async function playUploadedFile(filename) { if (!currentStatus || !currentStatus.active_voice_channel) { showToast('Bot musi być połączony z kanałem głosowym!', 'error'); return; } try { const res = await fetch('/api/play_file', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: filename }) }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || 'Błąd odtwarzania pliku'); showToast(`Dodano plik do kolejki!`, 'success'); } catch (err) { showToast(`Błąd odtwarzania: ${err.message}`, 'error'); } } // Delete Uploaded File async function deleteUploadedFile(filename) { try { const res = await fetch(`/api/uploads/${encodeURIComponent(filename)}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Błąd usuwania'); showToast('Usunięto plik z serwera', 'info'); fetchUploads(); } catch (err) { showToast('Nie udało się usunąć pliku', 'error'); } } // Render History function renderHistory(history) { if (!historyList) return; const items = historyList.querySelectorAll('.queue-item'); items.forEach(el => el.remove()); if (!history || history.length === 0) { if (emptyHistoryMsg) emptyHistoryMsg.style.display = 'flex'; if (historyCountBadge) historyCountBadge.textContent = '0 utworów'; return; } if (emptyHistoryMsg) emptyHistoryMsg.style.display = 'none'; if (historyCountBadge) historyCountBadge.textContent = `${history.length} ${history.length === 1 ? 'utwór' : history.length < 5 ? 'utwory' : 'utworów'}`; history.forEach((item, index) => { const li = document.createElement('li'); li.className = 'queue-item'; let durationStr = '--:--'; if (item.duration) { const mins = Math.floor(item.duration / 60); const secs = Math.floor(item.duration % 60); durationStr = `${mins}:${secs < 10 ? '0' : ''}${secs}`; } const safeTitle = (item.title || 'Nieznany tytuł').replace(/'/g, "\\'"); const safeQuery = (item.is_local ? (item.filepath || item.title) : item.url).replace(/'/g, "\\'"); li.innerHTML = `
${item.title}
${durationStr} ${item.is_local ? '• plik lokalny' : ''}
`; historyList.appendChild(li); }); lucide.createIcons(); } // Replay from History async function replayFromHistory(queryOrFile, isLocal) { if (!currentStatus || !currentStatus.active_voice_channel) { showToast('Bot musi być połączony z kanałem głosowym!', 'error'); return; } if (isLocal) { const filename = queryOrFile.split('/').pop().split('\\').pop(); playUploadedFile(filename); } else { try { const res = await fetch('/api/play', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: queryOrFile }) }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || 'Błąd odtwarzania'); showToast('Dodano z historii do kolejki!', 'success'); } catch (err) { showToast(`Błąd: ${err.message}`, 'error'); } } }