304 lines
13 KiB
JavaScript
304 lines
13 KiB
JavaScript
/* Batch upload handling — multi-file selection, upload, per-file status tracking */
|
|
|
|
let batchFiles = [];
|
|
let currentBatchId = null;
|
|
let batchPollInterval = null;
|
|
|
|
function switchUploadMode(mode) {
|
|
const tabSingle = document.getElementById('tabSingle');
|
|
const tabBatch = document.getElementById('tabBatch');
|
|
const singleArea = document.getElementById('singleUploadArea');
|
|
const batchArea = document.getElementById('batchUploadArea');
|
|
|
|
if (mode === 'batch') {
|
|
tabSingle.classList.remove('active');
|
|
tabSingle.setAttribute('aria-selected', 'false');
|
|
tabBatch.classList.add('active');
|
|
tabBatch.setAttribute('aria-selected', 'true');
|
|
singleArea.style.display = 'none';
|
|
batchArea.style.display = 'block';
|
|
batchArea.setAttribute('tabindex', '0'); singleArea.setAttribute('tabindex', '-1');
|
|
} else {
|
|
tabBatch.classList.remove('active');
|
|
tabBatch.setAttribute('aria-selected', 'false');
|
|
tabSingle.classList.add('active');
|
|
tabSingle.setAttribute('aria-selected', 'true');
|
|
batchArea.style.display = 'none';
|
|
singleArea.style.display = 'block';
|
|
singleArea.setAttribute('tabindex', '0'); batchArea.setAttribute('tabindex', '-1');
|
|
}
|
|
}
|
|
|
|
function initBatchUpload() {
|
|
const batchDrop = document.getElementById('batchDropArea');
|
|
const batchInput = document.getElementById('batchFileInput');
|
|
if (!batchDrop || !batchInput) return;
|
|
|
|
batchDrop.addEventListener('click', () => batchInput.click());
|
|
batchDrop.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); batchInput.click(); }
|
|
});
|
|
|
|
batchDrop.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
batchDrop.classList.add('dragover');
|
|
});
|
|
|
|
batchDrop.addEventListener('dragleave', () => {
|
|
batchDrop.classList.remove('dragover');
|
|
});
|
|
|
|
batchDrop.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
batchDrop.classList.remove('dragover');
|
|
addBatchFiles(e.dataTransfer.files);
|
|
});
|
|
|
|
batchInput.addEventListener('change', (e) => {
|
|
addBatchFiles(e.target.files);
|
|
});
|
|
}
|
|
|
|
function addBatchFiles(fileList) {
|
|
for (let i = 0; i < fileList.length; i++) {
|
|
const file = fileList[i];
|
|
if (!file.name.toLowerCase().endsWith('.pdf')) continue;
|
|
if (file.size > 50 * 1024 * 1024) continue;
|
|
if (batchFiles.length >= 10) break;
|
|
// Avoid duplicates
|
|
if (batchFiles.some(f => f.name === file.name && f.size === file.size)) continue;
|
|
batchFiles.push(file);
|
|
}
|
|
renderBatchFileList();
|
|
}
|
|
|
|
function renderBatchFileList() {
|
|
const listEl = document.getElementById('batchFileList');
|
|
const actionsEl = document.getElementById('batchActions');
|
|
|
|
if (batchFiles.length === 0) {
|
|
listEl.style.display = 'none';
|
|
actionsEl.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
listEl.style.display = 'block';
|
|
actionsEl.style.display = 'flex';
|
|
|
|
let html = '<div style="font-weight:600;margin-bottom:10px;">' + batchFiles.length + ' file(s) selected:</div>';
|
|
batchFiles.forEach((file, idx) => {
|
|
const sizeMB = (file.size / 1024 / 1024).toFixed(2);
|
|
html += '<div class="batch-file-item" style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--surface-alt);border-radius:6px;margin-bottom:6px;">';
|
|
html += '<span style="font-size:14px;">' + escapeHtml(file.name) + ' <span style="color:var(--text-light);font-size:12px;">(' + sizeMB + ' MB)</span></span>';
|
|
html += '<button onclick="removeBatchFile(' + idx + ')" style="background:none;border:none;color:var(--error);cursor:pointer;font-size:16px;padding:4px 8px;" aria-label="Remove ' + escapeHtml(file.name) + '">✕</button>';
|
|
html += '</div>';
|
|
});
|
|
listEl.innerHTML = html;
|
|
}
|
|
|
|
function removeBatchFile(index) {
|
|
batchFiles.splice(index, 1);
|
|
renderBatchFileList();
|
|
}
|
|
|
|
function clearBatchFiles() {
|
|
batchFiles = [];
|
|
document.getElementById('batchFileInput').value = '';
|
|
renderBatchFileList();
|
|
document.getElementById('batchProgress').style.display = 'none';
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async function startBatchUpload() {
|
|
if (batchFiles.length === 0) return;
|
|
|
|
const btn = document.getElementById('batchUploadBtn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Uploading...';
|
|
|
|
const progressEl = document.getElementById('batchProgress');
|
|
progressEl.style.display = 'block';
|
|
progressEl.innerHTML = '<div style="padding:10px;background:var(--surface-alt);border-radius:6px;">Uploading ' + batchFiles.length + ' files...</div>';
|
|
|
|
const quickMode = document.getElementById('quickMode').checked;
|
|
|
|
try {
|
|
const result = await uploadBatch(batchFiles);
|
|
|
|
if (result.success) {
|
|
currentBatchId = result.data.batch_id;
|
|
const uploaded = result.data.uploaded || [];
|
|
const errors = result.data.errors || [];
|
|
|
|
let html = '<div style="margin-bottom:15px;">';
|
|
html += '<div style="font-weight:600;margin-bottom:10px;">Batch: ' + currentBatchId + '</div>';
|
|
|
|
if (uploaded.length > 0) {
|
|
html += '<div style="color:var(--success);margin-bottom:5px;">' + uploaded.length + ' file(s) uploaded successfully</div>';
|
|
}
|
|
if (errors.length > 0) {
|
|
html += '<div style="color:var(--error);margin-bottom:5px;">' + errors.length + ' file(s) failed:</div>';
|
|
errors.forEach(e => {
|
|
html += '<div style="font-size:13px;color:var(--error);padding-left:10px;">' + escapeHtml(e.filename) + ': ' + escapeHtml(e.error) + '</div>';
|
|
});
|
|
}
|
|
html += '</div>';
|
|
|
|
// Per-file status rows
|
|
html += '<div id="batchStatusList">';
|
|
uploaded.forEach(f => {
|
|
html += '<div class="batch-status-row" id="batch-row-' + f.job_id + '" style="display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--surface);border:1px solid var(--border);border-radius:6px;margin-bottom:6px;">';
|
|
html += '<div><span style="font-weight:600;">' + escapeHtml(f.filename) + '</span></div>';
|
|
html += '<div style="display:flex;align-items:center;gap:10px;">';
|
|
html += '<span class="batch-file-status" id="batch-status-' + f.job_id + '" style="font-size:13px;color:var(--text-light);">Queued</span>';
|
|
html += '<span class="batch-file-score" id="batch-score-' + f.job_id + '"></span>';
|
|
html += '<a class="batch-file-link" id="batch-link-' + f.job_id + '" style="display:none;font-size:13px;" href="#">View</a>';
|
|
html += '</div></div>';
|
|
});
|
|
html += '</div>';
|
|
|
|
// Overall progress bar
|
|
html += '<div style="margin-top:15px;">';
|
|
html += '<div style="display:flex;justify-content:space-between;margin-bottom:5px;font-size:13px;"><span id="batchOverallText">Processing...</span><span id="batchOverallPct">0%</span></div>';
|
|
html += '<div class="progress-bar"><div class="progress-fill" id="batchOverallFill" style="width:0%"></div></div>';
|
|
html += '</div>';
|
|
|
|
progressEl.innerHTML = html;
|
|
|
|
// Start each check
|
|
for (const f of uploaded) {
|
|
startCheck(f.job_id, quickMode).catch(() => {});
|
|
}
|
|
|
|
// Poll batch status
|
|
pollBatchStatus(uploaded.map(f => f.job_id));
|
|
} else {
|
|
progressEl.innerHTML = '<div style="padding:15px;background:var(--error-bg);border-radius:6px;color:var(--error);">Batch upload failed: ' + escapeHtml(result.error) + '</div>';
|
|
}
|
|
} catch (error) {
|
|
progressEl.innerHTML = '<div style="padding:15px;background:var(--error-bg);border-radius:6px;color:var(--error);">Error: ' + escapeHtml(error.message) + '</div>';
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = 'Upload & Check All';
|
|
}
|
|
|
|
function pollBatchStatus(jobIds) {
|
|
const total = jobIds.length;
|
|
let completedSet = new Set();
|
|
|
|
batchPollInterval = setInterval(async () => {
|
|
for (const jobId of jobIds) {
|
|
if (completedSet.has(jobId)) continue;
|
|
|
|
try {
|
|
const result = await checkStatus(jobId);
|
|
if (!result.success) continue;
|
|
|
|
const data = result.data;
|
|
const statusEl = document.getElementById('batch-status-' + jobId);
|
|
const scoreEl = document.getElementById('batch-score-' + jobId);
|
|
const linkEl = document.getElementById('batch-link-' + jobId);
|
|
const rowEl = document.getElementById('batch-row-' + jobId);
|
|
|
|
if (!statusEl) continue;
|
|
|
|
if (data.status === 'completed') {
|
|
completedSet.add(jobId);
|
|
statusEl.textContent = 'Completed';
|
|
statusEl.style.color = 'var(--success)';
|
|
if (rowEl) rowEl.style.borderColor = 'var(--success)';
|
|
|
|
// Fetch score
|
|
try {
|
|
const res = await getResult(jobId);
|
|
if (res.success && res.data.accessibility_score !== undefined) {
|
|
const score = res.data.accessibility_score;
|
|
let color = 'var(--success)';
|
|
if (score < 50) color = 'var(--error)';
|
|
else if (score < 80) color = 'var(--warning)';
|
|
scoreEl.innerHTML = '<span style="font-weight:700;color:' + color + ';">' + score + '/100</span>';
|
|
}
|
|
} catch (_) {}
|
|
|
|
linkEl.style.display = 'inline';
|
|
linkEl.href = '#';
|
|
linkEl.onclick = (e) => { e.preventDefault(); viewBatchResult(jobId); };
|
|
} else if (data.status === 'failed' || data.status === 'error') {
|
|
completedSet.add(jobId);
|
|
statusEl.textContent = 'Failed';
|
|
statusEl.style.color = 'var(--error)';
|
|
if (rowEl) rowEl.style.borderColor = 'var(--error)';
|
|
} else if (data.status === 'processing') {
|
|
const pct = data.progress || 0;
|
|
statusEl.textContent = 'Processing' + (pct > 0 ? ' (' + pct + '%)' : '...');
|
|
statusEl.style.color = 'var(--info)';
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
// Update overall progress
|
|
const done = completedSet.size;
|
|
const pct = Math.round((done / total) * 100);
|
|
const fillEl = document.getElementById('batchOverallFill');
|
|
const pctEl = document.getElementById('batchOverallPct');
|
|
const txtEl = document.getElementById('batchOverallText');
|
|
if (fillEl) fillEl.style.width = pct + '%';
|
|
if (pctEl) pctEl.textContent = pct + '%';
|
|
if (txtEl) txtEl.textContent = done + ' of ' + total + ' complete';
|
|
|
|
if (done >= total) {
|
|
clearInterval(batchPollInterval);
|
|
batchPollInterval = null;
|
|
if (txtEl) txtEl.textContent = 'All ' + total + ' files processed';
|
|
}
|
|
}, 3000);
|
|
}
|
|
|
|
async function viewBatchResult(jobId) {
|
|
try {
|
|
const result = await getResult(jobId);
|
|
if (result.success) {
|
|
currentJobId = jobId;
|
|
document.getElementById('uploadSection').style.display = 'none';
|
|
displayResults(result.data);
|
|
}
|
|
} catch (error) {
|
|
alert('Failed to load result: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function exportReport(format) {
|
|
if (!currentJobId) return;
|
|
|
|
const hasAdjustments =
|
|
(typeof overriddenChecks !== 'undefined' && overriddenChecks.size > 0) ||
|
|
(typeof dismissedIndices !== 'undefined' && dismissedIndices.size > 0);
|
|
|
|
// Open the window synchronously first to avoid popup-blocker blocking an async call
|
|
const win = window.open('about:blank', '_blank');
|
|
|
|
if (hasAdjustments) {
|
|
try {
|
|
await fetch('api.php?action=save_adjusted_result', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ job_id: currentJobId })
|
|
});
|
|
} catch (e) {
|
|
console.warn('Could not save adjusted result before export:', e);
|
|
}
|
|
}
|
|
|
|
const url = getExportUrl(currentJobId, format);
|
|
if (win) {
|
|
win.location.href = url;
|
|
} else {
|
|
window.open(url, '_blank');
|
|
}
|
|
}
|