From c28184aca37cc03831180ad890dd4ccecff7bf89 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Mon, 3 Aug 2026 15:01:07 +0200 Subject: [PATCH] Refactor download queue item rendering and actions --- web/css/resolver-shell.css | 229 +++++++++++++++++++++++++- web/resolver/actions/queue_methods.js | 114 +++++++++---- web/utils/icon_utils.js | 3 + 3 files changed, 317 insertions(+), 29 deletions(-) diff --git a/web/css/resolver-shell.css b/web/css/resolver-shell.css index acb2829..d3f2203 100644 --- a/web/css/resolver-shell.css +++ b/web/css/resolver-shell.css @@ -671,6 +671,233 @@ margin-top: 0; } +.mr-queue-item.mr-download-queue-item { + position: relative; + gap: var(--mr-spacing-10); + margin: var(--mr-spacing-10); + padding: var(--mr-spacing-15) var(--mr-spacing-16) var(--mr-spacing-12); + background: var(--mr-card-bg); + border: 1px solid var(--mr-border-strong); + border-radius: var(--mr-radius-2xl); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22); +} + +.mr-queue-item.mr-download-queue-item:last-child { + border-bottom: 1px solid var(--mr-border-strong); +} + +.mr-queue-item.mr-download-queue-item:hover { + background: var(--mr-card-bg-alt); + border-color: rgba(114, 211, 138, 0.28); +} + +.mr-download-queue-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: var(--mr-spacing-12); + min-width: 0; +} + +.mr-download-queue-heading { + display: flex; + flex-direction: column; + min-width: 0; + gap: var(--mr-spacing-3); +} + +.mr-download-queue-filename { + min-width: 0; + overflow: hidden; + color: var(--mr-text-soft); + font-size: var(--mr-font-xl); + font-weight: 650; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mr-download-queue-status { + flex: 0 1 auto; + align-self: flex-start; + max-width: 100%; + padding: 0; + overflow: hidden; + color: var(--mr-text-dim); + font-size: var(--mr-font-xs); + font-weight: 700; + line-height: 1.2; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; + background: transparent; + border: 0; + border-radius: 0; +} + +#model-resolver-modal .mr-download-queue-actions { + display: inline-flex; + align-items: center; + gap: 3px; + padding: 3px 5px; + background: var(--mr-surface-glass-02); + border: 1px solid var(--mr-border-strong); + border-radius: var(--mr-missing-chip-radius); +} + +#model-resolver-modal .mr-download-queue-action { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + cursor: pointer; + color: var(--mr-text-muted); + background: transparent; + border: 0; + border-radius: var(--mr-radius-lg); + transition: color var(--mr-transition-normal), background var(--mr-transition-normal); +} + +#model-resolver-modal .mr-download-queue-action:hover { + color: var(--mr-text); + background: var(--mr-surface-glass-05); +} + +#model-resolver-modal .mr-download-queue-action:disabled { + cursor: not-allowed; + opacity: 0.42; +} + +#model-resolver-modal .mr-download-queue-action:focus-visible { + outline: none; + box-shadow: var(--mr-shadow-focus-ring); +} + +.mr-download-queue-action-icon { + width: 20px; + height: 20px; +} + +#model-resolver-modal .mr-download-queue-pause, +#model-resolver-modal .mr-download-queue-resume { + color: var(--mr-warning); +} + +#model-resolver-modal .mr-download-queue-pause:hover, +#model-resolver-modal .mr-download-queue-resume:hover { + color: var(--mr-warning); +} + +#model-resolver-modal .mr-download-queue-cancel { + color: var(--mr-color-danger-bright); +} + +#model-resolver-modal .mr-download-queue-cancel:hover { + color: var(--mr-color-danger-bright); +} + +.mr-download-queue-progress { + gap: var(--mr-spacing-8); + margin-top: var(--mr-spacing-3); +} + +.mr-download-queue-progress .mr-progress-bar { + height: 8px; + background: var(--mr-panel-bg-strong); + border-radius: var(--mr-missing-chip-radius); +} + +.mr-download-queue-progress .mr-progress-fill { + background: var(--mr-accent); + border-radius: inherit; + box-shadow: 0 0 10px rgba(114, 211, 138, 0.25); +} + +.mr-download-queue-stats { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--mr-spacing-10); + min-width: 0; + color: var(--mr-text-muted); + font-size: var(--mr-font-base); + line-height: 1.3; +} + +.mr-download-queue-size { + min-width: 0; + overflow: hidden; + font-weight: 650; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mr-download-queue-stat-group { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: var(--mr-spacing-10); + flex: 0 0 auto; + white-space: nowrap; +} + +.mr-download-queue-stat { + display: inline-flex; + align-items: center; + gap: var(--mr-spacing-4); +} + +.mr-download-queue-stat-icon { + width: 15px; + height: 15px; + color: var(--mr-text-dim); +} + +.mr-download-queue-stat-muted { + color: var(--mr-text-dim); + font-size: var(--mr-font-sm); + text-transform: uppercase; +} + +.mr-download-queue-details { + display: flex; + align-items: center; + gap: var(--mr-spacing-4); + min-width: 0; + overflow: hidden; + color: var(--mr-text-dim); + font-size: var(--mr-font-sm); + line-height: 1.3; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mr-download-queue-details > span:first-child { + flex: 0 0 auto; + color: var(--mr-text-muted); + font-weight: 650; +} + +.mr-download-queue-details > span:last-child { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (max-width: 420px) { + .mr-download-queue-header { + grid-template-columns: 1fr; + align-items: stretch; + } + + #model-resolver-modal .mr-download-queue-actions { + justify-self: end; + } +} + .model-resolver-selected { display: none; } @@ -843,4 +1070,4 @@ opacity: 1; transform: translateX(-50%) translateY(0); } -} \ No newline at end of file +} diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index 6ebbc0f..23ca434 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -1011,6 +1011,7 @@ export const queueMethods = { }; const getNodeKey = (node) => { if (node?.nodeType !== 1) return ''; + if (!node.classList?.contains('mr-download-queue-item')) return ''; return node.getAttribute?.('data-download-id') || ''; }; const canPatchNode = (current, next) => { @@ -1092,6 +1093,18 @@ export const queueMethods = { button.addEventListener('click', run); }; + const getDownloadContext = (button) => { + const downloadId = button?.dataset?.downloadId || ''; + const info = this.activeDownloads?.[downloadId]; + if (!downloadId || !info) return null; + + const progress = typeof this.applyPendingDownloadStatus === 'function' + ? this.applyPendingDownloadStatus(info, info.lastProgress || {}) + : (info.lastProgress || {}); + const workflowLabel = this.getDownloadWorkflowLabel?.(info) || 'Unknown workflow'; + return this.getDownloadQueueContext?.(progress, info, workflowLabel, downloadId) || null; + }; + this.queueList.querySelectorAll('.mr-downloads-subtab').forEach(button => { button.addEventListener('click', () => { this.setQueueDownloadsTab(button.dataset.downloadsTab || 'active'); @@ -1119,6 +1132,32 @@ export const queueMethods = { }); }); + this.queueList.querySelectorAll('.mr-download-queue-open-folder').forEach(button => { + bindInstantAction(button, () => { + const context = getDownloadContext(button); + if (context) this.openContainingFolder?.(context); + }); + }); + + this.queueList.querySelectorAll('.mr-download-queue-switch-workflow').forEach(button => { + bindInstantAction(button, () => { + const context = getDownloadContext(button); + if (context) this.switchToDownloadWorkflow?.(context); + }); + }); + + this.queueList.querySelectorAll('.mr-download-queue-more').forEach(button => { + if (button._hasListener) return; + button._hasListener = true; + button.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + if (button.disabled) return; + const item = button.closest('.mr-download-queue-item'); + if (item) window.MLOpenContextMenu?.(event, item); + }); + }); + const clearHistoryButton = this.queueList.querySelector('.mr-download-history-clear'); if (clearHistoryButton) { clearHistoryButton.addEventListener('click', () => this.clearDownloadHistory()); @@ -1149,7 +1188,6 @@ export const queueMethods = { percent, downloadedText: downloaded, totalText: total, - progressMeta, percentLabel, logicalDownloadedText: logicalDownloaded, logicalTotalText: logicalTotal, @@ -1171,47 +1209,67 @@ export const queueMethods = { const sizeText = presentation.isFinalizing && logicalTotal ? `Finalizing file: ${logicalDownloaded} / ${logicalTotal}` : (presentation.totalSize ? `${downloaded} / ${total}` : downloaded); + const speed = Number(progress.speed) || 0; + const speedText = speed > 0 ? `${this.formatBytes(speed)}/s` : ''; + const etaText = this.getDownloadEtaText?.(progress)?.replace(/^ETA\s+/i, 'Remaining ') || ''; const targetPath = progress.directory || info.downloadDirectory || info.downloadPath || ''; - const targetLabel = targetPath ? targetPath.split(/[/\\]/).filter(Boolean).pop() || targetPath : ''; const workflowLabel = this.getDownloadWorkflowLabel?.(info) || 'Unknown workflow'; const contextModel = this.getDownloadQueueContext?.(progress, info, workflowLabel, downloadId); const hasFolderAction = Boolean(contextModel?.folder_path || contextModel?.download_directory || contextModel?.directory || contextModel?.path || contextModel?.resolved_path); - const contextTooltip = hasFolderAction - ? 'Right-click for workflow and download folder actions' - : 'Right-click to switch to workflow'; + const hasWorkflowAction = Boolean(contextModel && this.canSwitchToDownloadWorkflow?.(contextModel)); const contextData = contextModel - ? this.getContextMenuAttrs(contextModel, contextTooltip) + ? this.getContextMenuAttrs(contextModel) : ''; + const safeDownloadId = this.escapeHtml(downloadId); + const nodeText = `${nodeLabel} #${info.missing?.node_id ?? ''}`; + const detailParts = [category, nodeText].filter(Boolean); + const renderIcon = (iconName, className) => ( + typeof getSvgIcon === 'function' + ? getSvgIcon(iconName, 'currentColor', className) + : '' + ); + const actionButton = (className, label, iconName, disabled = false) => ( + `` + ); html += `
`; - html += `
`; - html += `${this.escapeHtml(filename)}`; + html += `
`; + html += `
`; + html += `${this.escapeHtml(filename)}`; html += `${this.escapeHtml(statusLabel)}`; html += `
`; - html += `
Model${this.escapeHtml(nodeLabel)} #${this.escapeHtml(String(info.missing?.node_id ?? ''))}
`; - html += `
Workflow${this.escapeHtml(workflowLabel)}
`; - if (category) { - html += `
Type${this.escapeHtml(category)}
`; - } - if (backend) { - html += `
Backend${this.escapeHtml(backend)}
`; - } - if (targetLabel) { - html += `
Folder${this.escapeHtml(targetLabel)}
`; - } + html += `
`; + html += actionButton( + isPaused ? 'mr-download-queue-resume' : 'mr-download-queue-pause', + isPaused ? 'Resume download' : 'Pause download', + isPaused ? 'play' : 'pause', + !isAria2 + ); + html += actionButton('mr-download-queue-cancel', 'Cancel download', 'x'); + html += actionButton('mr-download-queue-open-folder', 'Open download folder', 'folderOpen', !hasFolderAction); + html += actionButton('mr-download-queue-switch-workflow', 'Switch to workflow', 'internalLink', !hasWorkflowAction); + html += actionButton('mr-download-queue-more', 'More download actions', 'info', !contextModel); + html += `
`; + html += `
`; html += `
`; - html += `
`; - html += `
${this.escapeHtml(sizeText)}${this.escapeHtml(progressMeta)}
`; - html += `
`; - html += `
`; - if (isAria2 && isPaused) { - html += ``; - } else if (isAria2 && status === 'downloading') { - html += ``; + html += `
`; + html += `
`; + html += `${this.escapeHtml(sizeText)}`; + html += ``; + if (speedText) { + html += `${renderIcon('download', 'mr-download-queue-stat-icon')}${this.escapeHtml(speedText)}`; } - html += ``; + if (etaText) { + html += `${this.escapeHtml(etaText)}`; + } + if (!speedText && !etaText) { + html += `${this.escapeHtml(statusLabel)}`; + } + html += ``; html += `
`; html += `
`; + html += `
Workflow${this.escapeHtml([workflowLabel, ...detailParts].filter(Boolean).join(' · '))}
`; + html += `
`; } html += '
'; return html; diff --git a/web/utils/icon_utils.js b/web/utils/icon_utils.js index ffbac67..6a3b87a 100644 --- a/web/utils/icon_utils.js +++ b/web/utils/icon_utils.js @@ -22,6 +22,9 @@ const SVG_ICONS = { refreshCw: ``, locate: ``, download: ``, + pause: ``, + play: ``, + info: ``, cloudDownload: ``, skull: ``, shield: ``,