mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
feat: upload progress bar — fieldset layout, accent colors, file name display, completion animation, 800ms redirect delay; decorelate formats from fichiers; server-side poll via token; bump PeerTube embed audio player
This commit is contained in:
@@ -162,7 +162,8 @@
|
||||
* Value: pipe-separated list of file names.
|
||||
*/
|
||||
function syncOrderInput(queueType, pond) {
|
||||
var form = pond.element ? pond.element.closest("form") : null;
|
||||
if (!pond || !pond.element) return;
|
||||
var form = pond.element.closest("form");
|
||||
if (!form) return;
|
||||
|
||||
var orderInput = form.querySelector("input[name='queue_order[" + queueType + "]']");
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
/**
|
||||
* upload-progress.js
|
||||
*
|
||||
* Intercepts admin form submissions (add.php / edit.php) and submits via
|
||||
* XMLHttpRequest to display a progress bar for large file uploads.
|
||||
* Falls back to native form POST when JavaScript is unavailable.
|
||||
* Intercepts admin form submissions with files and submits via XMLHttpRequest.
|
||||
* Polls GET /admin/actions/upload-progress.php?token=xxx for server-side
|
||||
* processing progress (PeerTube uploads, file moves).
|
||||
*
|
||||
* Requires an element with id="upload-progress-bar" inside the form.
|
||||
* The progress bar is normally hidden (display:none), shown only during upload.
|
||||
* Progress display:
|
||||
* 0%–25% : browser → server upload (XHR upload.progress)
|
||||
* 25%–99% : server processing (polled from progress endpoint)
|
||||
* 100% : response received — "Téléversé avec succès", then redirect
|
||||
*/
|
||||
(() => {
|
||||
'use strict';
|
||||
@@ -14,65 +16,133 @@
|
||||
const FORMS = document.querySelectorAll('form[data-upload-progress]');
|
||||
if (!FORMS.length) return;
|
||||
|
||||
const POLL_INTERVAL = 400;
|
||||
const UPLOAD_CAP = 25;
|
||||
const PROCESSING_MAX = 99;
|
||||
const SUCCESS_DELAY = 800;
|
||||
|
||||
for (const form of FORMS) {
|
||||
const progressWrap = form.querySelector('#upload-progress-wrap');
|
||||
const progressBar = form.querySelector('#upload-progress-bar');
|
||||
const progressText = form.querySelector('#upload-progress-text');
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const progressWrap = form.querySelector('#upload-progress-wrap');
|
||||
const progressBar = form.querySelector('#upload-progress-bar');
|
||||
const progressLabel = form.querySelector('#upload-progress-label');
|
||||
const progressFile = form.querySelector('#upload-progress-file');
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const tokenInput = form.querySelector('input[name="progress_token"]');
|
||||
|
||||
if (!progressBar || !progressWrap) continue;
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
// Only intercept if files are actually attached (FilePond inputs have files)
|
||||
const fileInputs = form.querySelectorAll('input[type="file"]');
|
||||
let hasFiles = false;
|
||||
for (const fi of fileInputs) {
|
||||
if (fi.files && fi.files.length > 0) {
|
||||
hasFiles = true;
|
||||
break;
|
||||
function collectFileNames() {
|
||||
const names = [];
|
||||
const inputs = form.querySelectorAll('input[type="file"]');
|
||||
for (const fi of inputs) {
|
||||
if (fi.files) {
|
||||
for (const f of fi.files) {
|
||||
if (f.name) names.push(f.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasFiles) return; // let native submit handle it
|
||||
return names;
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
const fileNames = collectFileNames();
|
||||
if (!fileNames.length) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Show progress bar
|
||||
progressWrap.style.display = 'block';
|
||||
const token = tokenInput ? tokenInput.value : '';
|
||||
|
||||
progressWrap.style.display = '';
|
||||
progressBar.value = 0;
|
||||
progressText.textContent = '0%';
|
||||
progressBar.removeAttribute('data-complete');
|
||||
progressLabel.textContent = 'Téléversement en cours…';
|
||||
progressFile.textContent = fileNames.length === 1
|
||||
? fileNames[0]
|
||||
: fileNames.length + ' fichiers';
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
|
||||
// Build FormData
|
||||
const fd = new FormData(form);
|
||||
// Ensure any FilePond-managed files are included — FilePond with
|
||||
// storeAsFile:true copies files into the <input>.files, so FormData
|
||||
// picks them up automatically from the DOM inputs.
|
||||
// But we must also respect queue_order hidden inputs.
|
||||
// FormData(form) already handles this since it reads all form fields.
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
let uploadDone = false;
|
||||
let lastUploadPct = 0;
|
||||
let pollingTimer = null;
|
||||
|
||||
/** Poll server-side progress */
|
||||
function startPolling() {
|
||||
if (pollingTimer || !token) return;
|
||||
progressLabel.textContent = 'Traitement en cours…';
|
||||
pollingTimer = setInterval(function () {
|
||||
fetch('/admin/actions/upload-progress.php?token=' + encodeURIComponent(token))
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data && data.stage && data.stage !== 'upload') {
|
||||
const pct = Math.min(PROCESSING_MAX, Math.max(UPLOAD_CAP, data.pct || UPLOAD_CAP));
|
||||
progressBar.value = pct;
|
||||
if (data.file) {
|
||||
progressFile.textContent = data.file;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function () { /* ignore poll errors */ });
|
||||
}, POLL_INTERVAL);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer);
|
||||
pollingTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function finishSuccess() {
|
||||
stopPolling();
|
||||
progressBar.value = 100;
|
||||
progressBar.setAttribute('data-complete', '');
|
||||
progressLabel.textContent = 'Téléversé avec succès';
|
||||
progressFile.textContent = '';
|
||||
}
|
||||
|
||||
// ── Upload phase (0% → UPLOAD_CAP) ──
|
||||
xhr.upload.addEventListener('progress', function (evt) {
|
||||
if (evt.lengthComputable) {
|
||||
const pct = Math.round((evt.loaded / evt.total) * 100);
|
||||
progressBar.value = pct;
|
||||
progressText.textContent = pct + '%';
|
||||
const rawPct = Math.round((evt.loaded / evt.total) * 100);
|
||||
const scaled = Math.round((rawPct / 100) * UPLOAD_CAP);
|
||||
if (scaled > lastUploadPct) {
|
||||
lastUploadPct = scaled;
|
||||
progressBar.value = scaled;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', function () {
|
||||
// Server returns a redirect (302) on success, or re-renders the form on error.
|
||||
// We can't follow 302 with XHR directly — the response body is the target page.
|
||||
// Check if we got a redirect by examining the response URL.
|
||||
const finalUrl = xhr.responseURL || '';
|
||||
const isRedirect = xhr.status >= 200 && xhr.status < 300 && finalUrl !== '' && !finalUrl.endsWith(form.action);
|
||||
xhr.upload.addEventListener('loadend', function () {
|
||||
uploadDone = true;
|
||||
progressBar.value = UPLOAD_CAP;
|
||||
startPolling();
|
||||
});
|
||||
|
||||
if (isRedirect) {
|
||||
// Success — navigate to the redirect target
|
||||
window.location.href = finalUrl;
|
||||
// ── Response handling ──
|
||||
xhr.addEventListener('readystatechange', function () {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) return;
|
||||
|
||||
stopPolling();
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
finishSuccess();
|
||||
|
||||
setTimeout(function () {
|
||||
const finalUrl = xhr.responseURL || '';
|
||||
if (finalUrl && finalUrl !== form.action) {
|
||||
window.location.href = finalUrl;
|
||||
} else {
|
||||
document.open();
|
||||
document.write(xhr.responseText);
|
||||
document.close();
|
||||
}
|
||||
}, SUCCESS_DELAY);
|
||||
} else {
|
||||
// Error — the server returned the form HTML with flash messages.
|
||||
// Replace the current page content.
|
||||
progressLabel.textContent = 'Erreur';
|
||||
progressFile.textContent = 'Échec du téléversement';
|
||||
document.open();
|
||||
document.write(xhr.responseText);
|
||||
document.close();
|
||||
@@ -80,11 +150,14 @@
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', function () {
|
||||
progressText.textContent = 'Erreur réseau';
|
||||
stopPolling();
|
||||
progressLabel.textContent = 'Erreur réseau';
|
||||
progressFile.textContent = '';
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
});
|
||||
|
||||
xhr.addEventListener('abort', function () {
|
||||
stopPolling();
|
||||
progressWrap.style.display = 'none';
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user