173 lines
6.5 KiB
JavaScript
173 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' });
|
|||
|
|
}
|