Updates galore. Improved folder structure, componentized, and notifications upon completion.

This commit is contained in:
Brian McGonagill 2026-03-17 14:01:35 -05:00
parent b48784e2ad
commit 7e0502ca40
33 changed files with 3565 additions and 728 deletions

View file

@ -0,0 +1,172 @@
/**
* 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 0100
*/
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 0100
* @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' });
}