- 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
99 lines
3.7 KiB
JavaScript
99 lines
3.7 KiB
JavaScript
/* URL-tool flow: paste link → instant info → 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 urlInput = document.getElementById('url-input');
|
|
var infoPanel = document.getElementById('info-panel');
|
|
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 pollTimer = null, currentJobId = null;
|
|
|
|
function track(name) {
|
|
try {
|
|
navigator.sendBeacon &&
|
|
navigator.sendBeacon('/api/events', new Blob([JSON.stringify({ name: name, tool: slug })], { type: 'application/json' }));
|
|
} catch (e) {}
|
|
}
|
|
|
|
function showError(msg) { errorBox.textContent = msg; errorBox.hidden = false; track('tool_failure'); }
|
|
|
|
function setStage(stage, progress) {
|
|
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) + '%';
|
|
}
|
|
|
|
form.addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
hideError();
|
|
resultPanel.classList.remove('show');
|
|
|
|
fetch('/api/jobs', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ tool: slug, url: urlInput.value })
|
|
})
|
|
.then(function (r) { return r.json().then(function (j) { return { status: r.status, body: j }; }); })
|
|
.then(function (res) {
|
|
if (!res.ok) throw new Error(res.body.message || res.body.error || 'Request failed.');
|
|
currentJobId = res.body.job_id;
|
|
setStage('preparing', 8);
|
|
poll();
|
|
})
|
|
.catch(function (err) { showError(err.message || 'Something went wrong. Please try again.'); });
|
|
});
|
|
|
|
function hideError() { errorBox.hidden = true; }
|
|
|
|
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');
|
|
if (data.status === 'completed') { finish(data); return; }
|
|
if (data.status === 'failed') { showError(data.message || 'This video could not be processed.'); return; }
|
|
var stageMap = { preparing: 'preparing', fetching: 'fetching', processing: 'processing', converting: 'converting' };
|
|
setStage(stageMap[data.stage] || 'processing', data.progress);
|
|
pollTimer = setTimeout(poll, 1600);
|
|
})
|
|
.catch(function () { pollTimer = setTimeout(poll, 3200); });
|
|
}
|
|
|
|
function finish(data) {
|
|
clearTimeout(pollTimer);
|
|
setStage('complete', 100);
|
|
resultPanel.innerHTML =
|
|
'<div class="result-meta">' +
|
|
'<span class="badge">Done</span>' +
|
|
'<strong>' + escapeHtml(data.output_name || 'download') + '</strong>' +
|
|
'<span>' + humanSize(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>';
|
|
resultPanel.classList.add('show');
|
|
}
|
|
|
|
function humanSize(bytes) {
|
|
var u = ['B', 'KB', 'MB', 'GB'], i = 0;
|
|
while (bytes >= 1024 && i < u.length - 1) { bytes /= 1024; i++; }
|
|
return (Math.round(bytes * 10) / 10) + ' ' + u[i];
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
var d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
})();
|