Refactor download queue item rendering and actions
This commit is contained in:
+228
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>`,
|
||||
|
||||
Reference in New Issue
Block a user