- Created websocket_client.js: Native WebSocket wrapper with Socket.IO-compatible interface - Automatic reconnection with exponential backoff - Room-based subscriptions for targeted updates - Message queueing during disconnection - Updated HTML templates (index.html, queue.html): - Replaced Socket.IO CDN with native websocket_client.js - No external dependencies needed - Updated JavaScript files (app.js, queue.js): - Added room subscriptions on WebSocket connect (scan_progress, download_progress, downloads) - Added dual event handlers for backward compatibility - Support both old (scan_completed) and new (scan_complete) message types - Support both old (download_error) and new (download_failed) message types - Support both old (queue_updated) and new (queue_status) message types - Registered anime router in fastapi_app.py: - Added anime_router import and registration - All API routers now properly included - Documentation: - Created FRONTEND_INTEGRATION.md with comprehensive integration guide - Updated infrastructure.md with frontend integration section - Updated instructions.md to mark task as completed - Testing: - Verified anime endpoint tests pass (pytest) - API endpoint mapping documented - WebSocket message format changes documented Benefits: - Native WebSocket API (faster, smaller footprint) - No external CDN dependencies - Full backward compatibility with existing code - Proper integration with backend services - Real-time updates via room-based messaging
749 lines
28 KiB
JavaScript
749 lines
28 KiB
JavaScript
/**
|
|
* Download Queue Management - JavaScript Application
|
|
*/
|
|
|
|
class QueueManager {
|
|
constructor() {
|
|
this.socket = null;
|
|
this.refreshInterval = null;
|
|
this.isReordering = false;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.initSocket();
|
|
this.bindEvents();
|
|
this.initTheme();
|
|
this.startRefreshTimer();
|
|
this.loadQueueData();
|
|
}
|
|
|
|
initSocket() {
|
|
this.socket = io();
|
|
|
|
// Handle initial connection message from server
|
|
this.socket.on('connected', (data) => {
|
|
console.log('WebSocket connection confirmed', data);
|
|
});
|
|
|
|
this.socket.on('connect', () => {
|
|
console.log('Connected to server');
|
|
|
|
// Subscribe to rooms for targeted updates
|
|
this.socket.join('downloads');
|
|
this.socket.join('download_progress');
|
|
|
|
this.showToast('Connected to server', 'success');
|
|
});
|
|
|
|
this.socket.on('disconnect', () => {
|
|
console.log('Disconnected from server');
|
|
this.showToast('Disconnected from server', 'warning');
|
|
});
|
|
|
|
// Queue update events - handle both old and new message types
|
|
this.socket.on('queue_updated', (data) => {
|
|
this.updateQueueDisplay(data);
|
|
});
|
|
this.socket.on('queue_status', (data) => {
|
|
// New backend sends queue_status messages
|
|
if (data.queue_status) {
|
|
this.updateQueueDisplay(data.queue_status);
|
|
} else {
|
|
this.updateQueueDisplay(data);
|
|
}
|
|
});
|
|
|
|
this.socket.on('download_progress_update', (data) => {
|
|
this.updateDownloadProgress(data);
|
|
});
|
|
|
|
// Download queue events
|
|
this.socket.on('download_started', () => {
|
|
this.showToast('Download queue started', 'success');
|
|
this.loadQueueData(); // Refresh data
|
|
});
|
|
this.socket.on('queue_started', () => {
|
|
this.showToast('Download queue started', 'success');
|
|
this.loadQueueData(); // Refresh data
|
|
});
|
|
|
|
this.socket.on('download_progress', (data) => {
|
|
this.updateDownloadProgress(data);
|
|
});
|
|
|
|
// Handle both old and new download completion events
|
|
const handleDownloadComplete = (data) => {
|
|
const serieName = data.serie_name || data.serie || 'Unknown';
|
|
const episode = data.episode || '';
|
|
this.showToast(`Completed: ${serieName}${episode ? ' - Episode ' + episode : ''}`, 'success');
|
|
this.loadQueueData(); // Refresh data
|
|
};
|
|
this.socket.on('download_completed', handleDownloadComplete);
|
|
this.socket.on('download_complete', handleDownloadComplete);
|
|
|
|
// Handle both old and new download error events
|
|
const handleDownloadError = (data) => {
|
|
const message = data.error || data.message || 'Unknown error';
|
|
this.showToast(`Download failed: ${message}`, 'error');
|
|
this.loadQueueData(); // Refresh data
|
|
};
|
|
this.socket.on('download_error', handleDownloadError);
|
|
this.socket.on('download_failed', handleDownloadError);
|
|
|
|
this.socket.on('download_queue_completed', () => {
|
|
this.showToast('All downloads completed!', 'success');
|
|
this.loadQueueData(); // Refresh data
|
|
});
|
|
|
|
this.socket.on('download_stop_requested', () => {
|
|
this.showToast('Stopping downloads...', 'info');
|
|
});
|
|
|
|
// Handle both old and new queue stopped events
|
|
const handleQueueStopped = () => {
|
|
this.showToast('Download queue stopped', 'success');
|
|
this.loadQueueData(); // Refresh data
|
|
};
|
|
this.socket.on('download_stopped', handleQueueStopped);
|
|
this.socket.on('queue_stopped', handleQueueStopped);
|
|
|
|
// Handle queue paused/resumed
|
|
this.socket.on('queue_paused', () => {
|
|
this.showToast('Queue paused', 'info');
|
|
this.loadQueueData();
|
|
});
|
|
|
|
this.socket.on('queue_resumed', () => {
|
|
this.showToast('Queue resumed', 'success');
|
|
this.loadQueueData();
|
|
});
|
|
}
|
|
|
|
bindEvents() {
|
|
// Theme toggle
|
|
document.getElementById('theme-toggle').addEventListener('click', () => {
|
|
this.toggleTheme();
|
|
});
|
|
|
|
// Queue management actions
|
|
document.getElementById('clear-queue-btn').addEventListener('click', () => {
|
|
this.clearQueue('pending');
|
|
});
|
|
|
|
document.getElementById('clear-completed-btn').addEventListener('click', () => {
|
|
this.clearQueue('completed');
|
|
});
|
|
|
|
document.getElementById('clear-failed-btn').addEventListener('click', () => {
|
|
this.clearQueue('failed');
|
|
});
|
|
|
|
document.getElementById('retry-all-btn').addEventListener('click', () => {
|
|
this.retryAllFailed();
|
|
});
|
|
|
|
document.getElementById('reorder-queue-btn').addEventListener('click', () => {
|
|
this.toggleReorderMode();
|
|
});
|
|
|
|
// Download controls
|
|
document.getElementById('start-queue-btn').addEventListener('click', () => {
|
|
this.startDownloadQueue();
|
|
});
|
|
|
|
document.getElementById('stop-queue-btn').addEventListener('click', () => {
|
|
this.stopDownloadQueue();
|
|
});
|
|
|
|
document.getElementById('pause-all-btn').addEventListener('click', () => {
|
|
this.pauseAllDownloads();
|
|
});
|
|
|
|
document.getElementById('resume-all-btn').addEventListener('click', () => {
|
|
this.resumeAllDownloads();
|
|
});
|
|
|
|
// Modal events
|
|
document.getElementById('close-confirm').addEventListener('click', () => {
|
|
this.hideConfirmModal();
|
|
});
|
|
|
|
document.getElementById('confirm-cancel').addEventListener('click', () => {
|
|
this.hideConfirmModal();
|
|
});
|
|
|
|
document.querySelector('#confirm-modal .modal-overlay').addEventListener('click', () => {
|
|
this.hideConfirmModal();
|
|
});
|
|
|
|
// Logout functionality
|
|
document.getElementById('logout-btn').addEventListener('click', () => {
|
|
this.logout();
|
|
});
|
|
}
|
|
|
|
initTheme() {
|
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
|
this.setTheme(savedTheme);
|
|
}
|
|
|
|
setTheme(theme) {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
localStorage.setItem('theme', theme);
|
|
|
|
const themeIcon = document.querySelector('#theme-toggle i');
|
|
themeIcon.className = theme === 'light' ? 'fas fa-moon' : 'fas fa-sun';
|
|
}
|
|
|
|
toggleTheme() {
|
|
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
|
|
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
|
this.setTheme(newTheme);
|
|
}
|
|
|
|
startRefreshTimer() {
|
|
// Refresh every 2 seconds
|
|
this.refreshInterval = setInterval(() => {
|
|
this.loadQueueData();
|
|
}, 2000);
|
|
}
|
|
|
|
async loadQueueData() {
|
|
try {
|
|
const response = await this.makeAuthenticatedRequest('/api/queue/status');
|
|
if (!response) return;
|
|
|
|
const data = await response.json();
|
|
this.updateQueueDisplay(data);
|
|
|
|
} catch (error) {
|
|
console.error('Error loading queue data:', error);
|
|
}
|
|
}
|
|
|
|
updateQueueDisplay(data) {
|
|
// Update statistics
|
|
this.updateStatistics(data.statistics, data);
|
|
|
|
// Update active downloads
|
|
this.renderActiveDownloads(data.active_downloads || []);
|
|
|
|
// Update pending queue
|
|
this.renderPendingQueue(data.pending_queue || []);
|
|
|
|
// Update completed downloads
|
|
this.renderCompletedDownloads(data.completed_downloads || []);
|
|
|
|
// Update failed downloads
|
|
this.renderFailedDownloads(data.failed_downloads || []);
|
|
|
|
// Update button states
|
|
this.updateButtonStates(data);
|
|
}
|
|
|
|
updateStatistics(stats, data) {
|
|
document.getElementById('total-items').textContent = stats.total_items || 0;
|
|
document.getElementById('pending-items').textContent = (data.pending_queue || []).length;
|
|
document.getElementById('completed-items').textContent = stats.completed_items || 0;
|
|
document.getElementById('failed-items').textContent = stats.failed_items || 0;
|
|
|
|
document.getElementById('current-speed').textContent = stats.current_speed || '0 MB/s';
|
|
document.getElementById('average-speed').textContent = stats.average_speed || '0 MB/s';
|
|
|
|
// Format ETA
|
|
const etaElement = document.getElementById('eta-time');
|
|
if (stats.eta) {
|
|
const eta = new Date(stats.eta);
|
|
const now = new Date();
|
|
const diffMs = eta - now;
|
|
|
|
if (diffMs > 0) {
|
|
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
etaElement.textContent = `${hours}h ${minutes}m`;
|
|
} else {
|
|
etaElement.textContent = 'Calculating...';
|
|
}
|
|
} else {
|
|
etaElement.textContent = '--:--';
|
|
}
|
|
}
|
|
|
|
renderActiveDownloads(downloads) {
|
|
const container = document.getElementById('active-downloads');
|
|
|
|
if (downloads.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-pause-circle"></i>
|
|
<p>No active downloads</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = downloads.map(download => this.createActiveDownloadCard(download)).join('');
|
|
}
|
|
|
|
createActiveDownloadCard(download) {
|
|
const progress = download.progress || {};
|
|
const progressPercent = progress.percent || 0;
|
|
const speed = progress.speed_mbps ? `${progress.speed_mbps.toFixed(1)} MB/s` : '0 MB/s';
|
|
const downloaded = progress.downloaded_mb ? `${progress.downloaded_mb.toFixed(1)} MB` : '0 MB';
|
|
const total = progress.total_mb ? `${progress.total_mb.toFixed(1)} MB` : 'Unknown';
|
|
|
|
return `
|
|
<div class="download-card active">
|
|
<div class="download-header">
|
|
<div class="download-info">
|
|
<h4>${this.escapeHtml(download.serie_name)}</h4>
|
|
<p>${this.escapeHtml(download.episode.season)}x${String(download.episode.episode).padStart(2, '0')} - ${this.escapeHtml(download.episode.title || 'Episode ' + download.episode.episode)}</p>
|
|
</div>
|
|
<div class="download-actions">
|
|
<button class="btn btn-small btn-secondary" onclick="queueManager.pauseDownload('${download.id}')">
|
|
<i class="fas fa-pause"></i>
|
|
</button>
|
|
<button class="btn btn-small btn-error" onclick="queueManager.cancelDownload('${download.id}')">
|
|
<i class="fas fa-stop"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="download-progress">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" style="width: ${progressPercent}%"></div>
|
|
</div>
|
|
<div class="progress-info">
|
|
<span>${progressPercent.toFixed(1)}% (${downloaded} / ${total})</span>
|
|
<span class="download-speed">${speed}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderPendingQueue(queue) {
|
|
const container = document.getElementById('pending-queue');
|
|
|
|
if (queue.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-list"></i>
|
|
<p>No items in queue</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = queue.map((item, index) => this.createPendingQueueCard(item, index)).join('');
|
|
}
|
|
|
|
createPendingQueueCard(download, index) {
|
|
const addedAt = new Date(download.added_at).toLocaleString();
|
|
const priorityClass = download.priority === 'high' ? 'high-priority' : '';
|
|
|
|
return `
|
|
<div class="download-card pending ${priorityClass}" data-id="${download.id}">
|
|
<div class="queue-position">${index + 1}</div>
|
|
<div class="download-header">
|
|
<div class="download-info">
|
|
<h4>${this.escapeHtml(download.serie_name)}</h4>
|
|
<p>${this.escapeHtml(download.episode.season)}x${String(download.episode.episode).padStart(2, '0')} - ${this.escapeHtml(download.episode.title || 'Episode ' + download.episode.episode)}</p>
|
|
<small>Added: ${addedAt}</small>
|
|
</div>
|
|
<div class="download-actions">
|
|
${download.priority === 'high' ? '<i class="fas fa-arrow-up priority-indicator" title="High Priority"></i>' : ''}
|
|
<button class="btn btn-small btn-secondary" onclick="queueManager.removeFromQueue('${download.id}')">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderCompletedDownloads(downloads) {
|
|
const container = document.getElementById('completed-downloads');
|
|
|
|
if (downloads.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-check-circle"></i>
|
|
<p>No completed downloads</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = downloads.map(download => this.createCompletedDownloadCard(download)).join('');
|
|
}
|
|
|
|
createCompletedDownloadCard(download) {
|
|
const completedAt = new Date(download.completed_at).toLocaleString();
|
|
const duration = this.calculateDuration(download.started_at, download.completed_at);
|
|
|
|
return `
|
|
<div class="download-card completed">
|
|
<div class="download-header">
|
|
<div class="download-info">
|
|
<h4>${this.escapeHtml(download.serie_name)}</h4>
|
|
<p>${this.escapeHtml(download.episode.season)}x${String(download.episode.episode).padStart(2, '0')} - ${this.escapeHtml(download.episode.title || 'Episode ' + download.episode.episode)}</p>
|
|
<small>Completed: ${completedAt} (${duration})</small>
|
|
</div>
|
|
<div class="download-status">
|
|
<i class="fas fa-check-circle text-success"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderFailedDownloads(downloads) {
|
|
const container = document.getElementById('failed-downloads');
|
|
|
|
if (downloads.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-check-circle text-success"></i>
|
|
<p>No failed downloads</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = downloads.map(download => this.createFailedDownloadCard(download)).join('');
|
|
}
|
|
|
|
createFailedDownloadCard(download) {
|
|
const failedAt = new Date(download.completed_at).toLocaleString();
|
|
const retryCount = download.retry_count || 0;
|
|
|
|
return `
|
|
<div class="download-card failed">
|
|
<div class="download-header">
|
|
<div class="download-info">
|
|
<h4>${this.escapeHtml(download.serie_name)}</h4>
|
|
<p>${this.escapeHtml(download.episode.season)}x${String(download.episode.episode).padStart(2, '0')} - ${this.escapeHtml(download.episode.title || 'Episode ' + download.episode.episode)}</p>
|
|
<small>Failed: ${failedAt} ${retryCount > 0 ? `(Retry ${retryCount})` : ''}</small>
|
|
${download.error ? `<small class="error-message">${this.escapeHtml(download.error)}</small>` : ''}
|
|
</div>
|
|
<div class="download-actions">
|
|
<button class="btn btn-small btn-warning" onclick="queueManager.retryDownload('${download.id}')">
|
|
<i class="fas fa-redo"></i>
|
|
</button>
|
|
<button class="btn btn-small btn-secondary" onclick="queueManager.removeFailedDownload('${download.id}')">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
updateButtonStates(data) {
|
|
const hasActive = (data.active_downloads || []).length > 0;
|
|
const hasPending = (data.pending_queue || []).length > 0;
|
|
const hasFailed = (data.failed_downloads || []).length > 0;
|
|
|
|
// Enable start button only if there are pending items and no active downloads
|
|
document.getElementById('start-queue-btn').disabled = !hasPending || hasActive;
|
|
|
|
// Show/hide start/stop buttons based on whether downloads are active
|
|
if (hasActive) {
|
|
document.getElementById('start-queue-btn').style.display = 'none';
|
|
document.getElementById('stop-queue-btn').style.display = 'inline-flex';
|
|
document.getElementById('stop-queue-btn').disabled = false;
|
|
} else {
|
|
document.getElementById('stop-queue-btn').style.display = 'none';
|
|
document.getElementById('start-queue-btn').style.display = 'inline-flex';
|
|
}
|
|
|
|
document.getElementById('pause-all-btn').disabled = !hasActive;
|
|
document.getElementById('clear-queue-btn').disabled = !hasPending;
|
|
document.getElementById('reorder-queue-btn').disabled = !hasPending || (data.pending_queue || []).length < 2;
|
|
document.getElementById('retry-all-btn').disabled = !hasFailed;
|
|
}
|
|
|
|
async clearQueue(type) {
|
|
const titles = {
|
|
pending: 'Clear Queue',
|
|
completed: 'Clear Completed Downloads',
|
|
failed: 'Clear Failed Downloads'
|
|
};
|
|
|
|
const messages = {
|
|
pending: 'Are you sure you want to clear all pending downloads from the queue?',
|
|
completed: 'Are you sure you want to clear all completed downloads?',
|
|
failed: 'Are you sure you want to clear all failed downloads?'
|
|
};
|
|
|
|
const confirmed = await this.showConfirmModal(titles[type], messages[type]);
|
|
if (!confirmed) return;
|
|
|
|
try {
|
|
const response = await this.makeAuthenticatedRequest('/api/queue/clear', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ type })
|
|
});
|
|
|
|
if (!response) return;
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
this.showToast(data.message, 'success');
|
|
this.loadQueueData();
|
|
} else {
|
|
this.showToast(data.message, 'error');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error clearing queue:', error);
|
|
this.showToast('Failed to clear queue', 'error');
|
|
}
|
|
}
|
|
|
|
async retryDownload(downloadId) {
|
|
try {
|
|
const response = await this.makeAuthenticatedRequest('/api/queue/retry', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id: downloadId })
|
|
});
|
|
|
|
if (!response) return;
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
this.showToast('Download added back to queue', 'success');
|
|
this.loadQueueData();
|
|
} else {
|
|
this.showToast(data.message, 'error');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error retrying download:', error);
|
|
this.showToast('Failed to retry download', 'error');
|
|
}
|
|
}
|
|
|
|
async retryAllFailed() {
|
|
const confirmed = await this.showConfirmModal('Retry All Failed Downloads', 'Are you sure you want to retry all failed downloads?');
|
|
if (!confirmed) return;
|
|
|
|
// Get all failed downloads and retry them individually
|
|
const failedCards = document.querySelectorAll('#failed-downloads .download-card.failed');
|
|
|
|
for (const card of failedCards) {
|
|
const downloadId = card.dataset.id;
|
|
if (downloadId) {
|
|
await this.retryDownload(downloadId);
|
|
}
|
|
}
|
|
}
|
|
|
|
async removeFromQueue(downloadId) {
|
|
try {
|
|
const response = await this.makeAuthenticatedRequest('/api/queue/remove', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id: downloadId })
|
|
});
|
|
|
|
if (!response) return;
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
this.showToast('Download removed from queue', 'success');
|
|
this.loadQueueData();
|
|
} else {
|
|
this.showToast(data.message, 'error');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error removing from queue:', error);
|
|
this.showToast('Failed to remove from queue', 'error');
|
|
}
|
|
}
|
|
|
|
calculateDuration(startTime, endTime) {
|
|
const start = new Date(startTime);
|
|
const end = new Date(endTime);
|
|
const diffMs = end - start;
|
|
|
|
const minutes = Math.floor(diffMs / (1000 * 60));
|
|
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
|
|
return `${minutes}m ${seconds}s`;
|
|
}
|
|
|
|
async startDownloadQueue() {
|
|
try {
|
|
const response = await this.makeAuthenticatedRequest('/api/queue/start', {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (!response) return;
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
this.showToast('Download queue started', 'success');
|
|
|
|
// Update UI
|
|
document.getElementById('start-queue-btn').style.display = 'none';
|
|
document.getElementById('stop-queue-btn').style.display = 'inline-flex';
|
|
document.getElementById('stop-queue-btn').disabled = false;
|
|
} else {
|
|
this.showToast(`Failed to start queue: ${data.message}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error starting download queue:', error);
|
|
this.showToast('Failed to start download queue', 'error');
|
|
}
|
|
}
|
|
|
|
async stopDownloadQueue() {
|
|
try {
|
|
const response = await this.makeAuthenticatedRequest('/api/queue/stop', {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (!response) return;
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
this.showToast('Download queue stopped', 'success');
|
|
|
|
// Update UI
|
|
document.getElementById('stop-queue-btn').style.display = 'none';
|
|
document.getElementById('start-queue-btn').style.display = 'inline-flex';
|
|
document.getElementById('start-queue-btn').disabled = false;
|
|
} else {
|
|
this.showToast(`Failed to stop queue: ${data.message}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error stopping download queue:', error);
|
|
this.showToast('Failed to stop download queue', 'error');
|
|
}
|
|
}
|
|
|
|
pauseAllDownloads() {
|
|
// TODO: Implement pause functionality
|
|
this.showToast('Pause functionality not yet implemented', 'info');
|
|
}
|
|
|
|
resumeAllDownloads() {
|
|
// TODO: Implement resume functionality
|
|
this.showToast('Resume functionality not yet implemented', 'info');
|
|
}
|
|
|
|
toggleReorderMode() {
|
|
// TODO: Implement reorder functionality
|
|
this.showToast('Reorder functionality not yet implemented', 'info');
|
|
}
|
|
|
|
async makeAuthenticatedRequest(url, options = {}) {
|
|
// Ensure credentials are included for session-based authentication
|
|
const requestOptions = {
|
|
credentials: 'same-origin',
|
|
...options
|
|
};
|
|
|
|
const response = await fetch(url, requestOptions);
|
|
|
|
if (response.status === 401) {
|
|
window.location.href = '/login';
|
|
return null;
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
showConfirmModal(title, message) {
|
|
return new Promise((resolve) => {
|
|
document.getElementById('confirm-title').textContent = title;
|
|
document.getElementById('confirm-message').textContent = message;
|
|
document.getElementById('confirm-modal').classList.remove('hidden');
|
|
|
|
const handleConfirm = () => {
|
|
cleanup();
|
|
resolve(true);
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
cleanup();
|
|
resolve(false);
|
|
};
|
|
|
|
const cleanup = () => {
|
|
document.getElementById('confirm-ok').removeEventListener('click', handleConfirm);
|
|
document.getElementById('confirm-cancel').removeEventListener('click', handleCancel);
|
|
this.hideConfirmModal();
|
|
};
|
|
|
|
document.getElementById('confirm-ok').addEventListener('click', handleConfirm);
|
|
document.getElementById('confirm-cancel').addEventListener('click', handleCancel);
|
|
});
|
|
}
|
|
|
|
hideConfirmModal() {
|
|
document.getElementById('confirm-modal').classList.add('hidden');
|
|
}
|
|
|
|
showToast(message, type = 'info') {
|
|
const container = document.getElementById('toast-container');
|
|
const toast = document.createElement('div');
|
|
|
|
toast.className = `toast ${type}`;
|
|
toast.innerHTML = `
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
<span>${this.escapeHtml(message)}</span>
|
|
<button onclick="this.parentElement.parentElement.remove()" style="background: none; border: none; color: var(--color-text-secondary); cursor: pointer; padding: 0; margin-left: 1rem;">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
container.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
if (toast.parentElement) {
|
|
toast.remove();
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async logout() {
|
|
try {
|
|
const response = await fetch('/api/auth/logout', { method: 'POST' });
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
this.showToast('Logged out successfully', 'success');
|
|
setTimeout(() => {
|
|
window.location.href = '/login';
|
|
}, 1000);
|
|
} else {
|
|
this.showToast('Logout failed', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Logout error:', error);
|
|
this.showToast('Logout failed', 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize the application when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.queueManager = new QueueManager();
|
|
});
|
|
|
|
// Global reference for inline event handlers
|
|
window.queueManager = null; |