/* Upload-tool flow: dropzone → upload → job → poll → download. */ (function () { 'use strict'; var panel = document.getElementById('tool-panel'); if (!panel) return; var slug = panel.dataset.tool; var form = document.getElementById('tool-form'); var zone = document.getElementById('dropzone'); var fileInput = document.getElementById('file-input'); var chip = document.getElementById('file-chip'); var chipName = document.getElementById('chip-name'); var chipSize = document.getElementById('chip-size'); var chipRemove = document.getElementById('chip-remove'); var submitBtn = document.getElementById('convert-btn'); var statusFlow = document.getElementById('status-flow'); var progressFill = document.getElementById('progress-fill'); var resultPanel = document.getElementById('result-panel'); var errorBox = document.getElementById('error-box'); var uploadedFile = null; var pollTimer = null; var currentJobId = null; function fmtSize(bytes) { var units = ['B', 'KB', 'MB', 'GB'], i = 0, n = bytes; while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } return (Math.round(n * 10) / 10) + ' ' + units[i]; } function showError(msg) { errorBox.textContent = msg; errorBox.hidden = false; track('tool_failure', {}); } function hideError() { errorBox.hidden = true; } function track(name, extra) { try { navigator.sendBeacon && navigator.sendBeacon('/api/events', new Blob([JSON.stringify(Object.assign({ name: name, tool: slug }, extra))], { type: 'application/json' })); } catch (e) {} } function setStage(stage, progress) { if (!statusFlow) return; statusFlow.hidden = false; document.querySelectorAll('.stages li').forEach(function (li) { li.classList.toggle('done', li.dataset.stage !== stage); li.classList.toggle('active', li.dataset.stage === stage); }); if (typeof progress === 'number') progressFill.style.width = Math.max(6, progress) + '%'; } function acceptFile(file) { if (!file) return; hideError(); chipName.textContent = file.name; chipSize.textContent = fmtSize(file.size); chip.hidden = false; submitBtn.disabled = false; } // ---- dropzone interactions ---- ['dragover', 'dragenter'].forEach(function (ev) { zone.addEventListener(ev, function (e) { e.preventDefault(); zone.classList.add('dragover'); }); }); ['dragleave', 'drop'].forEach(function (ev) { zone.addEventListener(ev, function (e) { e.preventDefault(); zone.classList.remove('dragover'); }); }); zone.addEventListener('drop', function (e) { fileInput.files = e.dataTransfer.files; acceptFile(fileInput.files[0]); }); zone.addEventListener('click', function () { fileInput.click(); }); zone.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') fileInput.click(); }); fileInput.addEventListener('change', function () { acceptFile(fileInput.files[0]); }); chipRemove.addEventListener('click', function () { fileInput.value = ''; chip.hidden = true; submitBtn.disabled = true; }); // ---- submit → upload → start job ---- form.addEventListener('submit', function (e) { e.preventDefault(); if (!fileInput.files.length || !submitBtn.disabled === false) { if (!fileInput.files.length) return; } submitBtn.disabled = true; hideError(); var fd = new FormData(); fd.append('tool', slug); fd.append('file', fileInput.files[0]); setStage('preparing', 4); fetch('/upload', { method: 'POST', body: fd }) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); }) .then(function (res) { if (!res.ok) throw new Error(res.body.error || 'Upload failed.'); uploadedFile = res.body.file; return startJob(res.body); }) .catch(function (err) { showError(err.message || 'Upload failed. Please check the file and try again.'); submitBtn.disabled = false; }); }); function opt(id) { var el = document.getElementById(id); return el ? el.value : undefined; } function startJob(uploadInfo) { var params = {}; ['format', 'bitrate', 'quality', 'width', 'height', 'start', 'end', 'x', 'y', 'crop_width', 'crop_height', 'degrees', 'direction', 'factor', 'fps', 'gain_db', 'level', 'preset', 'pages'].forEach(function (k) { var v = opt('opt-' + k); if (v !== undefined && v !== '') params[k] = isNaN(v) || v === '' ? v : Number(v); }); // map option ids to API param names where they differ if (params.quality !== undefined) params.quality = Number(params.quality); // level select feeds "level" if (opt('opt-level') !== undefined && opt('opt-level') !== '') { params.level = Number(opt('opt-level')); delete params.quality; } if (params.gain_db !== undefined) params.gain_db = Number(params.gain_db); var fd = new FormData(); fd.append('tool', slug); Object.keys(params).forEach(function (k) { fd.append(k, params[k]); }); return fetch('/jobs/start', { method: 'POST', body: fd }) .then(function (r) { return r.json(); }) .then(function (data) { if (!data.ok) throw new Error(data.error || 'Could not start processing.'); currentJobId = data.job_id; track('conversion_format', { format: params.format || (document.getElementById('opt-format') || {}).value || '' }); poll(); }); } function poll() { if (!currentJobId) return; fetch('/api/jobs/' + currentJobId) .then(function (r) { return r.json(); }) .then(function (data) { if (!data.ok) throw new Error('lost'); var stageMap = { preparing: 'preparing', fetching: 'processing', processing: 'processing', converting: 'converting' }; if (data.status === 'completed') { finish(data); return; } if (data.status === 'failed') { showError(data.message || 'Processing failed. Please try again.'); submitBtn.disabled = false; return; } setStage(stageMap[data.stage] || 'processing', data.progress); pollTimer = setTimeout(poll, 1500); }) .catch(function () { pollTimer = setTimeout(poll, 3000); }); } function finish(data) { setStage('complete', 100); clearTimeout(pollTimer); var ext = (data.output_name || '').split('.').pop() || 'file'; var isMedia = /^(jpg|jpeg|png|webp|gif|mp4|webm|mp3|wav|m4a|pdf)$/.test(ext); resultPanel.innerHTML = '
' + (isMedia ? '