refactor: split CSS and JS into modular files (SRP)

This commit is contained in:
2025-12-26 13:55:02 +01:00
parent 94cf36bff3
commit 2e5731b5d6
47 changed files with 8882 additions and 2298 deletions

View File

@@ -0,0 +1,189 @@
/**
* AniWorld - Progress Handler Module
*
* Handles real-time download progress updates.
*
* Dependencies: constants.js, ui-utils.js
*/
var AniWorld = window.AniWorld || {};
AniWorld.ProgressHandler = (function() {
'use strict';
// Store progress updates waiting for cards
let pendingProgressUpdates = new Map();
/**
* Update download progress in real-time
* @param {Object} data - Progress data from WebSocket
*/
function updateDownloadProgress(data) {
console.log('updateDownloadProgress called with:', JSON.stringify(data, null, 2));
// Extract download ID - prioritize metadata.item_id (actual item ID)
let downloadId = null;
// First try metadata.item_id (this is the actual download item ID)
if (data.metadata && data.metadata.item_id) {
downloadId = data.metadata.item_id;
}
// Fallback to other ID fields
if (!downloadId) {
downloadId = data.item_id || data.download_id;
}
// If ID starts with "download_", extract the actual ID
if (!downloadId && data.id) {
if (data.id.startsWith('download_')) {
downloadId = data.id.substring(9);
} else {
downloadId = data.id;
}
}
// Check if data is wrapped in another 'data' property
if (!downloadId && data.data) {
if (data.data.metadata && data.data.metadata.item_id) {
downloadId = data.data.metadata.item_id;
} else if (data.data.item_id) {
downloadId = data.data.item_id;
} else if (data.data.id && data.data.id.startsWith('download_')) {
downloadId = data.data.id.substring(9);
} else {
downloadId = data.data.id || data.data.download_id;
}
data = data.data;
}
if (!downloadId) {
console.warn('No download ID in progress data');
console.warn('Data structure:', data);
console.warn('Available keys:', Object.keys(data));
return;
}
console.log('Looking for download card with ID: ' + downloadId);
// Find the download card in active downloads
const card = document.querySelector('[data-download-id="' + downloadId + '"]');
if (!card) {
console.warn('Download card not found for ID: ' + downloadId);
// Debug: Log all existing download cards
const allCards = document.querySelectorAll('[data-download-id]');
console.log('Found ' + allCards.length + ' download cards:');
allCards.forEach(function(c) {
console.log(' - ' + c.getAttribute('data-download-id'));
});
// Store this progress update to retry after queue loads
console.log('Storing progress update for ' + downloadId + ' to retry after reload');
pendingProgressUpdates.set(downloadId, data);
return false;
}
console.log('Found download card for ID: ' + downloadId + ', updating progress');
// Extract progress information
const progress = data.progress || data;
const percent = progress.percent || 0;
const metadata = progress.metadata || data.metadata || {};
// Check data format
let speed;
if (progress.downloaded_mb !== undefined && progress.total_mb !== undefined) {
// yt-dlp detailed format
speed = progress.speed_mbps ? progress.speed_mbps.toFixed(1) : '0.0';
} else if (progress.current !== undefined && progress.total !== undefined) {
// ProgressService basic format
speed = metadata.speed_mbps ? metadata.speed_mbps.toFixed(1) : '0.0';
} else {
// Fallback
speed = metadata.speed_mbps ? metadata.speed_mbps.toFixed(1) : '0.0';
}
// Update progress bar
const progressFill = card.querySelector('.progress-fill');
if (progressFill) {
progressFill.style.width = percent + '%';
}
// Update progress text
const progressInfo = card.querySelector('.progress-info');
if (progressInfo) {
const percentSpan = progressInfo.querySelector('span:first-child');
const speedSpan = progressInfo.querySelector('.download-speed');
if (percentSpan) {
percentSpan.textContent = percent > 0 ? percent.toFixed(1) + '%' : 'Starting...';
}
if (speedSpan) {
speedSpan.textContent = speed + ' MB/s';
}
}
console.log('Updated progress for ' + downloadId + ': ' + percent.toFixed(1) + '%');
return true;
}
/**
* Process pending progress updates
*/
function processPendingProgressUpdates() {
if (pendingProgressUpdates.size === 0) {
return;
}
console.log('Processing ' + pendingProgressUpdates.size + ' pending progress updates...');
// Process each pending update
const processed = [];
pendingProgressUpdates.forEach(function(data, downloadId) {
// Check if card now exists
const card = document.querySelector('[data-download-id="' + downloadId + '"]');
if (card) {
console.log('Retrying progress update for ' + downloadId);
updateDownloadProgress(data);
processed.push(downloadId);
} else {
console.log('Card still not found for ' + downloadId + ', will retry on next reload');
}
});
// Remove processed updates
processed.forEach(function(id) {
pendingProgressUpdates.delete(id);
});
if (processed.length > 0) {
console.log('Successfully processed ' + processed.length + ' pending updates');
}
}
/**
* Clear all pending progress updates
*/
function clearPendingUpdates() {
pendingProgressUpdates.clear();
}
/**
* Clear pending update for specific download
* @param {string} downloadId - Download ID
*/
function clearPendingUpdate(downloadId) {
pendingProgressUpdates.delete(downloadId);
}
// Public API
return {
updateDownloadProgress: updateDownloadProgress,
processPendingProgressUpdates: processPendingProgressUpdates,
clearPendingUpdates: clearPendingUpdates,
clearPendingUpdate: clearPendingUpdate
};
})();

