progress
This commit is contained in:
parent
a78732fb08
commit
afd5662afa
3 changed files with 563 additions and 1 deletions
|
|
@ -592,6 +592,81 @@ def start_download():
|
||||||
print(f"Download error: {e}")
|
print(f"Download error: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/downloads/status')
|
||||||
|
def get_download_status():
|
||||||
|
"""
|
||||||
|
Sophisticated download status fetching that matches GUI's update_download_status method.
|
||||||
|
Fetches raw transfer data directly from slskd and flattens it like the GUI.
|
||||||
|
"""
|
||||||
|
if not soulseek_client:
|
||||||
|
return jsonify({"transfers": []})
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fetch raw transfers data exactly like GUI's StatusProcessingWorker
|
||||||
|
transfers_data = asyncio.run(soulseek_client._make_request('GET', 'transfers/downloads'))
|
||||||
|
|
||||||
|
if not transfers_data:
|
||||||
|
return jsonify({"transfers": []})
|
||||||
|
|
||||||
|
# Flatten the nested data structure exactly like the GUI
|
||||||
|
all_transfers = []
|
||||||
|
for user_data in transfers_data:
|
||||||
|
if 'directories' in user_data:
|
||||||
|
for directory in user_data['directories']:
|
||||||
|
if 'files' in directory:
|
||||||
|
for file_info in directory['files']:
|
||||||
|
# Add username to each file object for convenience
|
||||||
|
file_info['username'] = user_data.get('username', 'Unknown')
|
||||||
|
all_transfers.append(file_info)
|
||||||
|
|
||||||
|
return jsonify({"transfers": all_transfers})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching download status: {e}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/downloads/cancel', methods=['POST'])
|
||||||
|
def cancel_download():
|
||||||
|
"""
|
||||||
|
Cancel a specific download transfer, matching GUI functionality.
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({"success": False, "error": "No data provided."}), 400
|
||||||
|
|
||||||
|
download_id = data.get('download_id')
|
||||||
|
username = data.get('username')
|
||||||
|
|
||||||
|
if not all([download_id, username]):
|
||||||
|
return jsonify({"success": False, "error": "Missing download_id or username."}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Call the same client method the GUI uses
|
||||||
|
success = asyncio.run(soulseek_client.cancel_download(download_id, username, remove=True))
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "message": "Download cancelled."})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "error": "Failed to cancel download via slskd."}), 500
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cancelling download: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/downloads/clear-finished', methods=['POST'])
|
||||||
|
def clear_finished_downloads():
|
||||||
|
"""
|
||||||
|
Clear all terminal (completed, cancelled, failed) downloads from slskd.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# This single client call handles clearing everything that is no longer active
|
||||||
|
success = asyncio.run(soulseek_client.clear_all_completed_downloads())
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "message": "Finished downloads cleared."})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "error": "Backend failed to clear downloads."}), 500
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error clearing finished downloads: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/artists')
|
@app.route('/api/artists')
|
||||||
def get_artists():
|
def get_artists():
|
||||||
# Placeholder: returns mock artist data
|
# Placeholder: returns mock artist data
|
||||||
|
|
|
||||||
|
|
@ -96,18 +96,26 @@ async function loadPageData(pageId) {
|
||||||
try {
|
try {
|
||||||
switch (pageId) {
|
switch (pageId) {
|
||||||
case 'dashboard':
|
case 'dashboard':
|
||||||
|
// Stop download polling when leaving downloads page
|
||||||
|
stopDownloadPolling();
|
||||||
await loadDashboardData();
|
await loadDashboardData();
|
||||||
break;
|
break;
|
||||||
case 'sync':
|
case 'sync':
|
||||||
|
// Stop download polling when leaving downloads page
|
||||||
|
stopDownloadPolling();
|
||||||
await loadSyncData();
|
await loadSyncData();
|
||||||
break;
|
break;
|
||||||
case 'downloads':
|
case 'downloads':
|
||||||
await loadDownloadsData();
|
await loadDownloadsData();
|
||||||
break;
|
break;
|
||||||
case 'artists':
|
case 'artists':
|
||||||
|
// Stop download polling when leaving downloads page
|
||||||
|
stopDownloadPolling();
|
||||||
await loadArtistsData();
|
await loadArtistsData();
|
||||||
break;
|
break;
|
||||||
case 'settings':
|
case 'settings':
|
||||||
|
// Stop download polling when leaving downloads page
|
||||||
|
stopDownloadPolling();
|
||||||
await loadSettingsData();
|
await loadSettingsData();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -921,6 +929,12 @@ async function loadSyncData() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Download tracking state management - matching GUI functionality
|
||||||
|
let activeDownloads = {};
|
||||||
|
let finishedDownloads = {};
|
||||||
|
let downloadStatusInterval = null;
|
||||||
|
let isDownloadPollingActive = false;
|
||||||
|
|
||||||
async function loadDownloadsData() {
|
async function loadDownloadsData() {
|
||||||
// Downloads page loads search results dynamically
|
// Downloads page loads search results dynamically
|
||||||
console.log('Downloads page loaded');
|
console.log('Downloads page loaded');
|
||||||
|
|
@ -928,6 +942,7 @@ async function loadDownloadsData() {
|
||||||
// Connect downloads search button
|
// Connect downloads search button
|
||||||
const searchButton = document.getElementById('downloads-search-btn');
|
const searchButton = document.getElementById('downloads-search-btn');
|
||||||
const searchInput = document.getElementById('downloads-search-input');
|
const searchInput = document.getElementById('downloads-search-input');
|
||||||
|
const clearButton = document.querySelector('.controls-panel__clear-btn');
|
||||||
|
|
||||||
if (searchButton && searchInput) {
|
if (searchButton && searchInput) {
|
||||||
searchButton.addEventListener('click', performDownloadsSearch);
|
searchButton.addEventListener('click', performDownloadsSearch);
|
||||||
|
|
@ -935,6 +950,264 @@ async function loadDownloadsData() {
|
||||||
if (e.key === 'Enter') performDownloadsSearch();
|
if (e.key === 'Enter') performDownloadsSearch();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (clearButton) {
|
||||||
|
clearButton.addEventListener('click', clearFinishedDownloads);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start sophisticated polling system (1-second interval like GUI)
|
||||||
|
startDownloadPolling();
|
||||||
|
|
||||||
|
// Initialize tab management
|
||||||
|
initializeDownloadTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDownloadPolling() {
|
||||||
|
if (isDownloadPollingActive) return;
|
||||||
|
|
||||||
|
console.log('Starting download status polling (1-second interval)');
|
||||||
|
isDownloadPollingActive = true;
|
||||||
|
|
||||||
|
// Initial call
|
||||||
|
updateDownloadQueues();
|
||||||
|
|
||||||
|
// Start 1-second polling (matching GUI's 1000ms timer)
|
||||||
|
downloadStatusInterval = setInterval(updateDownloadQueues, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDownloadPolling() {
|
||||||
|
if (downloadStatusInterval) {
|
||||||
|
clearInterval(downloadStatusInterval);
|
||||||
|
downloadStatusInterval = null;
|
||||||
|
}
|
||||||
|
isDownloadPollingActive = false;
|
||||||
|
console.log('Stopped download status polling');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDownloadQueues() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/downloads/status');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
console.error("Error fetching download status:", data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newActive = {};
|
||||||
|
const newFinished = {};
|
||||||
|
|
||||||
|
// Terminal states matching GUI logic
|
||||||
|
const terminalStates = ['Completed', 'Succeeded', 'Cancelled', 'Canceled', 'Failed', 'Errored'];
|
||||||
|
|
||||||
|
// Process transfers exactly like GUI
|
||||||
|
data.transfers.forEach(item => {
|
||||||
|
const isTerminal = terminalStates.some(state =>
|
||||||
|
item.state && item.state.includes(state)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isTerminal) {
|
||||||
|
newFinished[item.id] = item;
|
||||||
|
} else {
|
||||||
|
newActive[item.id] = item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update global state
|
||||||
|
activeDownloads = newActive;
|
||||||
|
finishedDownloads = newFinished;
|
||||||
|
|
||||||
|
// Render both queues
|
||||||
|
renderQueue('active-queue', activeDownloads, true);
|
||||||
|
renderQueue('finished-queue', finishedDownloads, false);
|
||||||
|
|
||||||
|
// Update tab counts
|
||||||
|
updateTabCounts();
|
||||||
|
|
||||||
|
// Update stats in the side panel
|
||||||
|
updateDownloadStats();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Only log errors occasionally to avoid console spam
|
||||||
|
if (Math.random() < 0.1) {
|
||||||
|
console.error("Failed to update download queues:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderQueue(containerId, downloads, isActiveQueue) {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const downloadIds = Object.keys(downloads);
|
||||||
|
|
||||||
|
if (downloadIds.length === 0) {
|
||||||
|
container.innerHTML = `<div class="download-queue__empty-message">${isActiveQueue ? 'No active downloads.' : 'No finished downloads.'}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
for (const id of downloadIds) {
|
||||||
|
const item = downloads[id];
|
||||||
|
const title = item.filename ? item.filename.split(/[\\/]/).pop() : 'Unknown File';
|
||||||
|
const progress = item.percentComplete || 0;
|
||||||
|
const bytesTransferred = item.bytesTransferred || 0;
|
||||||
|
const totalBytes = item.size || 0;
|
||||||
|
const speed = item.averageSpeed || 0;
|
||||||
|
|
||||||
|
// Format file size
|
||||||
|
const formatSize = (bytes) => {
|
||||||
|
if (!bytes) return 'Unknown size';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
let size = bytes;
|
||||||
|
let unitIndex = 0;
|
||||||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1024;
|
||||||
|
unitIndex++;
|
||||||
|
}
|
||||||
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format speed
|
||||||
|
const formatSpeed = (bytesPerSecond) => {
|
||||||
|
if (!bytesPerSecond || bytesPerSecond <= 0) return '';
|
||||||
|
return `${formatSize(bytesPerSecond)}/s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
let actionButtonHTML = '';
|
||||||
|
if (isActiveQueue) {
|
||||||
|
// Active items get progress bar and cancel button
|
||||||
|
actionButtonHTML = `
|
||||||
|
<div class="download-item__progress-container">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" style="width: ${progress}%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-text">
|
||||||
|
${item.state} - ${progress.toFixed(1)}%
|
||||||
|
${speed > 0 ? `• ${formatSpeed(speed)}` : ''}
|
||||||
|
${totalBytes > 0 ? `• ${formatSize(bytesTransferred)} / ${formatSize(totalBytes)}` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="download-item__cancel-btn" onclick="cancelDownloadItem('${item.id}', '${item.username}')">✕ Cancel</button>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Finished items get status and open button
|
||||||
|
let statusClass = '';
|
||||||
|
if (item.state.includes('Cancelled')) statusClass = 'status--cancelled';
|
||||||
|
else if (item.state.includes('Failed') || item.state.includes('Errored')) statusClass = 'status--failed';
|
||||||
|
else if (item.state.includes('Completed') || item.state.includes('Succeeded')) statusClass = 'status--completed';
|
||||||
|
|
||||||
|
actionButtonHTML = `
|
||||||
|
<div class="download-item__status-container">
|
||||||
|
<span class="download-item__status-text ${statusClass}">${item.state}</span>
|
||||||
|
</div>
|
||||||
|
<button class="download-item__open-btn" title="Cannot open folder from web browser" disabled>📁 Open</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="download-item" data-id="${item.id}">
|
||||||
|
<div class="download-item__header">
|
||||||
|
<div class="download-item__title" title="${title}">${title}</div>
|
||||||
|
<div class="download-item__uploader" title="from ${item.username}">from ${item.username}</div>
|
||||||
|
</div>
|
||||||
|
<div class="download-item__content">
|
||||||
|
${actionButtonHTML}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTabCounts() {
|
||||||
|
const activeCount = Object.keys(activeDownloads).length;
|
||||||
|
const finishedCount = Object.keys(finishedDownloads).length;
|
||||||
|
|
||||||
|
const activeTabBtn = document.querySelector('.tab-btn[data-tab="active-queue"]');
|
||||||
|
const finishedTabBtn = document.querySelector('.tab-btn[data-tab="finished-queue"]');
|
||||||
|
|
||||||
|
if (activeTabBtn) activeTabBtn.textContent = `Download Queue (${activeCount})`;
|
||||||
|
if (finishedTabBtn) finishedTabBtn.textContent = `Finished (${finishedCount})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDownloadStats() {
|
||||||
|
const activeCount = Object.keys(activeDownloads).length;
|
||||||
|
const finishedCount = Object.keys(finishedDownloads).length;
|
||||||
|
|
||||||
|
const activeLabel = document.getElementById('active-downloads-label');
|
||||||
|
const finishedLabel = document.getElementById('finished-downloads-label');
|
||||||
|
|
||||||
|
if (activeLabel) activeLabel.textContent = `• Active Downloads: ${activeCount}`;
|
||||||
|
if (finishedLabel) finishedLabel.textContent = `• Finished Downloads: ${finishedCount}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeDownloadTabs() {
|
||||||
|
const tabButtons = document.querySelectorAll('.tab-btn');
|
||||||
|
tabButtons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => switchDownloadTab(btn));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchDownloadTab(button) {
|
||||||
|
const targetTabId = button.getAttribute('data-tab');
|
||||||
|
|
||||||
|
// Update buttons
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
button.classList.add('active');
|
||||||
|
|
||||||
|
// Update content panes
|
||||||
|
document.querySelectorAll('.download-queue').forEach(queue => queue.classList.remove('active'));
|
||||||
|
const targetQueue = document.getElementById(targetTabId);
|
||||||
|
if (targetQueue) targetQueue.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelDownloadItem(downloadId, username) {
|
||||||
|
if (!confirm('Are you sure you want to cancel this download?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/downloads/cancel', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ download_id: downloadId, username: username })
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showToast('Download cancelled', 'success');
|
||||||
|
} else {
|
||||||
|
showToast(`Failed to cancel: ${result.error}`, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cancelling download:', error);
|
||||||
|
showToast('Error sending cancel request', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearFinishedDownloads() {
|
||||||
|
const finishedCount = Object.keys(finishedDownloads).length;
|
||||||
|
if (finishedCount === 0) {
|
||||||
|
showToast('No finished downloads to clear', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm(`Are you sure you want to clear all ${finishedCount} finished downloads?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/downloads/clear-finished', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showToast('Finished downloads cleared', 'success');
|
||||||
|
} else {
|
||||||
|
showToast(`Failed to clear: ${result.error}`, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing finished downloads:', error);
|
||||||
|
showToast('Error sending clear request', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performDownloadsSearch() {
|
async function performDownloadsSearch() {
|
||||||
|
|
@ -1519,4 +1792,7 @@ window.startDownload = startDownload;
|
||||||
window.downloadTrack = downloadTrack;
|
window.downloadTrack = downloadTrack;
|
||||||
window.downloadAlbum = downloadAlbum;
|
window.downloadAlbum = downloadAlbum;
|
||||||
window.toggleAlbumExpansion = toggleAlbumExpansion;
|
window.toggleAlbumExpansion = toggleAlbumExpansion;
|
||||||
window.downloadAlbumTrack = downloadAlbumTrack;
|
window.downloadAlbumTrack = downloadAlbumTrack;
|
||||||
|
window.switchDownloadTab = switchDownloadTab;
|
||||||
|
window.cancelDownloadItem = cancelDownloadItem;
|
||||||
|
window.clearFinishedDownloads = clearFinishedDownloads;
|
||||||
|
|
@ -2287,4 +2287,215 @@ body {
|
||||||
rgba(30, 215, 96, 0.9),
|
rgba(30, 215, 96, 0.9),
|
||||||
rgba(25, 180, 80, 0.8));
|
rgba(25, 180, 80, 0.8));
|
||||||
border-color: rgba(29, 185, 84, 0.6);
|
border-color: rgba(29, 185, 84, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sophisticated Download Tracking Styles */
|
||||||
|
|
||||||
|
/* Download Queue Items */
|
||||||
|
.download-item {
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
rgba(45, 45, 50, 0.95),
|
||||||
|
rgba(35, 35, 40, 0.98));
|
||||||
|
border: 1px solid rgba(65, 65, 70, 0.5);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin: 6px 0;
|
||||||
|
padding: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-item:hover {
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
rgba(50, 50, 55, 0.98),
|
||||||
|
rgba(40, 40, 45, 1.0));
|
||||||
|
border-color: rgba(29, 185, 84, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-item__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-item__title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
max-width: 200px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-item__uploader {
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(29, 185, 84, 1.0);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-item__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bar Styles */
|
||||||
|
.download-item__progress-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #1ed760, #1db954);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #b3b3b3;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Buttons */
|
||||||
|
.download-item__cancel-btn {
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
rgba(255, 69, 58, 0.9),
|
||||||
|
rgba(255, 45, 85, 0.8));
|
||||||
|
border: 1px solid rgba(255, 69, 58, 0.4);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-item__cancel-btn:hover {
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
rgba(255, 69, 58, 1.0),
|
||||||
|
rgba(255, 45, 85, 0.9));
|
||||||
|
border-color: rgba(255, 69, 58, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-item__status-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-item__status-text {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-item__status-text.status--completed {
|
||||||
|
background: rgba(29, 185, 84, 0.2);
|
||||||
|
color: #1ed760;
|
||||||
|
border: 1px solid rgba(29, 185, 84, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-item__status-text.status--cancelled {
|
||||||
|
background: rgba(255, 159, 10, 0.2);
|
||||||
|
color: #ff9f0a;
|
||||||
|
border: 1px solid rgba(255, 159, 10, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-item__status-text.status--failed {
|
||||||
|
background: rgba(255, 69, 58, 0.2);
|
||||||
|
color: #ff453a;
|
||||||
|
border: 1px solid rgba(255, 69, 58, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-item__open-btn {
|
||||||
|
background: rgba(29, 185, 84, 0.1);
|
||||||
|
color: rgba(29, 185, 84, 0.6);
|
||||||
|
border: 1px solid rgba(29, 185, 84, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab Management */
|
||||||
|
.download-manager__tabs .tab-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-manager__tabs .tab-btn.active {
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
rgba(29, 185, 84, 0.2),
|
||||||
|
rgba(29, 185, 84, 0.1));
|
||||||
|
color: #1ed760;
|
||||||
|
border-color: rgba(29, 185, 84, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-manager__tabs .tab-btn:hover:not(.active) {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Queue Content */
|
||||||
|
.download-queue {
|
||||||
|
display: none;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-queue.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-queue__empty-message {
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar Styling for Download Queue */
|
||||||
|
.download-queue::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-queue::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-queue::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(29, 185, 84, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-queue::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(29, 185, 84, 0.5);
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue