Refactor download queue item rendering and actions

This commit is contained in:
Dariusz L
2026-08-03 15:01:07 +02:00
parent 7cba340de1
commit c28184aca3
3 changed files with 317 additions and 29 deletions
+228 -1
View File
@@ -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);
}
}
}
+86 -28
View File
@@ -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) => (
`<button type="button" class="mr-download-queue-action ${className}" data-download-id="${safeDownloadId}" data-tooltip="${label}" aria-label="${label}"${disabled ? ' disabled' : ''}>${renderIcon(iconName, 'mr-download-queue-action-icon')}</button>`
);
html += `<div class="mr-queue-item mr-download-queue-item" data-download-id="${this.escapeHtml(downloadId)}"${contextData}>`;
html += `<div class="mr-queue-item-title mr-download-queue-title">`;
html += `<span data-tooltip="${this.escapeHtml(filename)}">${this.escapeHtml(filename)}</span>`;
html += `<div class="mr-download-queue-header">`;
html += `<div class="mr-download-queue-heading">`;
html += `<span class="mr-download-queue-filename" data-tooltip="${this.escapeHtml(filename)}">${this.escapeHtml(filename)}</span>`;
html += `<span class="mr-download-queue-status">${this.escapeHtml(statusLabel)}</span>`;
html += `</div>`;
html += `<div class="mr-queue-item-meta"><span>Model</span><code>${this.escapeHtml(nodeLabel)} #${this.escapeHtml(String(info.missing?.node_id ?? ''))}</code></div>`;
html += `<div class="mr-queue-item-meta"><span>Workflow</span><code data-tooltip="${this.escapeHtml(workflowLabel)}">${this.escapeHtml(workflowLabel)}</code></div>`;
if (category) {
html += `<div class="mr-queue-item-meta"><span>Type</span><code>${this.escapeHtml(category)}</code></div>`;
}
if (backend) {
html += `<div class="mr-queue-item-meta"><span>Backend</span><code>${this.escapeHtml(backend)}</code></div>`;
}
if (targetLabel) {
html += `<div class="mr-queue-item-meta"><span>Folder</span><code data-tooltip="${this.escapeHtml(targetPath)}">${this.escapeHtml(targetLabel)}</code></div>`;
}
html += `<div class="mr-download-queue-actions" role="group" aria-label="Download actions">`;
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 += `</div>`;
html += `</div>`;
html += `<div class="mr-download-queue-progress">`;
html += `<div class="mr-progress-bar"><div class="mr-progress-fill" style="width: ${percent}%;"></div></div>`;
html += `<div class="mr-progress-text"><span>${this.escapeHtml(sizeText)}</span><span>${this.escapeHtml(progressMeta)}</span></div>`;
html += `</div>`;
html += `<div class="mr-queue-item-actions">`;
if (isAria2 && isPaused) {
html += `<button type="button" class="mr-btn mr-btn-primary mr-btn-sm mr-download-queue-resume" data-download-id="${this.escapeHtml(downloadId)}">Resume</button>`;
} else if (isAria2 && status === 'downloading') {
html += `<button type="button" class="mr-btn mr-btn-secondary mr-btn-sm mr-download-queue-pause" data-download-id="${this.escapeHtml(downloadId)}">Pause</button>`;
html += `<div class="mr-progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${percent}" aria-label="Download progress"><div class="mr-progress-fill" style="width: ${percent}%;"></div></div>`;
html += `<div class="mr-download-queue-stats">`;
html += `<span class="mr-download-queue-size">${this.escapeHtml(sizeText)}</span>`;
html += `<span class="mr-download-queue-stat-group">`;
if (speedText) {
html += `<span class="mr-download-queue-stat">${renderIcon('download', 'mr-download-queue-stat-icon')}<span>${this.escapeHtml(speedText)}</span></span>`;
}
html += `<button type="button" class="mr-btn mr-btn-danger mr-btn-sm mr-download-queue-cancel" data-download-id="${this.escapeHtml(downloadId)}">Cancel</button>`;
if (etaText) {
html += `<span class="mr-download-queue-stat"><span>${this.escapeHtml(etaText)}</span></span>`;
}
if (!speedText && !etaText) {
html += `<span class="mr-download-queue-stat mr-download-queue-stat-muted">${this.escapeHtml(statusLabel)}</span>`;
}
html += `</span>`;
html += `</div>`;
html += `</div>`;
html += `<div class="mr-download-queue-details" data-tooltip="${this.escapeHtml([targetPath, backend].filter(Boolean).join(' · '))}"><span>Workflow</span><span>${this.escapeHtml([workflowLabel, ...detailParts].filter(Boolean).join(' · '))}</span></div>`;
html += `</div>`;
}
html += '</div>';
return html;
+3
View File
@@ -22,6 +22,9 @@ const SVG_ICONS = {
refreshCw: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36"></path><path d="M21 3v6h-6"></path></svg>`,
locate: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12h3"></path><path d="M19 12h3"></path><path d="M12 2v3"></path><path d="M12 19v3"></path><circle cx="12" cy="12" r="7"></circle><circle cx="12" cy="12" r="2.5"></circle></svg>`,
download: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg>`,
pause: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6.5" y="4.5" width="4" height="15" rx="1"></rect><rect x="13.5" y="4.5" width="4" height="15" rx="1"></rect></svg>`,
play: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 5 11 7-11 7V5Z"></path></svg>`,
info: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M12 11v5"></path><path d="M12 8h.01"></path></svg>`,
cloudDownload: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 13v8l-4-4"></path><path d="m12 21 4-4"></path><path d="M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284"></path></svg>`,
skull: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12.5 17-.5-1-.5 1h1z"></path><path d="M15 22a1 1 0 0 0 1-1v-1a2 2 0 0 0 1.56-3.25 8 8 0 1 0-11.12 0A2 2 0 0 0 8 20v1a1 1 0 0 0 1 1z"></path><circle cx="15" cy="12" r="1"></circle><circle cx="9" cy="12" r="1"></circle></svg>`,
shield: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"></path></svg>`,