Show a loading spinner on the Export/Preview buttons while exporting
The export dialog's Export and Preview buttons now disable and show a spinner with status text while the corresponding async operation is in flight, so the user knows the (sometimes slow) export is running and the app isn't stuck. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
+34
-19
@@ -413,28 +413,43 @@ function showExportDialog({
|
|||||||
outDir: outDirInput.value.trim() || null,
|
outDir: outDirInput.value.trim() || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cancelBtn = el('button', { onClick: () => { close(); resolve(false); } }, 'Cancel');
|
||||||
|
const previewBtn = el('button', {
|
||||||
|
onClick: async () => {
|
||||||
|
if (typeof onPreview !== 'function') return;
|
||||||
|
setButtonLoading(previewBtn, true, 'Preview…');
|
||||||
|
try {
|
||||||
|
await onPreview(payload()); // keep dialog open so settings can be tweaked
|
||||||
|
} finally {
|
||||||
|
setButtonLoading(previewBtn, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, 'Preview');
|
||||||
|
const exportBtn = el('button.primary', {
|
||||||
|
onClick: async () => {
|
||||||
|
if (typeof onExport !== 'function') return;
|
||||||
|
cancelBtn.disabled = true;
|
||||||
|
previewBtn.disabled = true;
|
||||||
|
setButtonLoading(exportBtn, true, 'Exporting…');
|
||||||
|
try {
|
||||||
|
const ok = await onExport(payload());
|
||||||
|
if (ok !== false) {
|
||||||
|
close();
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cancelBtn.disabled = false;
|
||||||
|
previewBtn.disabled = false;
|
||||||
|
setButtonLoading(exportBtn, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, 'Export');
|
||||||
|
|
||||||
const { close } = openModal({
|
const { close } = openModal({
|
||||||
title: 'Export',
|
title: 'Export',
|
||||||
body,
|
body,
|
||||||
footer: [
|
footer: [cancelBtn, previewBtn, exportBtn],
|
||||||
el('button', { onClick: () => { close(); resolve(false); } }, 'Cancel'),
|
|
||||||
el('button', {
|
|
||||||
onClick: async () => {
|
|
||||||
if (typeof onPreview !== 'function') return;
|
|
||||||
await onPreview(payload()); // keep dialog open so settings can be tweaked
|
|
||||||
},
|
|
||||||
}, 'Preview'),
|
|
||||||
el('button.primary', {
|
|
||||||
onClick: async () => {
|
|
||||||
if (typeof onExport !== 'function') return;
|
|
||||||
const ok = await onExport(payload());
|
|
||||||
if (ok !== false) {
|
|
||||||
close();
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}, 'Export'),
|
|
||||||
],
|
|
||||||
wide: true,
|
wide: true,
|
||||||
onClose: () => resolve(false),
|
onClose: () => resolve(false),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,6 +90,25 @@ button.tool.active {
|
|||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
color: var(--accent-fg);
|
color: var(--accent-fg);
|
||||||
}
|
}
|
||||||
|
button:disabled { opacity: 0.6; cursor: default; }
|
||||||
|
button.loading {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.85;
|
||||||
|
animation: spin 0.7s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
input, select, textarea {
|
input, select, textarea {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
|||||||
@@ -34,6 +34,27 @@ function clearNode(node) {
|
|||||||
while (node.firstChild) node.removeChild(node.firstChild);
|
while (node.firstChild) node.removeChild(node.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle a button into/out of a busy state: disables it and swaps its label
|
||||||
|
* for a spinner + `label` (e.g. "Exporting…"), restoring the original label
|
||||||
|
* afterwards. Used for actions that block on a slow main-process call.
|
||||||
|
*/
|
||||||
|
function setButtonLoading(btn, loading, label) {
|
||||||
|
if (loading) {
|
||||||
|
if (btn.dataset.origLabel === undefined) btn.dataset.origLabel = btn.textContent;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.classList.add('loading');
|
||||||
|
clearNode(btn);
|
||||||
|
btn.append(el('span.spinner'), label || btn.dataset.origLabel);
|
||||||
|
} else {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.classList.remove('loading');
|
||||||
|
clearNode(btn);
|
||||||
|
btn.append(btn.dataset.origLabel ?? '');
|
||||||
|
delete btn.dataset.origLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function debounce(fn, ms) {
|
function debounce(fn, ms) {
|
||||||
let t = null;
|
let t = null;
|
||||||
const wrapped = (...args) => {
|
const wrapped = (...args) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user