Polish torrent and usenet download UX
Clarifies album-bundle progress text in the download modal and active downloads panel so release-first downloads read as downloading a release, then matching tracks after staging. Adds waiting-state copy and tooltips for rows blocked on release staging, plus source-specific library history badge styling for Torrent, Usenet, Staging, and Auto-Import.
This commit is contained in:
parent
6c9b43225a
commit
b6b83c8bf8
4 changed files with 124 additions and 15 deletions
|
|
@ -3328,6 +3328,46 @@ function _downloadModalFormatSpeed(bytesPerSecond) {
|
|||
return formatted ? `${formatted}/s` : '';
|
||||
}
|
||||
|
||||
function _downloadModalSourceLabel(source) {
|
||||
const labels = {
|
||||
torrent: 'Torrent',
|
||||
usenet: 'Usenet',
|
||||
soulseek: 'Soulseek',
|
||||
youtube: 'YouTube',
|
||||
tidal: 'Tidal',
|
||||
qobuz: 'Qobuz',
|
||||
hifi: 'HiFi',
|
||||
deezer_dl: 'Deezer',
|
||||
amazon: 'Amazon',
|
||||
lidarr: 'Lidarr',
|
||||
soundcloud: 'SoundCloud'
|
||||
};
|
||||
const key = String(source || '').toLowerCase();
|
||||
return labels[key] || (source ? String(source) : 'Release');
|
||||
}
|
||||
|
||||
function _downloadModalBundleStateLabel(state) {
|
||||
const labels = {
|
||||
searching: 'searching for release',
|
||||
downloading: 'downloading release',
|
||||
staged: 'matching tracks',
|
||||
failed: 'release failed'
|
||||
};
|
||||
const key = String(state || '').toLowerCase();
|
||||
return labels[key] || (state ? String(state).replace(/_/g, ' ') : 'downloading release');
|
||||
}
|
||||
|
||||
function _downloadModalBundleProgressText(bundle) {
|
||||
const percent = _downloadModalBundleProgressPercent(bundle);
|
||||
const source = _downloadModalSourceLabel(bundle && bundle.source);
|
||||
const state = _downloadModalBundleStateLabel(bundle && bundle.state);
|
||||
const release = bundle && bundle.release ? ` - ${bundle.release}` : '';
|
||||
const speed = _downloadModalFormatSpeed(bundle && bundle.speed);
|
||||
const size = _downloadModalFormatBytes(bundle && bundle.size);
|
||||
const detail = speed || size ? ` (${[speed, size].filter(Boolean).join(' of ')})` : '';
|
||||
return `${source} ${state} ${percent}%${release}${detail}`;
|
||||
}
|
||||
|
||||
function processModalStatusUpdate(playlistId, data) {
|
||||
// This function contains ALL the existing polling logic from startModalDownloadPolling
|
||||
// Extracted so it can be called from both individual and batched polling
|
||||
|
|
@ -3384,16 +3424,12 @@ function processModalStatusUpdate(playlistId, data) {
|
|||
|
||||
const bundle = data.album_bundle || {};
|
||||
const percent = _downloadModalBundleProgressPercent(bundle);
|
||||
const source = bundle.source ? `${bundle.source} ` : '';
|
||||
const release = bundle.release ? ` - ${bundle.release}` : '';
|
||||
const speed = _downloadModalFormatSpeed(bundle.speed);
|
||||
const size = _downloadModalFormatBytes(bundle.size);
|
||||
const detail = speed || size ? ` (${[speed, size].filter(Boolean).join(' of ')})` : '';
|
||||
const downloadFill = document.getElementById(`download-progress-fill-${playlistId}`);
|
||||
const downloadText = document.getElementById(`download-progress-text-${playlistId}`);
|
||||
if (downloadFill) downloadFill.style.width = `${percent}%`;
|
||||
if (downloadText) {
|
||||
downloadText.textContent = `${source}album ${bundle.state || 'downloading'} ${percent}%${release}${detail}`;
|
||||
downloadText.textContent = _downloadModalBundleProgressText(bundle);
|
||||
downloadText.title = 'SoulSync downloads one album release first, then matches the selected tracks from the staged files.';
|
||||
}
|
||||
|
||||
const modal = document.getElementById(`download-missing-modal-${playlistId}`);
|
||||
|
|
@ -3401,7 +3437,9 @@ function processModalStatusUpdate(playlistId, data) {
|
|||
modal.querySelectorAll('[id^="download-"]').forEach(statusEl => {
|
||||
if (!statusEl.id.startsWith(`download-${playlistId}-`)) return;
|
||||
if (!statusEl.textContent || statusEl.textContent === '-' || statusEl.textContent.includes('Pending')) {
|
||||
statusEl.textContent = 'Waiting for album bundle';
|
||||
statusEl.textContent = 'Waiting for release';
|
||||
statusEl.classList.add('album-bundle-waiting');
|
||||
statusEl.title = 'The album release is downloading first. Tracks will move to processing once SoulSync can match files from it.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2473,6 +2473,46 @@ function _adlFormatSpeed(bytesPerSecond) {
|
|||
return formatted ? `${formatted}/s` : '';
|
||||
}
|
||||
|
||||
function _adlSourceLabel(source) {
|
||||
const labels = {
|
||||
torrent: 'Torrent',
|
||||
usenet: 'Usenet',
|
||||
soulseek: 'Soulseek',
|
||||
youtube: 'YouTube',
|
||||
tidal: 'Tidal',
|
||||
qobuz: 'Qobuz',
|
||||
hifi: 'HiFi',
|
||||
deezer_dl: 'Deezer',
|
||||
amazon: 'Amazon',
|
||||
lidarr: 'Lidarr',
|
||||
soundcloud: 'SoundCloud'
|
||||
};
|
||||
const key = String(source || '').toLowerCase();
|
||||
return labels[key] || (source ? String(source) : 'Release');
|
||||
}
|
||||
|
||||
function _adlBundleStateLabel(state) {
|
||||
const labels = {
|
||||
searching: 'searching for release',
|
||||
downloading: 'downloading release',
|
||||
staged: 'matching tracks',
|
||||
failed: 'release failed'
|
||||
};
|
||||
const key = String(state || '').toLowerCase();
|
||||
return labels[key] || (state ? String(state).replace(/_/g, ' ') : 'downloading release');
|
||||
}
|
||||
|
||||
function _adlBundleProgressText(bundle) {
|
||||
const pct = _adlBundleProgressPercent(bundle);
|
||||
const source = _adlSourceLabel(bundle && bundle.source);
|
||||
const state = _adlBundleStateLabel(bundle && bundle.state);
|
||||
const release = bundle && bundle.release ? ` - ${bundle.release}` : '';
|
||||
const speed = _adlFormatSpeed(bundle && bundle.speed);
|
||||
const size = _adlFormatBytes(bundle && bundle.size);
|
||||
const detail = speed || size ? ` (${[speed, size].filter(Boolean).join(' of ')})` : '';
|
||||
return `${source} ${state} ${pct}%${release}${detail}`;
|
||||
}
|
||||
|
||||
async function adlClearCompleted() {
|
||||
try {
|
||||
const resp = await fetch('/api/downloads/clear-completed', { method: 'POST' });
|
||||
|
|
@ -2565,12 +2605,7 @@ function _adlRenderBatchPanel() {
|
|||
phaseText = 'Analyzing...';
|
||||
phaseIcon = '<span class="adl-spinner" style="margin-right:4px"></span>';
|
||||
} else if (batch.phase === 'album_downloading') {
|
||||
const source = albumBundle && albumBundle.source ? `${albumBundle.source} ` : '';
|
||||
const release = albumBundle && albumBundle.release ? ` - ${albumBundle.release}` : '';
|
||||
const speed = _adlFormatSpeed(albumBundle && albumBundle.speed);
|
||||
const size = _adlFormatBytes(albumBundle && albumBundle.size);
|
||||
const detail = speed || size ? ` (${[speed, size].filter(Boolean).join(' of ')})` : '';
|
||||
phaseText = `${source}album ${albumBundle && albumBundle.state ? albumBundle.state : 'downloading'} ${bundleProgress}%${release}${detail}`;
|
||||
phaseText = _adlBundleProgressText(albumBundle);
|
||||
phaseIcon = '<span class="adl-spinner" style="margin-right:4px"></span>';
|
||||
} else if (batch.phase === 'downloading') {
|
||||
phaseText = `${batch.completed}/${total} tracks`;
|
||||
|
|
@ -2635,7 +2670,9 @@ function _adlRenderBatchPanel() {
|
|||
</div>`;
|
||||
}).join('');
|
||||
} else {
|
||||
tracksHtml = '<div style="font-size:0.7rem;color:rgba(255,255,255,0.3);padding:4px 0">No tracks loaded</div>';
|
||||
tracksHtml = batch.phase === 'album_downloading'
|
||||
? '<div class="adl-batch-release-note">Downloading one release first. Track matching starts after staging.</div>'
|
||||
: '<div style="font-size:0.7rem;color:rgba(255,255,255,0.3);padding:4px 0">No tracks loaded</div>';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10333,6 +10333,25 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
border: 1px solid rgba(100, 200, 130, 0.2);
|
||||
}
|
||||
|
||||
.library-history-badge.download.source-torrent {
|
||||
color: #5dade2;
|
||||
background: rgba(93, 173, 226, 0.1);
|
||||
border-color: rgba(93, 173, 226, 0.25);
|
||||
}
|
||||
|
||||
.library-history-badge.download.source-usenet {
|
||||
color: #a78bfa;
|
||||
background: rgba(167, 139, 250, 0.1);
|
||||
border-color: rgba(167, 139, 250, 0.25);
|
||||
}
|
||||
|
||||
.library-history-badge.download.source-staging,
|
||||
.library-history-badge.download.source-auto-import {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.library-history-badge.import {
|
||||
color: rgba(120, 160, 230, 0.9);
|
||||
background: rgba(120, 160, 230, 0.1);
|
||||
|
|
@ -19885,6 +19904,11 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
inset 0 1px 2px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.album-bundle-waiting {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Track Table Section */
|
||||
.download-tracks-section {
|
||||
/* Premium glassmorphic card matching other sections */
|
||||
|
|
@ -58357,6 +58381,13 @@ body.reduce-effects *::after {
|
|||
.adl-batch-track-status.failed { color: #ef4444; }
|
||||
.adl-batch-track-status.queued { color: rgba(255, 255, 255, 0.3); }
|
||||
|
||||
.adl-batch-release-note {
|
||||
padding: 6px 0;
|
||||
color: rgba(255, 255, 255, 0.38);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Mini progress bar per track */
|
||||
.adl-batch-track-progress {
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -3449,7 +3449,10 @@ function renderHistoryEntry(entry) {
|
|||
const parts = [];
|
||||
if (entry.download_source) parts.push(entry.download_source);
|
||||
if (entry.quality) parts.push(entry.quality);
|
||||
badge = parts.map(p => `<span class="library-history-badge download">${escapeHtml(p)}</span>`).join('');
|
||||
badge = parts.map(p => {
|
||||
const cls = String(p || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
return `<span class="library-history-badge download source-${escapeHtml(cls)}">${escapeHtml(p)}</span>`;
|
||||
}).join('');
|
||||
} else if (entry.event_type === 'import' && entry.server_source) {
|
||||
const sourceName = { plex: 'Plex', jellyfin: 'Jellyfin', navidrome: 'Navidrome' }[entry.server_source] || entry.server_source;
|
||||
badge = `<span class="library-history-badge import">${escapeHtml(sourceName)}</span>`;
|
||||
|
|
|
|||
Loading…
Reference in a new issue