336 lines
13 KiB
JavaScript
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
|
|
};
|
|
})();
|