Files
Aniworld/src/server/web/static/js/queue/queue-renderer.js

336 lines
13 KiB
JavaScript

/**
* 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
};
})();