View File

@@ -0,0 +1,159 @@
/**
* AniWorld - Queue API Module
*
* Handles API requests for the download queue.
*
* Dependencies: constants.js, api-client.js
*/
var AniWorld = window.AniWorld || {};
AniWorld.QueueAPI = (function() {
'use strict';
const API = AniWorld.Constants.API;
/**
* Load queue data from server
* @returns {Promise<Object>} Queue data
*/
async function loadQueueData() {
try {
const response = await AniWorld.ApiClient.get(API.QUEUE_STATUS);
if (!response) {
return null;
}
const data = await response.json();
// API returns nested structure with 'status' and 'statistics'
// Transform it to the expected flat structure
return {
...data.status,
statistics: data.statistics
};
} catch (error) {
console.error('Error loading queue data:', error);
return null;
}
}
/**
* Start queue processing
* @returns {Promise<Object>} Response data
*/
async function startQueue() {
try {
const response = await AniWorld.ApiClient.post(API.QUEUE_START, {});
if (!response) return null;
return await response.json();
} catch (error) {
console.error('Error starting queue:', error);
throw error;
}
}
/**
* Stop queue processing
* @returns {Promise<Object>} Response data
*/
async function stopQueue() {
try {
const response = await AniWorld.ApiClient.post(API.QUEUE_STOP, {});
if (!response) return null;
return await response.json();
} catch (error) {
console.error('Error stopping queue:', error);
throw error;
}
}
/**
* Remove item from queue
* @param {string} downloadId - Download item ID
* @returns {Promise<boolean>} Success status
*/
async function removeFromQueue(downloadId) {
try {
const response = await AniWorld.ApiClient.delete(API.QUEUE_REMOVE + '/' + downloadId);
if (!response) return false;
return response.status === 204;
} catch (error) {
console.error('Error removing from queue:', error);
throw error;
}
}
/**
* Retry failed downloads
* @param {Array<string>} itemIds - Array of download item IDs
* @returns {Promise<Object>} Response data
*/
async function retryDownloads(itemIds) {
try {
const response = await AniWorld.ApiClient.post(API.QUEUE_RETRY, { item_ids: itemIds });
if (!response) return null;
return await response.json();
} catch (error) {
console.error('Error retrying downloads:', error);
throw error;
}
}
/**
* Clear completed downloads
* @returns {Promise<Object>} Response data
*/
async function clearCompleted() {
try {
const response = await AniWorld.ApiClient.delete(API.QUEUE_COMPLETED);
if (!response) return null;
return await response.json();
} catch (error) {
console.error('Error clearing completed:', error);
throw error;
}
}
/**
* Clear failed downloads
* @returns {Promise<Object>} Response data
*/
async function clearFailed() {
try {
const response = await AniWorld.ApiClient.delete(API.QUEUE_FAILED);
if (!response) return null;
return await response.json();
} catch (error) {
console.error('Error clearing failed:', error);
throw error;
}
}
/**
* Clear pending downloads
* @returns {Promise<Object>} Response data
*/
async function clearPending() {
try {
const response = await AniWorld.ApiClient.delete(API.QUEUE_PENDING);
if (!response) return null;
return await response.json();
} catch (error) {
console.error('Error clearing pending:', error);
throw error;
}
}
// Public API
return {
loadQueueData: loadQueueData,
startQueue: startQueue,
stopQueue: stopQueue,
removeFromQueue: removeFromQueue,
retryDownloads: retryDownloads,
clearCompleted: clearCompleted,
clearFailed: clearFailed,
clearPending: clearPending
};
})();

