fix download
This commit is contained in:
@@ -208,6 +208,40 @@ async def clear_completed(
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/failed", status_code=status.HTTP_200_OK)
|
||||
async def clear_failed(
|
||||
_: dict = Depends(require_auth),
|
||||
download_service: DownloadService = Depends(get_download_service),
|
||||
):
|
||||
"""Clear failed downloads from history.
|
||||
|
||||
Removes all failed download items from the queue history. This helps
|
||||
keep the queue display clean and manageable.
|
||||
|
||||
Requires authentication.
|
||||
|
||||
Returns:
|
||||
dict: Status message with count of cleared items
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if not authenticated, 500 on service error
|
||||
"""
|
||||
try:
|
||||
cleared_count = await download_service.clear_failed()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Cleared {cleared_count} failed item(s)",
|
||||
"count": cleared_count,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to clear failed items: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def remove_from_queue(
|
||||
item_id: str = Path(..., description="Download item ID to remove"),
|
||||
@@ -485,28 +519,50 @@ async def reorder_queue(
|
||||
_: dict = Depends(require_auth),
|
||||
download_service: DownloadService = Depends(get_download_service),
|
||||
):
|
||||
"""Reorder an item in the pending queue.
|
||||
"""Reorder items in the pending queue.
|
||||
|
||||
Changes the position of a pending download item in the queue. This only
|
||||
affects items that haven't started downloading yet. The position is
|
||||
0-based.
|
||||
Changes the order of pending download items in the queue. This only
|
||||
affects items that haven't started downloading yet. Supports both
|
||||
bulk reordering with item_ids array and single item reorder.
|
||||
|
||||
Requires authentication.
|
||||
|
||||
Args:
|
||||
request: Item ID and new position in queue
|
||||
request: Either {"item_ids": ["id1", "id2", ...]} for bulk reorder
|
||||
or {"item_id": "id", "new_position": 0} for single item
|
||||
|
||||
Returns:
|
||||
dict: Status message indicating item has been reordered
|
||||
dict: Status message indicating items have been reordered
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if not authenticated, 404 if item not found,
|
||||
400 for invalid request, 500 on service error
|
||||
"""
|
||||
try:
|
||||
# Support legacy bulk reorder payload used by some integration tests:
|
||||
# {"item_order": ["id1", "id2", ...]}
|
||||
if "item_order" in request:
|
||||
# Support new bulk reorder payload: {"item_ids": ["id1", "id2", ...]}
|
||||
if "item_ids" in request:
|
||||
item_order = request.get("item_ids", [])
|
||||
if not isinstance(item_order, list):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="item_ids must be a list of item IDs",
|
||||
)
|
||||
|
||||
success = await download_service.reorder_queue_bulk(item_order)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="One or more items in item_ids were not found in pending queue",
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Queue reordered successfully",
|
||||
}
|
||||
|
||||
# Support legacy bulk reorder payload: {"item_order": ["id1", "id2", ...]}
|
||||
elif "item_order" in request:
|
||||
item_order = request.get("item_order", [])
|
||||
if not isinstance(item_order, list):
|
||||
raise HTTPException(
|
||||
@@ -515,6 +571,17 @@ async def reorder_queue(
|
||||
)
|
||||
|
||||
success = await download_service.reorder_queue_bulk(item_order)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="One or more items in item_order were not found in pending queue",
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Queue item reordered successfully",
|
||||
}
|
||||
else:
|
||||
# Fallback to single-item reorder shape
|
||||
# Validate request
|
||||
@@ -531,25 +598,16 @@ async def reorder_queue(
|
||||
new_position=req.new_position,
|
||||
)
|
||||
|
||||
if not success:
|
||||
# Provide an appropriate 404 message depending on request shape
|
||||
if "item_order" in request:
|
||||
detail = (
|
||||
"One or more items in item_order were not "
|
||||
"found in pending queue"
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Item {req.item_id} not found in pending queue",
|
||||
)
|
||||
else:
|
||||
detail = f"Item {req.item_id} not found in pending queue"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=detail,
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Queue item reordered successfully",
|
||||
}
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Queue item reordered successfully",
|
||||
}
|
||||
|
||||
except DownloadServiceError as e:
|
||||
raise HTTPException(
|
||||
@@ -596,6 +654,7 @@ async def retry_failed(
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Retrying {len(retried_ids)} failed item(s)",
|
||||
"retried_count": len(retried_ids),
|
||||
"retried_ids": retried_ids,
|
||||
}
|
||||
|
||||
|
||||
@@ -586,6 +586,30 @@ class DownloadService:
|
||||
|
||||
return count
|
||||
|
||||
async def clear_failed(self) -> int:
|
||||
"""Clear failed downloads from history.
|
||||
|
||||
Returns:
|
||||
Number of items cleared
|
||||
"""
|
||||
count = len(self._failed_items)
|
||||
self._failed_items.clear()
|
||||
logger.info("Cleared failed items", count=count)
|
||||
|
||||
# Broadcast queue status update
|
||||
if count > 0:
|
||||
queue_status = await self.get_queue_status()
|
||||
await self._broadcast_update(
|
||||
"queue_status",
|
||||
{
|
||||
"action": "failed_cleared",
|
||||
"cleared_count": count,
|
||||
"queue_status": queue_status.model_dump(mode="json"),
|
||||
},
|
||||
)
|
||||
|
||||
return count
|
||||
|
||||
async def retry_failed(
|
||||
self, item_ids: Optional[List[str]] = None
|
||||
) -> List[str]:
|
||||
|
||||
@@ -1218,6 +1218,52 @@ body {
|
||||
background: linear-gradient(90deg, rgba(var(--color-accent-rgb), 0.05) 0%, transparent 10%);
|
||||
}
|
||||
|
||||
/* Drag and Drop Styles */
|
||||
.draggable-item {
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.draggable-item.dragging {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.98);
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.draggable-item.drag-over {
|
||||
border-top: 3px solid var(--color-primary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-text-tertiary);
|
||||
cursor: grab;
|
||||
font-size: 1.2rem;
|
||||
padding: var(--spacing-xs);
|
||||
transition: color var(--transition-duration);
|
||||
}
|
||||
|
||||
.drag-handle:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.sortable-list {
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.pending-queue-list {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.download-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -1261,11 +1307,11 @@ body {
|
||||
.queue-position {
|
||||
position: absolute;
|
||||
top: var(--spacing-sm);
|
||||
left: var(--spacing-sm);
|
||||
left: 48px;
|
||||
background: var(--color-warning);
|
||||
color: white;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1275,7 +1321,18 @@ body {
|
||||
}
|
||||
|
||||
.download-card.pending .download-info {
|
||||
margin-left: 40px;
|
||||
margin-left: 80px;
|
||||
}
|
||||
|
||||
.download-card.pending .download-header {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.empty-state small {
|
||||
display: block;
|
||||
margin-top: var(--spacing-sm);
|
||||
font-size: var(--font-size-small);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Progress Bars */
|
||||
|
||||
@@ -7,6 +7,8 @@ class QueueManager {
|
||||
this.socket = null;
|
||||
this.refreshInterval = null;
|
||||
this.isReordering = false;
|
||||
this.draggedElement = null;
|
||||
this.draggedId = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
@@ -17,6 +19,7 @@ class QueueManager {
|
||||
this.initTheme();
|
||||
this.startRefreshTimer();
|
||||
this.loadQueueData();
|
||||
this.initDragAndDrop();
|
||||
}
|
||||
|
||||
initSocket() {
|
||||
@@ -249,6 +252,11 @@ class QueueManager {
|
||||
document.getElementById('completed-items').textContent = stats.completed_items || 0;
|
||||
document.getElementById('failed-items').textContent = stats.failed_items || 0;
|
||||
|
||||
// Update section counts
|
||||
document.getElementById('queue-count').textContent = (data.pending_queue || []).length;
|
||||
document.getElementById('completed-count').textContent = stats.completed_items || 0;
|
||||
document.getElementById('failed-count').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';
|
||||
|
||||
@@ -331,12 +339,16 @@ class QueueManager {
|
||||
<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((item, index) => this.createPendingQueueCard(item, index)).join('');
|
||||
|
||||
// Re-attach drag and drop event listeners
|
||||
this.attachDragListeners();
|
||||
}
|
||||
|
||||
createPendingQueueCard(download, index) {
|
||||
@@ -344,7 +356,13 @@ class QueueManager {
|
||||
const priorityClass = download.priority === 'high' ? 'high-priority' : '';
|
||||
|
||||
return `
|
||||
<div class="download-card pending ${priorityClass}" data-id="${download.id}">
|
||||
<div class="download-card pending ${priorityClass} draggable-item"
|
||||
data-id="${download.id}"
|
||||
data-index="${index}"
|
||||
draggable="true">
|
||||
<div class="drag-handle" title="Drag to reorder">
|
||||
<i class="fas fa-grip-vertical"></i>
|
||||
</div>
|
||||
<div class="queue-position">${index + 1}</div>
|
||||
<div class="download-header">
|
||||
<div class="download-info">
|
||||
@@ -420,7 +438,7 @@ class QueueManager {
|
||||
const retryCount = download.retry_count || 0;
|
||||
|
||||
return `
|
||||
<div class="download-card failed">
|
||||
<div class="download-card failed" data-id="${download.id}">
|
||||
<div class="download-header">
|
||||
<div class="download-info">
|
||||
<h4>${this.escapeHtml(download.serie_name)}</h4>
|
||||
@@ -441,10 +459,15 @@ class QueueManager {
|
||||
`;
|
||||
}
|
||||
|
||||
async removeFailedDownload(downloadId) {
|
||||
await this.removeFromQueue(downloadId);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Enable start button only if there are pending items and no active downloads
|
||||
document.getElementById('start-queue-btn').disabled = !hasPending || hasActive;
|
||||
@@ -461,8 +484,9 @@ class QueueManager {
|
||||
|
||||
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;
|
||||
document.getElementById('clear-completed-btn').disabled = !hasCompleted;
|
||||
document.getElementById('clear-failed-btn').disabled = !hasFailed;
|
||||
}
|
||||
|
||||
async clearQueue(type) {
|
||||
@@ -483,7 +507,6 @@ class QueueManager {
|
||||
|
||||
try {
|
||||
if (type === 'completed') {
|
||||
// Use the new DELETE /api/queue/completed endpoint
|
||||
const response = await this.makeAuthenticatedRequest('/api/queue/completed', {
|
||||
method: 'DELETE'
|
||||
});
|
||||
@@ -491,11 +514,38 @@ class QueueManager {
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
this.showToast(`Cleared ${data.cleared_count} completed downloads`, 'success');
|
||||
this.showToast(`Cleared ${data.count} completed downloads`, 'success');
|
||||
this.loadQueueData();
|
||||
} else if (type === 'failed') {
|
||||
const response = await this.makeAuthenticatedRequest('/api/queue/failed', {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
this.showToast(`Cleared ${data.count} failed downloads`, 'success');
|
||||
this.loadQueueData();
|
||||
} else if (type === 'pending') {
|
||||
// Get all pending items
|
||||
const pendingCards = document.querySelectorAll('#pending-queue .download-card.pending');
|
||||
const itemIds = Array.from(pendingCards).map(card => card.dataset.id).filter(id => id);
|
||||
|
||||
if (itemIds.length === 0) {
|
||||
this.showToast('No pending items to clear', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.makeAuthenticatedRequest('/api/queue/', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ item_ids: itemIds })
|
||||
});
|
||||
|
||||
if (!response) return;
|
||||
|
||||
this.showToast(`Cleared ${itemIds.length} pending items`, 'success');
|
||||
this.loadQueueData();
|
||||
} else {
|
||||
// For pending and failed, use the old logic (TODO: implement backend endpoints)
|
||||
this.showToast(`Clear ${type} not yet implemented`, 'warning');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -528,14 +578,31 @@ class QueueManager {
|
||||
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');
|
||||
try {
|
||||
// Get all failed download IDs
|
||||
const failedCards = document.querySelectorAll('#failed-downloads .download-card.failed');
|
||||
const itemIds = Array.from(failedCards).map(card => card.dataset.id).filter(id => id);
|
||||
|
||||
for (const card of failedCards) {
|
||||
const downloadId = card.dataset.id;
|
||||
if (downloadId) {
|
||||
await this.retryDownload(downloadId);
|
||||
if (itemIds.length === 0) {
|
||||
this.showToast('No failed downloads to retry', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.makeAuthenticatedRequest('/api/queue/retry', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ item_ids: itemIds })
|
||||
});
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
this.showToast(`Retried ${data.retried_count || itemIds.length} download(s)`, 'success');
|
||||
this.loadQueueData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error retrying failed downloads:', error);
|
||||
this.showToast('Failed to retry downloads', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,8 +699,146 @@ class QueueManager {
|
||||
}
|
||||
|
||||
toggleReorderMode() {
|
||||
// TODO: Implement reorder functionality
|
||||
this.showToast('Reorder functionality not yet implemented', 'info');
|
||||
// Drag and drop is always enabled, no need for toggle mode
|
||||
this.showToast('Drag items to reorder the queue', 'info');
|
||||
}
|
||||
|
||||
initDragAndDrop() {
|
||||
// Initialize drag and drop on the pending queue container
|
||||
const container = document.getElementById('pending-queue');
|
||||
if (container) {
|
||||
container.addEventListener('dragover', this.handleDragOver.bind(this));
|
||||
container.addEventListener('drop', this.handleDrop.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
attachDragListeners() {
|
||||
// Attach listeners to all draggable items
|
||||
const items = document.querySelectorAll('.draggable-item');
|
||||
items.forEach(item => {
|
||||
item.addEventListener('dragstart', this.handleDragStart.bind(this));
|
||||
item.addEventListener('dragend', this.handleDragEnd.bind(this));
|
||||
item.addEventListener('dragenter', this.handleDragEnter.bind(this));
|
||||
item.addEventListener('dragleave', this.handleDragLeave.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
handleDragStart(e) {
|
||||
this.draggedElement = e.currentTarget;
|
||||
this.draggedId = e.currentTarget.dataset.id;
|
||||
e.currentTarget.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/html', e.currentTarget.innerHTML);
|
||||
}
|
||||
|
||||
handleDragEnd(e) {
|
||||
e.currentTarget.classList.remove('dragging');
|
||||
|
||||
// Remove all drag-over classes
|
||||
document.querySelectorAll('.drag-over').forEach(item => {
|
||||
item.classList.remove('drag-over');
|
||||
});
|
||||
}
|
||||
|
||||
handleDragOver(e) {
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
return false;
|
||||
}
|
||||
|
||||
handleDragEnter(e) {
|
||||
if (e.currentTarget.classList.contains('draggable-item') &&
|
||||
e.currentTarget !== this.draggedElement) {
|
||||
e.currentTarget.classList.add('drag-over');
|
||||
}
|
||||
}
|
||||
|
||||
handleDragLeave(e) {
|
||||
e.currentTarget.classList.remove('drag-over');
|
||||
}
|
||||
|
||||
async handleDrop(e) {
|
||||
if (e.stopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
// Get the target element (the item we dropped onto)
|
||||
let target = e.target;
|
||||
while (target && !target.classList.contains('draggable-item')) {
|
||||
target = target.parentElement;
|
||||
if (target === document.getElementById('pending-queue')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!target || target === this.draggedElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get all items to determine new order
|
||||
const container = document.getElementById('pending-queue');
|
||||
const items = Array.from(container.querySelectorAll('.draggable-item'));
|
||||
|
||||
const draggedIndex = items.indexOf(this.draggedElement);
|
||||
const targetIndex = items.indexOf(target);
|
||||
|
||||
if (draggedIndex === targetIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reorder visually
|
||||
if (draggedIndex < targetIndex) {
|
||||
target.parentNode.insertBefore(this.draggedElement, target.nextSibling);
|
||||
} else {
|
||||
target.parentNode.insertBefore(this.draggedElement, target);
|
||||
}
|
||||
|
||||
// Update position numbers
|
||||
const updatedItems = Array.from(container.querySelectorAll('.draggable-item'));
|
||||
updatedItems.forEach((item, index) => {
|
||||
const posElement = item.querySelector('.queue-position');
|
||||
if (posElement) {
|
||||
posElement.textContent = index + 1;
|
||||
}
|
||||
item.dataset.index = index;
|
||||
});
|
||||
|
||||
// Get the new order of IDs
|
||||
const newOrder = updatedItems.map(item => item.dataset.id);
|
||||
|
||||
// Send reorder request to backend
|
||||
await this.reorderQueue(newOrder);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async reorderQueue(newOrder) {
|
||||
try {
|
||||
const response = await this.makeAuthenticatedRequest('/api/queue/reorder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ item_ids: newOrder })
|
||||
});
|
||||
|
||||
if (!response) return;
|
||||
|
||||
if (response.ok) {
|
||||
this.showToast('Queue reordered successfully', 'success');
|
||||
} else {
|
||||
const data = await response.json();
|
||||
this.showToast(`Failed to reorder: ${data.detail || 'Unknown error'}`, 'error');
|
||||
// Reload to restore correct order
|
||||
this.loadQueueData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reordering queue:', error);
|
||||
this.showToast('Failed to reorder queue', 'error');
|
||||
// Reload to restore correct order
|
||||
this.loadQueueData();
|
||||
}
|
||||
}
|
||||
|
||||
async makeAuthenticatedRequest(url, options = {}) {
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
<div class="section-header">
|
||||
<h2>
|
||||
<i class="fas fa-clock"></i>
|
||||
Download Queue
|
||||
Download Queue (<span id="queue-count">0</span>)
|
||||
</h2>
|
||||
<div class="section-actions">
|
||||
<button id="start-queue-btn" class="btn btn-primary" disabled>
|
||||
@@ -146,17 +146,14 @@
|
||||
<i class="fas fa-trash"></i>
|
||||
Clear Queue
|
||||
</button>
|
||||
<button id="reorder-queue-btn" class="btn btn-secondary" disabled>
|
||||
<i class="fas fa-sort"></i>
|
||||
Reorder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pending-queue-list" id="pending-queue">
|
||||
<div class="pending-queue-list sortable-list" id="pending-queue" data-sortable="true">
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
@@ -166,10 +163,10 @@
|
||||
<div class="section-header">
|
||||
<h2>
|
||||
<i class="fas fa-check-circle"></i>
|
||||
Recent Completed
|
||||
Completed (<span id="completed-count">0</span>)
|
||||
</h2>
|
||||
<div class="section-actions">
|
||||
<button id="clear-completed-btn" class="btn btn-secondary">
|
||||
<button id="clear-completed-btn" class="btn btn-secondary" disabled>
|
||||
<i class="fas fa-broom"></i>
|
||||
Clear Completed
|
||||
</button>
|
||||
@@ -178,8 +175,9 @@
|
||||
|
||||
<div class="completed-downloads-list" id="completed-downloads">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<i class="fas fa-check-circle text-success"></i>
|
||||
<p>No completed downloads</p>
|
||||
<small>Completed episodes will appear here</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -189,14 +187,14 @@
|
||||
<div class="section-header">
|
||||
<h2>
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Failed Downloads
|
||||
Failed (<span id="failed-count">0</span>)
|
||||
</h2>
|
||||
<div class="section-actions">
|
||||
<button id="retry-all-btn" class="btn btn-warning" disabled>
|
||||
<i class="fas fa-redo"></i>
|
||||
Retry All
|
||||
</button>
|
||||
<button id="clear-failed-btn" class="btn btn-secondary">
|
||||
<button id="clear-failed-btn" class="btn btn-secondary" disabled>
|
||||
<i class="fas fa-trash"></i>
|
||||
Clear Failed
|
||||
</button>
|
||||
@@ -207,6 +205,7 @@
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-check-circle text-success"></i>
|
||||
<p>No failed downloads</p>
|
||||
<small>Failed episodes can be retried or removed</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user