172 lines
6.5 KiB
JavaScript
172 lines
6.5 KiB
JavaScript
/**
|
||
* progress.js
|
||
* -----------
|
||
* Progress section DOM management: per-file bars, overall bar, and
|
||
* the final results summary.
|
||
*
|
||
* These functions are called by both stream.js (live SSE updates) and
|
||
* session.js (snapshot restore on reconnect / page reload).
|
||
*
|
||
* Exports
|
||
* -------
|
||
* setupProgressSection(files)
|
||
* setOverallProgress(pct)
|
||
* updateFileProgress(idx, pct, statusClass, statusText, detail, detailClass)
|
||
* showStreamLost()
|
||
* hideStreamLost()
|
||
* showResults(finalStatus)
|
||
*/
|
||
|
||
import { state, els, announce } from './state.js';
|
||
import { esc } from './utils.js';
|
||
|
||
// ─── Progress section setup ───────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Render the initial per-file progress items and reset counters.
|
||
* Called when a new compression job starts or when the DOM needs to be
|
||
* rebuilt after a full page reload.
|
||
* @param {Array} files — file objects from the job (name, path, …)
|
||
*/
|
||
export function setupProgressSection(files) {
|
||
els.progTotal.textContent = files.length;
|
||
els.progDone.textContent = '0';
|
||
els.progStatus.textContent = 'Running';
|
||
setOverallProgress(0);
|
||
|
||
let html = '';
|
||
files.forEach((f, idx) => {
|
||
html += `
|
||
<div class="file-progress-item" id="fpi-${idx}" role="listitem"
|
||
aria-label="File ${idx + 1} of ${files.length}: ${esc(f.name)}">
|
||
<div class="fp-header">
|
||
<span class="fp-name">${esc(f.name)}</span>
|
||
<span class="fp-status waiting" id="fps-${idx}"
|
||
aria-live="polite">Waiting</span>
|
||
</div>
|
||
<div class="fp-bar-wrap" aria-label="Progress for ${esc(f.name)}">
|
||
<div class="fp-bar" role="progressbar"
|
||
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"
|
||
id="fpbar-${idx}">
|
||
<div class="fp-bar-fill" id="fpfill-${idx}" style="width:0%"></div>
|
||
</div>
|
||
<span class="fp-pct" id="fppct-${idx}" aria-hidden="true">0%</span>
|
||
</div>
|
||
<div class="fp-detail" id="fpdetail-${idx}"></div>
|
||
</div>`;
|
||
});
|
||
els.fileProgressList.innerHTML = html;
|
||
}
|
||
|
||
// ─── Bar helpers ─────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Update the overall progress bar.
|
||
* @param {number} pct 0–100
|
||
*/
|
||
export function setOverallProgress(pct) {
|
||
const p = Math.min(100, Math.round(pct));
|
||
els.overallBarFill.style.width = `${p}%`;
|
||
els.overallBar.setAttribute('aria-valuenow', p);
|
||
els.overallPct.textContent = `${p}%`;
|
||
}
|
||
|
||
/**
|
||
* Update a single file's progress bar, status badge, and detail text.
|
||
*
|
||
* @param {number} idx — file index (0-based)
|
||
* @param {number} pct — 0–100
|
||
* @param {string} statusClass — 'waiting' | 'running' | 'done' | 'error'
|
||
* @param {string} statusText — visible badge text
|
||
* @param {string} [detail] — optional sub-text (elapsed time, size saved…)
|
||
* @param {string} [detailClass]— optional class applied to the detail element
|
||
*/
|
||
export function updateFileProgress(idx, pct, statusClass, statusText, detail, detailClass) {
|
||
const fill = document.getElementById(`fpfill-${idx}`);
|
||
const bar = document.getElementById(`fpbar-${idx}`);
|
||
const pctEl = document.getElementById(`fppct-${idx}`);
|
||
const status = document.getElementById(`fps-${idx}`);
|
||
const item = document.getElementById(`fpi-${idx}`);
|
||
const det = document.getElementById(`fpdetail-${idx}`);
|
||
if (!fill) return;
|
||
|
||
const p = Math.min(100, Math.round(pct));
|
||
fill.style.width = `${p}%`;
|
||
bar.setAttribute('aria-valuenow', p);
|
||
pctEl.textContent = `${p}%`;
|
||
status.className = `fp-status ${statusClass}`;
|
||
status.textContent = statusText;
|
||
item.className = `file-progress-item ${statusClass}`;
|
||
fill.classList.toggle('active', statusClass === 'running');
|
||
|
||
if (detail !== undefined) {
|
||
det.textContent = detail;
|
||
det.className = `fp-detail ${detailClass || ''}`;
|
||
}
|
||
}
|
||
|
||
// ─── Stream-lost banner ───────────────────────────────────────────────────────
|
||
|
||
/** Show the disconnection warning banner and reveal the Reconnect button. */
|
||
export function showStreamLost() {
|
||
els.streamLostBanner.hidden = false;
|
||
els.reconnectBtn.hidden = false;
|
||
els.progStatus.textContent = 'Disconnected';
|
||
announce('Live progress stream disconnected. Use Reconnect to resume.');
|
||
}
|
||
|
||
/** Hide the disconnection warning banner and Reconnect button. */
|
||
export function hideStreamLost() {
|
||
els.streamLostBanner.hidden = true;
|
||
els.reconnectBtn.hidden = true;
|
||
}
|
||
|
||
// ─── Results summary ─────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Render the Step 4 results card and scroll it into view.
|
||
* @param {'done'|'cancelled'} finalStatus
|
||
*/
|
||
export function showResults(finalStatus) {
|
||
const results = state.compressionResults;
|
||
let html = '';
|
||
|
||
if (finalStatus === 'cancelled') {
|
||
html += `<p style="color:var(--text-muted);margin-bottom:var(--space-md)">
|
||
Compression was cancelled. Completed files are listed below.</p>`;
|
||
}
|
||
|
||
if (!results.length && finalStatus === 'cancelled') {
|
||
html += '<p style="color:var(--text-muted)">No files were completed before cancellation.</p>';
|
||
}
|
||
|
||
results.forEach(r => {
|
||
if (r.status === 'done') {
|
||
html += `
|
||
<div class="result-row">
|
||
<span class="result-icon">✅</span>
|
||
<div class="result-info">
|
||
<div class="result-name">${esc(r.filename)}</div>
|
||
<div class="result-meta">→ ${esc(r.output || '')}</div>
|
||
</div>
|
||
<span class="result-reduction">-${r.reduction_pct}%</span>
|
||
</div>`;
|
||
} else if (r.status === 'error') {
|
||
html += `
|
||
<div class="result-row">
|
||
<span class="result-icon">❌</span>
|
||
<div class="result-info">
|
||
<div class="result-name">${esc(r.filename)}</div>
|
||
<div class="result-meta" style="color:var(--text-danger)">
|
||
${esc(r.message)}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
});
|
||
|
||
if (!html) html = '<p style="color:var(--text-muted)">No results to display.</p>';
|
||
els.resultsContent.innerHTML = html;
|
||
els.sectionResults.hidden = false;
|
||
els.sectionResults.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|