View File

@@ -0,0 +1,313 @@
/**
* AniWorld - Queue Page Application Initializer
*
* Main entry point for the queue page. Initializes all modules.
*
* Dependencies: All shared and queue modules
*/
var AniWorld = window.AniWorld || {};
AniWorld.QueueApp = (function() {
'use strict';
/**
* Initialize the queue page application
*/
async function init() {
console.log('AniWorld Queue App initializing...');
// Check authentication first
const isAuthenticated = await AniWorld.Auth.checkAuth();
if (!isAuthenticated) {
return; // Auth module handles redirect
}
// Initialize theme
AniWorld.Theme.init();
// Initialize WebSocket connection
AniWorld.WebSocketClient.init();
// Initialize socket event handlers for this page
AniWorld.QueueSocketHandler.init(AniWorld.QueueApp);
// Bind UI events
bindEvents();
// Load initial data
await loadQueueData();
console.log('AniWorld Queue App initialized successfully');
}
/**
* Bind UI event handlers
*/
function bindEvents() {
// Theme toggle
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', function() {
AniWorld.Theme.toggle();
});
}
// Queue management actions
const clearCompletedBtn = document.getElementById('clear-completed-btn');
if (clearCompletedBtn) {
clearCompletedBtn.addEventListener('click', function() {
clearQueue('completed');
});
}
const clearFailedBtn = document.getElementById('clear-failed-btn');
if (clearFailedBtn) {
clearFailedBtn.addEventListener('click', function() {
clearQueue('failed');
});
}
const clearPendingBtn = document.getElementById('clear-pending-btn');
if (clearPendingBtn) {
clearPendingBtn.addEventListener('click', function() {
clearQueue('pending');
});
}
const retryAllBtn = document.getElementById('retry-all-btn');
if (retryAllBtn) {
retryAllBtn.addEventListener('click', retryAllFailed);
}
// Download controls
const startQueueBtn = document.getElementById('start-queue-btn');
if (startQueueBtn) {
startQueueBtn.addEventListener('click', startDownload);
}
const stopQueueBtn = document.getElementById('stop-queue-btn');
if (stopQueueBtn) {
stopQueueBtn.addEventListener('click', stopDownloads);
}
// Modal events
const closeConfirm = document.getElementById('close-confirm');
if (closeConfirm) {
closeConfirm.addEventListener('click', AniWorld.UI.hideConfirmModal);
}
const confirmCancel = document.getElementById('confirm-cancel');
if (confirmCancel) {
confirmCancel.addEventListener('click', AniWorld.UI.hideConfirmModal);
}
const modalOverlay = document.querySelector('#confirm-modal .modal-overlay');
if (modalOverlay) {
modalOverlay.addEventListener('click', AniWorld.UI.hideConfirmModal);
}
// Logout functionality
const logoutBtn = document.getElementById('logout-btn');
if (logoutBtn) {
logoutBtn.addEventListener('click', function() {
AniWorld.Auth.logout(AniWorld.UI.showToast);
});
}
}
/**
* Load queue data and update display
*/
async function loadQueueData() {
const data = await AniWorld.QueueAPI.loadQueueData();
if (data) {
AniWorld.QueueRenderer.updateQueueDisplay(data);
AniWorld.ProgressHandler.processPendingProgressUpdates();
}
}
/**
* Clear queue by type
* @param {string} type - 'completed', 'failed', or 'pending'
*/
async function clearQueue(type) {
const titles = {
completed: 'Clear Completed Downloads',
failed: 'Clear Failed Downloads',
pending: 'Remove All Pending Downloads'
};
const messages = {
completed: 'Are you sure you want to clear all completed downloads?',
failed: 'Are you sure you want to clear all failed downloads?',
pending: 'Are you sure you want to remove all pending downloads from the queue?'
};
const confirmed = await AniWorld.UI.showConfirmModal(titles[type], messages[type]);
if (!confirmed) return;
try {
let data;
if (type === 'completed') {
data = await AniWorld.QueueAPI.clearCompleted();
AniWorld.UI.showToast('Cleared ' + (data?.count || 0) + ' completed downloads', 'success');
} else if (type === 'failed') {
data = await AniWorld.QueueAPI.clearFailed();
AniWorld.UI.showToast('Cleared ' + (data?.count || 0) + ' failed downloads', 'success');
} else if (type === 'pending') {
data = await AniWorld.QueueAPI.clearPending();
AniWorld.UI.showToast('Removed ' + (data?.count || 0) + ' pending downloads', 'success');
}
await loadQueueData();
} catch (error) {
console.error('Error clearing queue:', error);
AniWorld.UI.showToast('Failed to clear queue', 'error');
}
}
/**
* Retry a failed download
* @param {string} downloadId - Download item ID
*/
async function retryDownload(downloadId) {
try {
const data = await AniWorld.QueueAPI.retryDownloads([downloadId]);
AniWorld.UI.showToast('Retried ' + (data?.retried_count || 1) + ' download(s)', 'success');
await loadQueueData();
} catch (error) {
console.error('Error retrying download:', error);
AniWorld.UI.showToast('Failed to retry download', 'error');
}
}
/**
* Retry all failed downloads
*/
async function retryAllFailed() {
const confirmed = await AniWorld.UI.showConfirmModal(
'Retry All Failed Downloads',
'Are you sure you want to retry all failed downloads?'
);
if (!confirmed) return;
try {
// Get all failed download IDs
const failedCards = document.querySelectorAll('#failed-downloads .download-card.failed');
const itemIds = Array.from(failedCards).map(function(card) {
return card.dataset.id;
}).filter(function(id) {
return id;
});
if (itemIds.length === 0) {
AniWorld.UI.showToast('No failed downloads to retry', 'info');
return;
}
const data = await AniWorld.QueueAPI.retryDownloads(itemIds);
AniWorld.UI.showToast('Retried ' + (data?.retried_count || itemIds.length) + ' download(s)', 'success');
await loadQueueData();
} catch (error) {
console.error('Error retrying failed downloads:', error);
AniWorld.UI.showToast('Failed to retry downloads', 'error');
}
}
/**
* Remove item from queue
* @param {string} downloadId - Download item ID
*/
async function removeFromQueue(downloadId) {
try {
const success = await AniWorld.QueueAPI.removeFromQueue(downloadId);
if (success) {
AniWorld.UI.showToast('Download removed from queue', 'success');
await loadQueueData();
} else {
AniWorld.UI.showToast('Failed to remove from queue', 'error');
}
} catch (error) {
console.error('Error removing from queue:', error);
AniWorld.UI.showToast('Failed to remove from queue', 'error');
}
}
/**
* Start queue processing
*/
async function startDownload() {
try {
const data = await AniWorld.QueueAPI.startQueue();
if (data && data.status === 'success') {
AniWorld.UI.showToast('Queue processing started - all items will download automatically', '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;
await loadQueueData();
} else {
AniWorld.UI.showToast('Failed to start queue: ' + (data?.message || 'Unknown error'), 'error');
}
} catch (error) {
console.error('Error starting queue:', error);
AniWorld.UI.showToast('Failed to start queue processing', 'error');
}
}
/**
* Stop queue processing
*/
async function stopDownloads() {
try {
const data = await AniWorld.QueueAPI.stopQueue();
if (data && data.status === 'success') {
AniWorld.UI.showToast('Queue processing 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;
await loadQueueData();
} else {
AniWorld.UI.showToast('Failed to stop queue: ' + (data?.message || 'Unknown error'), 'error');
}
} catch (error) {
console.error('Error stopping queue:', error);
AniWorld.UI.showToast('Failed to stop queue', 'error');
}
}
// Public API
return {
init: init,
loadQueueData: loadQueueData,
retryDownload: retryDownload,
removeFromQueue: removeFromQueue,
startDownload: startDownload,
stopDownloads: stopDownloads
};
})();
// Initialize the application when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
AniWorld.QueueApp.init();
});
// Expose for inline event handlers (backwards compatibility)
window.queueManager = {
retryDownload: function(id) {
return AniWorld.QueueApp.retryDownload(id);
},
removeFromQueue: function(id) {
return AniWorld.QueueApp.removeFromQueue(id);
},
removeFailedDownload: function(id) {
return AniWorld.QueueApp.removeFromQueue(id);
}
};

