video_press/static/js/modules/progress.js

172 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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' });
}