Sanjana Vekhande commited on
Commit ·
d94bead
1
Parent(s): 3f0cc99
add download progress bar
Browse files- frontend/index.html +112 -4
frontend/index.html
CHANGED
|
@@ -470,13 +470,61 @@ input[type="range"]::-webkit-slider-thumb:hover {
|
|
| 470 |
transition: all 0.15s;
|
| 471 |
}
|
| 472 |
|
| 473 |
-
.btn-download:hover {
|
| 474 |
background: var(--teal);
|
| 475 |
color: white;
|
| 476 |
transform: translateY(-1px);
|
| 477 |
box-shadow: var(--shadow-md);
|
| 478 |
}
|
| 479 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
/* ── Footer ──────────────────────────── */
|
| 481 |
footer {
|
| 482 |
margin-top: 4rem;
|
|
@@ -636,6 +684,12 @@ footer a:hover { color: var(--ink); }
|
|
| 636 |
<button class="btn-download" id="downloadBtn" onclick="triggerDownload()">
|
| 637 |
⬇ Download Bootstrapped ZIP
|
| 638 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
</div>
|
| 640 |
</div>
|
| 641 |
|
|
@@ -787,19 +841,73 @@ async function startJob() {
|
|
| 787 |
async function triggerDownload() {
|
| 788 |
const jobId = document.getElementById('downloadBtn').dataset.jobId;
|
| 789 |
if (!jobId) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 790 |
try {
|
| 791 |
const res = await fetch(`/api/download/${jobId}`);
|
| 792 |
if (!res.ok) throw new Error('Download failed');
|
| 793 |
-
|
| 794 |
-
const
|
| 795 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 796 |
a.href = url;
|
| 797 |
a.download = 'geneExpr_bootstrapped.zip';
|
| 798 |
document.body.appendChild(a);
|
| 799 |
a.click();
|
| 800 |
document.body.removeChild(a);
|
| 801 |
URL.revokeObjectURL(url);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 802 |
} catch (e) {
|
|
|
|
|
|
|
|
|
|
| 803 |
alert('Download failed: ' + e.message);
|
| 804 |
}
|
| 805 |
}
|
|
|
|
| 470 |
transition: all 0.15s;
|
| 471 |
}
|
| 472 |
|
| 473 |
+
.btn-download:hover:not(:disabled) {
|
| 474 |
background: var(--teal);
|
| 475 |
color: white;
|
| 476 |
transform: translateY(-1px);
|
| 477 |
box-shadow: var(--shadow-md);
|
| 478 |
}
|
| 479 |
|
| 480 |
+
.btn-download:disabled {
|
| 481 |
+
opacity: 0.7;
|
| 482 |
+
cursor: not-allowed;
|
| 483 |
+
transform: none;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
/* Download progress bar */
|
| 487 |
+
.download-progress-wrap {
|
| 488 |
+
margin-top: 0.65rem;
|
| 489 |
+
display: none;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.download-progress-wrap.show { display: block; }
|
| 493 |
+
|
| 494 |
+
.download-progress-track {
|
| 495 |
+
height: 4px;
|
| 496 |
+
background: var(--border);
|
| 497 |
+
border-radius: 2px;
|
| 498 |
+
overflow: hidden;
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
.download-progress-fill {
|
| 502 |
+
height: 100%;
|
| 503 |
+
background: linear-gradient(90deg, var(--teal), #6B8FCF);
|
| 504 |
+
border-radius: 2px;
|
| 505 |
+
width: 0%;
|
| 506 |
+
transition: width 0.3s ease;
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
.download-progress-label {
|
| 510 |
+
font-size: 12px;
|
| 511 |
+
color: var(--ink2);
|
| 512 |
+
margin-top: 0.3rem;
|
| 513 |
+
text-align: center;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
@keyframes indeterminate {
|
| 517 |
+
0% { left: -40%; width: 40%; }
|
| 518 |
+
60% { left: 100%; width: 40%; }
|
| 519 |
+
100% { left: 100%; width: 40%; }
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
.download-progress-fill.indeterminate {
|
| 523 |
+
position: relative;
|
| 524 |
+
width: 40%;
|
| 525 |
+
animation: indeterminate 1.4s ease infinite;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
/* ── Footer ──────────────────────────── */
|
| 529 |
footer {
|
| 530 |
margin-top: 4rem;
|
|
|
|
| 684 |
<button class="btn-download" id="downloadBtn" onclick="triggerDownload()">
|
| 685 |
⬇ Download Bootstrapped ZIP
|
| 686 |
</button>
|
| 687 |
+
<div class="download-progress-wrap" id="dlProgressWrap">
|
| 688 |
+
<div class="download-progress-track">
|
| 689 |
+
<div class="download-progress-fill indeterminate" id="dlProgressFill"></div>
|
| 690 |
+
</div>
|
| 691 |
+
<div class="download-progress-label" id="dlProgressLabel">Preparing ZIP…</div>
|
| 692 |
+
</div>
|
| 693 |
</div>
|
| 694 |
</div>
|
| 695 |
|
|
|
|
| 841 |
async function triggerDownload() {
|
| 842 |
const jobId = document.getElementById('downloadBtn').dataset.jobId;
|
| 843 |
if (!jobId) return;
|
| 844 |
+
|
| 845 |
+
const btn = document.getElementById('downloadBtn');
|
| 846 |
+
const wrap = document.getElementById('dlProgressWrap');
|
| 847 |
+
const fill = document.getElementById('dlProgressFill');
|
| 848 |
+
const label = document.getElementById('dlProgressLabel');
|
| 849 |
+
|
| 850 |
+
btn.disabled = true;
|
| 851 |
+
btn.textContent = 'Preparing download…';
|
| 852 |
+
wrap.classList.add('show');
|
| 853 |
+
fill.classList.add('indeterminate');
|
| 854 |
+
label.textContent = 'Fetching ZIP from server…';
|
| 855 |
+
|
| 856 |
try {
|
| 857 |
const res = await fetch(`/api/download/${jobId}`);
|
| 858 |
if (!res.ok) throw new Error('Download failed');
|
| 859 |
+
|
| 860 |
+
const contentLength = res.headers.get('Content-Length');
|
| 861 |
+
let blob;
|
| 862 |
+
|
| 863 |
+
if (contentLength) {
|
| 864 |
+
// Real progress from Content-Length
|
| 865 |
+
const total = parseInt(contentLength);
|
| 866 |
+
const chunks = [];
|
| 867 |
+
let received = 0;
|
| 868 |
+
const reader = res.body.getReader();
|
| 869 |
+
fill.classList.remove('indeterminate');
|
| 870 |
+
|
| 871 |
+
while (true) {
|
| 872 |
+
const { done, value } = await reader.read();
|
| 873 |
+
if (done) break;
|
| 874 |
+
chunks.push(value);
|
| 875 |
+
received += value.length;
|
| 876 |
+
const pct = Math.min(Math.round(received / total * 100), 99);
|
| 877 |
+
fill.style.width = pct + '%';
|
| 878 |
+
label.textContent = `Downloading… ${pct}%`;
|
| 879 |
+
}
|
| 880 |
+
blob = new Blob(chunks, { type: 'application/zip' });
|
| 881 |
+
} else {
|
| 882 |
+
// No content-length — just show spinner until done
|
| 883 |
+
label.textContent = 'Downloading…';
|
| 884 |
+
blob = await res.blob();
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
fill.classList.remove('indeterminate');
|
| 888 |
+
fill.style.width = '100%';
|
| 889 |
+
label.textContent = 'Done!';
|
| 890 |
+
|
| 891 |
+
const url = URL.createObjectURL(blob);
|
| 892 |
+
const a = document.createElement('a');
|
| 893 |
a.href = url;
|
| 894 |
a.download = 'geneExpr_bootstrapped.zip';
|
| 895 |
document.body.appendChild(a);
|
| 896 |
a.click();
|
| 897 |
document.body.removeChild(a);
|
| 898 |
URL.revokeObjectURL(url);
|
| 899 |
+
|
| 900 |
+
setTimeout(() => {
|
| 901 |
+
btn.disabled = false;
|
| 902 |
+
btn.textContent = '⬇ Download Bootstrapped ZIP';
|
| 903 |
+
wrap.classList.remove('show');
|
| 904 |
+
fill.style.width = '0%';
|
| 905 |
+
}, 2000);
|
| 906 |
+
|
| 907 |
} catch (e) {
|
| 908 |
+
btn.disabled = false;
|
| 909 |
+
btn.textContent = '⬇ Download Bootstrapped ZIP';
|
| 910 |
+
wrap.classList.remove('show');
|
| 911 |
alert('Download failed: ' + e.message);
|
| 912 |
}
|
| 913 |
}
|