View File

@@ -0,0 +1,335 @@
/**
* AniWorld - Queue Renderer Module
*
* Handles rendering of queue items and statistics.
*
* Dependencies: constants.js, ui-utils.js
*/
var AniWorld = window.AniWorld || {};
AniWorld.QueueRenderer = (function() {
'use strict';
/**
* Update full queue display
* @param {Object} data - Queue data
*/
function updateQueueDisplay(data) {
// Update statistics
updateStatistics(data.statistics, data);
// Update active downloads
renderActiveDownloads(data.active_downloads || []);
// Update pending queue
renderPendingQueue(data.pending_queue || []);
// Update completed downloads
renderCompletedDownloads(data.completed_downloads || []);
// Update failed downloads
renderFailedDownloads(data.failed_downloads || []);
// Update button states
updateButtonStates(data);
}
/**
* Update statistics display
* @param {Object} stats - Statistics object
* @param {Object} data - Full queue data
*/
function updateStatistics(stats, data) {
const statistics = stats || {};
document.getElementById('total-items').textContent = statistics.total_items || 0;
document.getElementById('pending-items').textContent = (data.pending_queue || []).length;
document.getElementById('completed-items').textContent = statistics.completed_items || 0;
document.getElementById('failed-items').textContent = statistics.failed_items || 0;
// Update section counts
document.getElementById('queue-count').textContent = (data.pending_queue || []).length;
document.getElementById('completed-count').textContent = statistics.completed_items || 0;
document.getElementById('failed-count').textContent = statistics.failed_items || 0;
document.getElementById('current-speed').textContent = statistics.current_speed || '0 MB/s';
document.getElementById('average-speed').textContent = statistics.average_speed || '0 MB/s';
// Format ETA
const etaElement = document.getElementById('eta-time');
if (statistics.eta) {
const eta = new Date(statistics.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 = '--:--';
}
}
/**
* Render active downloads
* @param {Array} downloads - Active downloads array
*/
function 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(function(download) {
return createActiveDownloadCard(download);
}).join('');
}
/**
* Create active download card HTML
* @param {Object} download - Download item
* @returns {string} HTML string
*/
function 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 episodeNum = String(download.episode.episode).padStart(2, '0');
const episodeTitle = download.episode.title || 'Episode ' + download.episode.episode;
return '<div class="download-card active" data-download-id="' + download.id + '">' +
'<div class="download-header">' +
'<div class="download-info">' +
'<h4>' + AniWorld.UI.escapeHtml(download.serie_name) + '</h4>' +
'<p>' + AniWorld.UI.escapeHtml(download.episode.season) + 'x' + episodeNum + ' - ' +
AniWorld.UI.escapeHtml(episodeTitle) + '</p>' +
'</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 > 0 ? progressPercent.toFixed(1) + '%' : 'Starting...') + '</span>' +
'<span class="download-speed">' + speed + '</span>' +
'</div>' +
'</div>' +
'</div>';
}
/**
* Render pending queue
* @param {Array} queue - Pending queue array
*/
function 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>' +
'<small>Add episodes from the main page to start downloading</small>' +
'</div>';
return;
}
container.innerHTML = queue.map(function(item, index) {
return createPendingQueueCard(item, index);
}).join('');
}
/**
* Create pending queue card HTML
* @param {Object} download - Download item
* @param {number} index - Queue position
* @returns {string} HTML string
*/
function createPendingQueueCard(download, index) {
const addedAt = new Date(download.added_at).toLocaleString();
const episodeNum = String(download.episode.episode).padStart(2, '0');
const episodeTitle = download.episode.title || 'Episode ' + download.episode.episode;
return '<div class="download-card pending" data-id="' + download.id + '" data-index="' + index + '">' +
'<div class="queue-position">' + (index + 1) + '</div>' +
'<div class="download-header">' +
'<div class="download-info">' +
'<h4>' + AniWorld.UI.escapeHtml(download.serie_name) + '</h4>' +
'<p>' + AniWorld.UI.escapeHtml(download.episode.season) + 'x' + episodeNum + ' - ' +
AniWorld.UI.escapeHtml(episodeTitle) + '</p>' +
'<small>Added: ' + addedAt + '</small>' +
'</div>' +
'<div class="download-actions">' +
'<button class="btn btn-small btn-secondary" onclick="AniWorld.QueueApp.removeFromQueue(\'' + download.id + '\')">' +
'<i class="fas fa-trash"></i>' +
'</button>' +
'</div>' +
'</div>' +
'</div>';
}
/**
* Render completed downloads
* @param {Array} downloads - Completed downloads array
*/
function 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(function(download) {
return createCompletedDownloadCard(download);
}).join('');
}
/**
* Create completed download card HTML
* @param {Object} download - Download item
* @returns {string} HTML string
*/
function createCompletedDownloadCard(download) {
const completedAt = new Date(download.completed_at).toLocaleString();
const duration = AniWorld.UI.calculateDuration(download.started_at, download.completed_at);
const episodeNum = String(download.episode.episode).padStart(2, '0');
const episodeTitle = download.episode.title || 'Episode ' + download.episode.episode;
return '<div class="download-card completed">' +
'<div class="download-header">' +
'<div class="download-info">' +
'<h4>' + AniWorld.UI.escapeHtml(download.serie_name) + '</h4>' +
'<p>' + AniWorld.UI.escapeHtml(download.episode.season) + 'x' + episodeNum + ' - ' +
AniWorld.UI.escapeHtml(episodeTitle) + '</p>' +
'<small>Completed: ' + completedAt + ' (' + duration + ')</small>' +
'</div>' +
'<div class="download-status">' +
'<i class="fas fa-check-circle text-success"></i>' +
'</div>' +
'</div>' +
'</div>';
}
/**
* Render failed downloads
* @param {Array} downloads - Failed downloads array
*/
function 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(function(download) {
return createFailedDownloadCard(download);
}).join('');
}
/**
* Create failed download card HTML
* @param {Object} download - Download item
* @returns {string} HTML string
*/
function createFailedDownloadCard(download) {
const failedAt = new Date(download.completed_at).toLocaleString();
const retryCount = download.retry_count || 0;
const episodeNum = String(download.episode.episode).padStart(2, '0');
const episodeTitle = download.episode.title || 'Episode ' + download.episode.episode;
return '<div class="download-card failed" data-id="' + download.id + '">' +
'<div class="download-header">' +
'<div class="download-info">' +
'<h4>' + AniWorld.UI.escapeHtml(download.serie_name) + '</h4>' +
'<p>' + AniWorld.UI.escapeHtml(download.episode.season) + 'x' + episodeNum + ' - ' +
AniWorld.UI.escapeHtml(episodeTitle) + '</p>' +
'<small>Failed: ' + failedAt + (retryCount > 0 ? ' (Retry ' + retryCount + ')' : '') + '</small>' +
(download.error ? '<small class="error-message">' + AniWorld.UI.escapeHtml(download.error) + '</small>' : '') +
'</div>' +
'<div class="download-actions">' +
'<button class="btn btn-small btn-warning" onclick="AniWorld.QueueApp.retryDownload(\'' + download.id + '\')">' +
'<i class="fas fa-redo"></i>' +
'</button>' +
'<button class="btn btn-small btn-secondary" onclick="AniWorld.QueueApp.removeFromQueue(\'' + download.id + '\')">' +
'<i class="fas fa-trash"></i>' +
'</button>' +
'</div>' +
'</div>' +
'</div>';
}
/**
* Update button states based on queue data
* @param {Object} data - Queue data
*/
function updateButtonStates(data) {
const hasActive = (data.active_downloads || []).length > 0;
const hasPending = (data.pending_queue || []).length > 0;
const hasFailed = (data.failed_downloads || []).length > 0;
const hasCompleted = (data.completed_downloads || []).length > 0;
console.log('Button states update:', {
hasPending: hasPending,
pendingCount: (data.pending_queue || []).length,
hasActive: hasActive,
hasFailed: hasFailed,
hasCompleted: hasCompleted
});
// 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('retry-all-btn').disabled = !hasFailed;
document.getElementById('clear-completed-btn').disabled = !hasCompleted;
document.getElementById('clear-failed-btn').disabled = !hasFailed;
// Update clear pending button if it exists
const clearPendingBtn = document.getElementById('clear-pending-btn');
if (clearPendingBtn) {
clearPendingBtn.disabled = !hasPending;
}
}
// Public API
return {
updateQueueDisplay: updateQueueDisplay,
updateStatistics: updateStatistics,
renderActiveDownloads: renderActiveDownloads,
renderPendingQueue: renderPendingQueue,
renderCompletedDownloads: renderCompletedDownloads,
renderFailedDownloads: renderFailedDownloads,
updateButtonStates: updateButtonStates
};
})();

