1196 lines
45 KiB
JavaScript
1196 lines
45 KiB
JavaScript
// 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 = `<i data-lucide="${icon}"></i> <span>${message}</span>`;
|
|
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 = '<i data-lucide="log-out"></i> Odłącz';
|
|
btnConnect.className = 'btn btn-danger';
|
|
} else {
|
|
statusDot.className = 'status-indicator offline';
|
|
statusText.textContent = 'Rozłączony';
|
|
btnConnect.innerHTML = '<i data-lucide="log-in"></i> 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 = '<option value="" disabled>Wybierz kanał...</option>';
|
|
|
|
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 = `<i data-lucide="${targetIcon}"></i>`;
|
|
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 = '<i data-lucide="repeat-1"></i>';
|
|
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 = '<i data-lucide="repeat-1"></i> Zapętlenie utworu';
|
|
} else if (loopMode === 'playlist') {
|
|
btnLoop.innerHTML = '<i data-lucide="repeat"></i>';
|
|
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 = '<i data-lucide="repeat"></i> Zapętlenie kolejki';
|
|
} else {
|
|
btnLoop.innerHTML = '<i data-lucide="repeat"></i>';
|
|
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 = `<div class="queue-index">${idx + 1}</div>`;
|
|
|
|
if (idx === queueIndex) {
|
|
classes += ' playing-item';
|
|
indexHtml = `<div class="queue-index" title="Aktualnie odtwarzane"><i data-lucide="volume-2" style="color: var(--primary);"></i></div>`;
|
|
} 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 = `
|
|
<div class="drag-handle" title="Przeciągnij, aby zmienić kolejność">
|
|
<i data-lucide="grip-vertical"></i>
|
|
</div>
|
|
${indexHtml}
|
|
<div class="queue-track-info">
|
|
<div class="queue-track-title" title="${track.title}">${track.title}</div>
|
|
<div class="track-plays-control" style="display: flex; align-items: center; gap: 6px; font-size: 0.75rem; background: rgba(255,255,255,0.06); padding: 3px 10px; border-radius: 12px; margin-top: 6px; width: fit-content; border: 1px solid rgba(255,255,255,0.1);">
|
|
<i data-lucide="repeat" style="width: 13px; height: 13px; opacity: 0.9; color: var(--primary);"></i>
|
|
<span title="Pozostało odtworzeń z kolejki" style="color: var(--text-muted);">Pozostało:</span>
|
|
<button class="btn-tiny-action" onclick="changeTrackPlays('${track.id}', -1)" title="Zmniejsz o 1" style="background: rgba(255,255,255,0.12); border: none; color: inherit; cursor: pointer; padding: 2px 6px; border-radius: 6px; margin-left: 2px; transition: background 0.2s;"><i data-lucide="minus" style="width: 11px; height: 11px;"></i></button>
|
|
<input type="number" class="track-plays-input" min="1" max="999" value="${track.plays_left !== undefined ? track.plays_left : (currentStatus && currentStatus.default_plays ? currentStatus.default_plays : 5)}" onchange="changeTrackPlaysDirect('${track.id}', this.value)" style="width: 44px; text-align: center; background: rgba(0,0,0,0.35); border: 1px solid rgba(255,255,255,0.2); color: #fff; border-radius: 6px; padding: 2px 4px; font-size: 0.8rem; font-weight: bold;">
|
|
<button class="btn-tiny-action" onclick="changeTrackPlays('${track.id}', 1)" title="Zwiększ o 1" style="background: rgba(255,255,255,0.12); border: none; color: inherit; cursor: pointer; padding: 2px 6px; border-radius: 6px; transition: background 0.2s;"><i data-lucide="plus" style="width: 11px; height: 11px;"></i></button>
|
|
</div>
|
|
</div>
|
|
<div class="queue-item-actions">
|
|
<button class="btn-action-icon" onclick="jumpToTrack(${idx})" title="Odtwarzaj od tej pozycji">
|
|
<i data-lucide="play"></i>
|
|
</button>
|
|
<button class="btn-action-icon" onclick="moveItemUp(${idx})" title="Przesuń w górę">
|
|
<i data-lucide="arrow-up"></i>
|
|
</button>
|
|
<button class="btn-action-icon" onclick="moveItemDown(${idx})" title="Przesuń w dół">
|
|
<i data-lucide="arrow-down"></i>
|
|
</button>
|
|
<button class="btn-action-icon delete" onclick="removeTrack('${track.id}')" title="Usuń z kolejki">
|
|
<i data-lucide="trash-2"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = '<div class="empty-queue-message"><i data-lucide="server"></i><p>Brak serwerów Discord w pamięci bota.</p></div>';
|
|
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 += `<option value="${tc.channel_id}" ${isSelected}>#${tc.channel_name}</option>`;
|
|
});
|
|
} else {
|
|
optionsHtml = '<option value="" disabled>Brak kanałów tekstowych</option>';
|
|
}
|
|
|
|
html += `
|
|
<div class="guild-setting-row glass-card-subtle">
|
|
<div class="guild-info">
|
|
<div class="guild-icon"><i data-lucide="shield"></i></div>
|
|
<div>
|
|
<div class="guild-name">${guild.guild_name}</div>
|
|
<div class="guild-id">ID: ${guild.guild_id}</div>
|
|
</div>
|
|
</div>
|
|
<div class="guild-control">
|
|
<select class="premium-select guild-log-select" data-guild-id="${guild.guild_id}" onchange="saveGuildLogChannel('${guild.guild_id}', this.value)">
|
|
<option value="">-- Brak / Domyślny --</option>
|
|
${optionsHtml}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
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 = `
|
|
<div class="queue-index"><i data-lucide="file-audio"></i></div>
|
|
<div class="queue-track-info" style="flex: 1; min-width: 0;">
|
|
<div class="queue-track-title" title="${file.filename}" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${file.filename}</div>
|
|
<div class="queue-track-artist">${file.size_mb} MB</div>
|
|
</div>
|
|
<div class="queue-item-actions" style="display: flex; gap: 6px; align-items: center;">
|
|
<button class="btn btn-primary" style="padding: 4px 10px; font-size: 0.8rem; display: flex; align-items: center; gap: 4px;" onclick="playUploadedFile('${safeFilename}')" title="Dodaj do kolejki">
|
|
<i data-lucide="plus"></i> Dodaj
|
|
</button>
|
|
<button class="btn-action-icon delete" onclick="deleteUploadedFile('${safeFilename}')" title="Usuń z serwera">
|
|
<i data-lucide="trash-2"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
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 = `
|
|
<div class="queue-index"><i data-lucide="file-audio"></i></div>
|
|
<div class="queue-track-info" style="flex: 1; min-width: 0;">
|
|
<div class="queue-track-title" title="${file.filename}" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${file.filename}</div>
|
|
<div class="queue-track-artist">${file.size_mb} MB</div>
|
|
</div>
|
|
<div class="queue-item-actions" style="display: flex; gap: 6px; align-items: center;">
|
|
<button class="btn btn-primary" style="padding: 4px 10px; font-size: 0.8rem; display: flex; align-items: center; gap: 4px;" onclick="playUploadedFile('${safeFilename}')" title="Dodaj do kolejki">
|
|
<i data-lucide="plus"></i> Dodaj
|
|
</button>
|
|
<button class="btn-action-icon delete" onclick="deleteUploadedFile('${safeFilename}')" title="Usuń z serwera">
|
|
<i data-lucide="trash-2"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
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 = `
|
|
<div class="queue-index"><i data-lucide="rotate-ccw"></i></div>
|
|
<div class="queue-track-info" style="flex: 1; min-width: 0;">
|
|
<div class="queue-track-title" title="${item.title}" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${item.title}</div>
|
|
<div class="queue-track-artist">${durationStr} ${item.is_local ? '• plik lokalny' : ''}</div>
|
|
</div>
|
|
<div class="queue-item-actions">
|
|
<button class="btn btn-secondary" style="padding: 4px 10px; font-size: 0.8rem; display: flex; align-items: center; gap: 4px;" onclick="replayFromHistory('${safeQuery}', ${item.is_local})" title="Dodaj ponownie do kolejki">
|
|
<i data-lucide="plus"></i> Dodaj
|
|
</button>
|
|
</div>
|
|
`;
|
|
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');
|
|
}
|
|
}
|
|
}
|
|
|