- 134-tool registry with programmatic SEO (unique titles/H1/descriptions, JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter) - DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD), Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG) - Security: SSRF guard, MIME validation, rate limits, API-key auth, bcrypt admin login, security headers - Admin panel: dashboard, tools/categories/guides CRUD, SEO audit, analytics, job inspector with retry, system health, feature flags - Docker deployment (nginx + web/api FPM pools + scalable workers), PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
195 lines
7.4 KiB
JavaScript
195 lines
7.4 KiB
JavaScript
/* 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 =
|
|
'<div class="result-meta">' +
|
|
'<span class="badge">Done</span>' +
|
|
'<strong>' + escapeHtml(data.output_name || 'result.' + ext) + '</strong>' +
|
|
'<span class="hint">' + fmtSize(data.output_size || 0) + '</span>' +
|
|
'<a class="btn-primary" style="text-decoration:none;padding:.55rem 1.3rem;font-size:.95rem" href="' + data.download_url + '" download>Download</a>' +
|
|
'</div>' +
|
|
(isMedia ? '<div style="margin-top:.9rem;text-align:center"><img src="' + data.preview_url + '" alt="Result preview" loading="lazy" style="max-height:260px;border-radius:10px"></div>'.replace('<img', ext.match(/^(mp4|webm)$/) ? '<video controls autoplay muted loop><source src="' + data.preview_url + '" type="video/mp4"></video><img style="display:none"' : '<img') : '');
|
|
|
|
resultPanel.classList.add('show');
|
|
submitBtn.disabled = false;
|
|
track('download', { format: ext });
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
var d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
})();
|