View File

@@ -0,0 +1,161 @@
/**
* AniWorld - Queue Socket Handler Module
*
* Handles WebSocket events specific to the queue page.
*
* Dependencies: constants.js, websocket-client.js, ui-utils.js, queue-renderer.js, progress-handler.js
*/
var AniWorld = window.AniWorld || {};
AniWorld.QueueSocketHandler = (function() {
'use strict';
const WS_EVENTS = AniWorld.Constants.WS_EVENTS;
// Reference to queue app for data reloading
let queueApp = null;
/**
* Initialize socket handler
* @param {Object} app - Reference to queue app
*/
function init(app) {
queueApp = app;
setupSocketHandlers();
}
/**
* Set up WebSocket event handlers
*/
function setupSocketHandlers() {
const socket = AniWorld.WebSocketClient.getSocket();
if (!socket) {
console.warn('Socket not available for handler setup');
return;
}
// Connection events
socket.on('connect', function() {
AniWorld.UI.showToast('Connected to server', 'success');
});
socket.on('disconnect', function() {
AniWorld.UI.showToast('Disconnected from server', 'warning');
});
// Queue update events - handle both old and new message types
socket.on('queue_updated', function(data) {
AniWorld.QueueRenderer.updateQueueDisplay(data);
});
socket.on('queue_status', function(data) {
// New backend sends queue_status messages with nested structure
if (data.status && data.statistics) {
const queueData = {
...data.status,
statistics: data.statistics
};
AniWorld.QueueRenderer.updateQueueDisplay(queueData);
} else if (data.queue_status) {
AniWorld.QueueRenderer.updateQueueDisplay(data.queue_status);
} else {
AniWorld.QueueRenderer.updateQueueDisplay(data);
}
});
// Download started events
socket.on('download_started', function() {
AniWorld.UI.showToast('Download queue started', 'success');
if (queueApp) queueApp.loadQueueData();
});
socket.on('queue_started', function() {
AniWorld.UI.showToast('Download queue started', 'success');
if (queueApp) queueApp.loadQueueData();
});
// Download progress
socket.on('download_progress', function(data) {
console.log('Received download progress:', data);
const success = AniWorld.ProgressHandler.updateDownloadProgress(data);
if (!success && queueApp) {
// Card not found, reload queue
queueApp.loadQueueData();
}
});
// Download complete events
const handleDownloadComplete = function(data) {
const serieName = data.serie_name || data.serie || 'Unknown';
const episode = data.episode || '';
AniWorld.UI.showToast('Completed: ' + serieName + (episode ? ' - Episode ' + episode : ''), 'success');
// Clear pending progress updates
const downloadId = data.item_id || data.download_id || data.id;
if (downloadId) {
AniWorld.ProgressHandler.clearPendingUpdate(downloadId);
}
if (queueApp) queueApp.loadQueueData();
};
socket.on(WS_EVENTS.DOWNLOAD_COMPLETED, handleDownloadComplete);
socket.on(WS_EVENTS.DOWNLOAD_COMPLETE, handleDownloadComplete);
// Download error events
const handleDownloadError = function(data) {
const message = data.error || data.message || 'Unknown error';
AniWorld.UI.showToast('Download failed: ' + message, 'error');
// Clear pending progress updates
const downloadId = data.item_id || data.download_id || data.id;
if (downloadId) {
AniWorld.ProgressHandler.clearPendingUpdate(downloadId);
}
if (queueApp) queueApp.loadQueueData();
};
socket.on(WS_EVENTS.DOWNLOAD_ERROR, handleDownloadError);
socket.on(WS_EVENTS.DOWNLOAD_FAILED, handleDownloadError);
// Queue completed events
socket.on(WS_EVENTS.DOWNLOAD_QUEUE_COMPLETED, function() {
AniWorld.UI.showToast('All downloads completed!', 'success');
if (queueApp) queueApp.loadQueueData();
});
socket.on(WS_EVENTS.QUEUE_COMPLETED, function() {
AniWorld.UI.showToast('All downloads completed!', 'success');
if (queueApp) queueApp.loadQueueData();
});
// Download stop requested
socket.on(WS_EVENTS.DOWNLOAD_STOP_REQUESTED, function() {
AniWorld.UI.showToast('Stopping downloads...', 'info');
});
// Queue stopped events
const handleQueueStopped = function() {
AniWorld.UI.showToast('Download queue stopped', 'success');
if (queueApp) queueApp.loadQueueData();
};
socket.on(WS_EVENTS.DOWNLOAD_STOPPED, handleQueueStopped);
socket.on(WS_EVENTS.QUEUE_STOPPED, handleQueueStopped);
// Queue paused/resumed
socket.on(WS_EVENTS.QUEUE_PAUSED, function() {
AniWorld.UI.showToast('Queue paused', 'info');
if (queueApp) queueApp.loadQueueData();
});
socket.on(WS_EVENTS.QUEUE_RESUMED, function() {
AniWorld.UI.showToast('Queue resumed', 'success');
if (queueApp) queueApp.loadQueueData();
});
}
// Public API
return {
init: init
};
})();