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` : '';
|
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) {
|
function processModalStatusUpdate(playlistId, data) {
|
||||||
// This function contains ALL the existing polling logic from startModalDownloadPolling
|
// This function contains ALL the existing polling logic from startModalDownloadPolling
|
||||||
// Extracted so it can be called from both individual and batched polling
|
// 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 bundle = data.album_bundle || {};
|
||||||
const percent = _downloadModalBundleProgressPercent(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 downloadFill = document.getElementById(`download-progress-fill-${playlistId}`);
|
||||||
const downloadText = document.getElementById(`download-progress-text-${playlistId}`);
|
const downloadText = document.getElementById(`download-progress-text-${playlistId}`);
|
||||||
if (downloadFill) downloadFill.style.width = `${percent}%`;
|
if (downloadFill) downloadFill.style.width = `${percent}%`;
|
||||||
if (downloadText) {
|
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}`);
|
const modal = document.getElementById(`download-missing-modal-${playlistId}`);
|
||||||
|
|
@ -3401,7 +3437,9 @@ function processModalStatusUpdate(playlistId, data) {
|
||||||
modal.querySelectorAll('[id^="download-"]').forEach(statusEl => {
|
modal.querySelectorAll('[id^="download-"]').forEach(statusEl => {
|
||||||
if (!statusEl.id.startsWith(`download-${playlistId}-`)) return;
|
if (!statusEl.id.startsWith(`download-${playlistId}-`)) return;
|
||||||
if (!statusEl.textContent || statusEl.textContent === '-' || statusEl.textContent.includes('Pending')) {
|
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` : '';
|
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() {
|
async function adlClearCompleted() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/downloads/clear-completed', { method: 'POST' });
|
const resp = await fetch('/api/downloads/clear-completed', { method: 'POST' });
|
||||||
|
|
@ -2565,12 +2605,7 @@ function _adlRenderBatchPanel() {
|
||||||
phaseText = 'Analyzing...';
|
phaseText = 'Analyzing...';
|
||||||
phaseIcon = '<span class="adl-spinner" style="margin-right:4px"></span>';
|
phaseIcon = '<span class="adl-spinner" style="margin-right:4px"></span>';
|
||||||
} else if (batch.phase === 'album_downloading') {
|
} else if (batch.phase === 'album_downloading') {
|
||||||
const source = albumBundle && albumBundle.source ? `${albumBundle.source} ` : '';
|
phaseText = _adlBundleProgressText(albumBundle);
|
||||||
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}`;
|
|
||||||
phaseIcon = '<span class="adl-spinner" style="margin-right:4px"></span>';
|
phaseIcon = '<span class="adl-spinner" style="margin-right:4px"></span>';
|
||||||
} else if (batch.phase === 'downloading') {
|
} else if (batch.phase === 'downloading') {
|
||||||
phaseText = `${batch.completed}/${total} tracks`;
|
phaseText = `${batch.completed}/${total} tracks`;
|
||||||
|
|
@ -2635,7 +2670,9 @@ function _adlRenderBatchPanel() {
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
} else {
|
} 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);
|
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 {
|
.library-history-badge.import {
|
||||||
color: rgba(120, 160, 230, 0.9);
|
color: rgba(120, 160, 230, 0.9);
|
||||||
background: rgba(120, 160, 230, 0.1);
|
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);
|
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 */
|
/* Track Table Section */
|
||||||
.download-tracks-section {
|
.download-tracks-section {
|
||||||
/* Premium glassmorphic card matching other sections */
|
/* 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.failed { color: #ef4444; }
|
||||||
.adl-batch-track-status.queued { color: rgba(255, 255, 255, 0.3); }
|
.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 */
|
/* Mini progress bar per track */
|
||||||
.adl-batch-track-progress {
|
.adl-batch-track-progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -3449,7 +3449,10 @@ function renderHistoryEntry(entry) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (entry.download_source) parts.push(entry.download_source);
|
if (entry.download_source) parts.push(entry.download_source);
|
||||||
if (entry.quality) parts.push(entry.quality);
|
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) {
|
} else if (entry.event_type === 'import' && entry.server_source) {
|
||||||
const sourceName = { plex: 'Plex', jellyfin: 'Jellyfin', navidrome: 'Navidrome' }[entry.server_source] || 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>`;
|
badge = `<span class="library-history-badge import">${escapeHtml(sourceName)}</span>`;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue