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:
@@ -32,7 +32,12 @@ require_once APP_ROOT . '/src/ErrorHandler.php';
|
||||
|
||||
try {
|
||||
$ctrl = ThesisEditController::create();
|
||||
$ctrl->save($thesisId, $_POST, $_FILES);
|
||||
$progressToken = $_POST['progress_token'] ?? bin2hex(random_bytes(8));
|
||||
$ctrl->save($thesisId, $_POST, $_FILES, $progressToken);
|
||||
|
||||
// Clean up progress file
|
||||
require_once APP_ROOT . '/src/PeerTubeService.php';
|
||||
PeerTubeService::clearProgress($progressToken);
|
||||
|
||||
// Regenerate CSRF token after successful save
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
|
||||
43
app/public/admin/actions/upload-progress.php
Normal file
43
app/public/admin/actions/upload-progress.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* upload-progress.php
|
||||
*
|
||||
* Returns the current upload/processing progress for a given token.
|
||||
* Called by the client-side upload-progress.js while the form XHR is in flight.
|
||||
*
|
||||
* GET /admin/actions/upload-progress.php?token=<token>
|
||||
*
|
||||
* Response: JSON
|
||||
* { "stage": "upload"|"processing"|"done", "pct": 45, "file": "video.mp4" }
|
||||
*
|
||||
* Progress data is written by ThesisEditController / ThesisCreateController
|
||||
* to a temp file during processing.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$token = $_GET['token'] ?? '';
|
||||
if (!preg_match('/^[a-f0-9]{16}$/', $token)) {
|
||||
echo json_encode(['stage' => 'error', 'error' => 'Invalid token']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$progressFile = sys_get_temp_dir() . '/xamxam_upload_' . $token . '.json';
|
||||
|
||||
if (!file_exists($progressFile)) {
|
||||
// No progress file yet — still in upload phase (or token invalid)
|
||||
echo json_encode(['stage' => 'upload', 'pct' => 0, 'file' => '']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents($progressFile), true);
|
||||
if (!$data) {
|
||||
echo json_encode(['stage' => 'upload', 'pct' => 0, 'file' => '']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
@@ -322,7 +322,7 @@
|
||||
.licence-explanation {
|
||||
background: var(--bg-secondary);
|
||||
border-left: 4px solid var(--border-secondary, var(--border-primary));
|
||||
padding: var(--space-m);
|
||||
/* padding: var(--space-m); */
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -1287,3 +1287,78 @@ a.recap-file-name:hover {
|
||||
font-size: var(--step--2);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* ── Upload progress bar ─────────────────────────────────── */
|
||||
|
||||
#upload-progress-wrap {
|
||||
border: 1px solid var(--accent-muted);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--space-s);
|
||||
margin-bottom: var(--space-s);
|
||||
}
|
||||
|
||||
#upload-progress-wrap legend {
|
||||
font-weight: 600;
|
||||
font-size: var(--step--1);
|
||||
color: var(--text-primary);
|
||||
padding: 0 var(--space-2xs);
|
||||
}
|
||||
|
||||
#upload-progress-bar {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 0.85rem;
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
background: var(--accent-muted);
|
||||
border: none;
|
||||
}
|
||||
|
||||
#upload-progress-bar::-webkit-progress-bar {
|
||||
background: var(--accent-muted);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
#upload-progress-bar::-webkit-progress-value {
|
||||
background: var(--accent-primary);
|
||||
border-radius: var(--radius);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
#upload-progress-bar::-moz-progress-bar {
|
||||
background: var(--accent-primary);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
/* Completion state */
|
||||
#upload-progress-bar[data-complete] {
|
||||
box-shadow: 0 0 12px rgba(149, 87, 181, 0.4);
|
||||
}
|
||||
|
||||
#upload-progress-bar[data-complete]::-webkit-progress-value {
|
||||
background: var(--accent-green);
|
||||
box-shadow: 0 0 8px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
#upload-progress-bar[data-complete]::-moz-progress-bar {
|
||||
background: var(--accent-green);
|
||||
}
|
||||
|
||||
/* ── Sticky formats fieldset ──────────────────────────────── */
|
||||
|
||||
#fieldset-formats {
|
||||
position: sticky;
|
||||
top: var(--space-s);
|
||||
z-index: 10;
|
||||
/* background: var(--bg-primary); */
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
legend {
|
||||
text-shadow: var(--bg-primary) 0px 0px 2px;
|
||||
}
|
||||
|
||||
/* Stickiness is scoped to the parent container */
|
||||
#format-fichiers-block {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -4,15 +4,9 @@
|
||||
*
|
||||
* Returns the combined Format(s) + Fichiers block.
|
||||
*
|
||||
* Architecture:
|
||||
* - Formats checkboxes: static, never swapped. They trigger HTMX swaps
|
||||
* on individual #slot-siteweb, #slot-video, #slot-audio elements.
|
||||
* - File inputs (couverture, note d'intention, TFE, annexes): always
|
||||
* static in the DOM — never destroyed by format toggling.
|
||||
* - Format-specific extras: each is a standalone HTMX fragment slot.
|
||||
* When unchecked → empty hidden placeholder. When checked → input
|
||||
* fields rendered via HTMX. This preserves FilePond instances on
|
||||
* the main file inputs across format changes.
|
||||
* All slots (Site web, Vidéo, Audio) are always visible — decorelated from
|
||||
* the format checkboxes. The checkboxes serve only as metadata selectors;
|
||||
* they no longer trigger HTMX swaps.
|
||||
*
|
||||
* Expected POST:
|
||||
* formats[] — array of selected format_type IDs
|
||||
@@ -28,21 +22,9 @@ $peerTubeSettings = PeerTubeService::getSettings($_ptDb);
|
||||
|
||||
$db = $_ptDb->getConnection();
|
||||
|
||||
// Load all format types in display order
|
||||
$allFormats = $db->query('SELECT id, name FROM format_types ORDER BY sort_order, id')
|
||||
->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Build name→id map for format logic
|
||||
$formatIdByName = [];
|
||||
foreach ($allFormats as $f) {
|
||||
$formatIdByName[$f['name']] = (int)$f['id'];
|
||||
}
|
||||
|
||||
$siteWebId = $formatIdByName['Site web'] ?? null;
|
||||
$videoId = $formatIdByName['Vidéo'] ?? null;
|
||||
$audioId = $formatIdByName['Audio'] ?? null;
|
||||
$imageId = $formatIdByName['Image'] ?? null;
|
||||
|
||||
$selectedFormats = isset($_POST['formats']) && is_array($_POST['formats'])
|
||||
? array_map('intval', $_POST['formats'])
|
||||
: [];
|
||||
@@ -50,25 +32,10 @@ $selectedFormats = isset($_POST['formats']) && is_array($_POST['formats'])
|
||||
$adminMode = ($_POST['admin_mode'] ?? '0') === '1';
|
||||
$editMode = ($_POST['edit_mode'] ?? '0') === '1';
|
||||
|
||||
$hasSiteWeb = $siteWebId && in_array($siteWebId, $selectedFormats, true);
|
||||
$hasVideo = $videoId && in_array($videoId, $selectedFormats, true);
|
||||
$hasAudio = $audioId && in_array($audioId, $selectedFormats, true);
|
||||
$hasImage = $imageId && in_array($imageId, $selectedFormats, true);
|
||||
|
||||
$hasNonWebFormat = !empty(array_filter(
|
||||
$selectedFormats,
|
||||
fn($id) => $id !== $siteWebId
|
||||
));
|
||||
$showUploadBlock = $hasNonWebFormat || !$hasSiteWeb;
|
||||
|
||||
$websiteUrl = htmlspecialchars($_POST['website_url'] ?? '');
|
||||
$websiteLabel = htmlspecialchars($_POST['website_label'] ?? '');
|
||||
|
||||
$hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragment';
|
||||
|
||||
$hasAnnexesChecked = !empty($_POST['has_annexes']);
|
||||
?>
|
||||
<!-- ═══════════════════ Format(s) + Fichiers (static, never swapped) ═══════════════════ -->
|
||||
<!-- ═══════════════════ Format(s) + Fichiers ═══════════════════ -->
|
||||
<div id="format-fichiers-block">
|
||||
<input type="hidden" name="admin_mode" value="<?= $adminMode ? '1' : '0' ?>">
|
||||
<input type="hidden" name="edit_mode" value="<?= $editMode ? '1' : '0' ?>">
|
||||
@@ -76,8 +43,8 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
||||
<input type="hidden" name="_cover" value="<?= htmlspecialchars($_POST['_cover']) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- ═══════════════════ Format(s) ═══════════════════ -->
|
||||
<fieldset>
|
||||
<!-- ═══════════════════ Format(s) — sticky ═══════════════════ -->
|
||||
<fieldset id="fieldset-formats">
|
||||
<legend>Format(s)</legend>
|
||||
<div>
|
||||
<span class="admin-row-label">Format(s) du TFE :<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></span>
|
||||
@@ -91,30 +58,7 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
||||
<input type="checkbox"
|
||||
name="formats[]"
|
||||
value="<?= htmlspecialchars((string)$opt['id']) ?>"
|
||||
<?= in_array((int)$opt['id'], $selectedFormats, true) ? 'checked' : '' ?>
|
||||
<?php if ((int)$opt['id'] === ($siteWebId ?? 0)): ?>
|
||||
hx-post="<?= htmlspecialchars($hxPost) ?>"
|
||||
hx-target="#slot-siteweb"
|
||||
hx-select="#slot-siteweb"
|
||||
hx-trigger="change"
|
||||
hx-include="[name='formats[]'], [name='website_url'], [name='admin_mode'], [name='edit_mode'], [name='_cover']"
|
||||
hx-swap="outerHTML"
|
||||
<?php elseif ((int)$opt['id'] === ($videoId ?? 0)): ?>
|
||||
hx-post="<?= htmlspecialchars($hxPost) ?>"
|
||||
hx-target="#slot-video"
|
||||
hx-select="#slot-video"
|
||||
hx-trigger="change"
|
||||
hx-include="[name='formats[]'], [name='admin_mode'], [name='edit_mode'], [name='_cover']"
|
||||
hx-swap="outerHTML"
|
||||
<?php elseif ((int)$opt['id'] === ($audioId ?? 0)): ?>
|
||||
hx-post="<?= htmlspecialchars($hxPost) ?>"
|
||||
hx-target="#slot-audio"
|
||||
hx-select="#slot-audio"
|
||||
hx-trigger="change"
|
||||
hx-include="[name='formats[]'], [name='admin_mode'], [name='edit_mode'], [name='_cover']"
|
||||
hx-swap="outerHTML"
|
||||
<?php endif; ?>
|
||||
>
|
||||
<?= in_array((int)$opt['id'], $selectedFormats, true) ? 'checked' : '' ?>>
|
||||
<?= htmlspecialchars($opt['name']) ?>
|
||||
</label>
|
||||
</li>
|
||||
@@ -129,16 +73,14 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
||||
<legend>Fichiers</legend>
|
||||
|
||||
<?php
|
||||
// Existing files + cover preview (edit mode only, initial render — not re-sent on HTMX swaps)
|
||||
// Existing files + cover preview (edit mode only)
|
||||
$_efiles = $currentFiles ?? [];
|
||||
$_cover = $_POST['_cover'] ?? null;
|
||||
if ($editMode && (!empty($_efiles) || $_cover)):
|
||||
?>
|
||||
<div class="admin-form-group">
|
||||
<ul id="existing-files-sortable" class="admin-file-list">
|
||||
<?php
|
||||
// ── Couverture preview ──
|
||||
if ($_cover): ?>
|
||||
<?php if ($_cover): ?>
|
||||
<li class="admin-file-list-item">
|
||||
<span class="admin-file-icon-col">🖼️</span>
|
||||
<span class="admin-file-info">
|
||||
@@ -154,7 +96,6 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
// ── Existing files ──
|
||||
$_thesisFilesList = array_values(array_filter($_efiles, fn($f) => $f["file_type"] !== "cover"));
|
||||
foreach ($_thesisFilesList as $_f):
|
||||
$_fPath = $_f["file_path"] ?? "";
|
||||
@@ -204,7 +145,7 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- ── 1. Couverture (always, FilePond single-file) ── -->
|
||||
<!-- ── 1. Couverture ── -->
|
||||
<?php
|
||||
$_cover = $_POST['_cover'] ?? null;
|
||||
if (!$editMode || !$_cover): ?>
|
||||
@@ -220,7 +161,7 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
||||
</div>
|
||||
<?php endif; unset($_cover); ?>
|
||||
|
||||
<!-- ── 2. Note d'intention (always, FilePond single-file) ── -->
|
||||
<!-- ── 2. Note d'intention ── -->
|
||||
<div class="admin-form-group">
|
||||
<label for="note_intention">Note d'intention<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
||||
<div class="admin-file-input">
|
||||
@@ -233,7 +174,7 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 3. TFE — multi-file upload (FilePond) ── -->
|
||||
<!-- ── 3. TFE ── -->
|
||||
<div class="admin-form-group admin-files-fieldgroup">
|
||||
<label for="tfe-files-input">TFE<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
||||
<div class="admin-file-input">
|
||||
@@ -256,9 +197,8 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 4. Annexes — multi-file upload (FilePond), always visible ── -->
|
||||
<!-- ── 4. Annexes ── -->
|
||||
<div id="annexes-input-block">
|
||||
<!-- has_annexes checkbox disabled — annexe pool always on -->
|
||||
<input type="hidden" name="has_annexes" value="0">
|
||||
<div class="admin-form-group admin-files-fieldgroup">
|
||||
<label for="annexe-files-input">Annexes (optionnel)</label>
|
||||
@@ -272,87 +212,70 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Format-specific extras (individual swappable slots) ── -->
|
||||
<div id="format-extras-block" style="display:flex;flex-direction:column;gap:var(--space-s);">
|
||||
<!-- Slot: Site web -->
|
||||
<?php if ($hasSiteWeb): ?>
|
||||
<div id="slot-siteweb" class="admin-form-group">
|
||||
<label for="website_url">URL du site<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
||||
<div class="admin-file-input">
|
||||
<input type="url" id="website_url" name="website_url"
|
||||
value="<?= $websiteUrl ?>"
|
||||
placeholder="https://mon-tfe.erg.be"
|
||||
<?= !$adminMode ? 'required' : '' ?>>
|
||||
<small>Le TFE sera affiché comme un site embarqué sur sa page publique.</small>
|
||||
</div>
|
||||
<!-- ── 5. Site web url ── -->
|
||||
<div id="slot-siteweb" class="admin-form-group">
|
||||
<label for="website_url">URL du site (optionnel)</label>
|
||||
<div class="admin-file-input">
|
||||
<input type="url" id="website_url" name="website_url"
|
||||
value="<?= $websiteUrl ?>"
|
||||
placeholder="https://mon-tfe.erg.be">
|
||||
<small>Le TFE sera affiché comme un site embarqué sur sa page publique.</small>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div id="slot-siteweb" hidden></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Slot: Video (always visible when PeerTube enabled) -->
|
||||
<?php if ($peerTubeEnabled): ?>
|
||||
<div id="slot-video" class="admin-form-group admin-files-fieldgroup">
|
||||
<label for="peertube-video-input">Vidéo<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="peertube-video-input"
|
||||
name="queue_file[peertube_video][]"
|
||||
multiple
|
||||
accept="video/mp4,video/webm,video/ogg,video/quicktime,.mp4,.webm,.ogv,.mov"
|
||||
class="tfe-file-picker"
|
||||
<?= !$adminMode ? 'required' : '' ?>>
|
||||
<small class="admin-file-hint">MP4, WebM ou MOV. Max 500 MB. Glissez pour réordonner. Hébergé sur <a href="<?= htmlspecialchars($peerTubeSettings['instance_url']) ?>" target="_blank" rel="noopener">PeerTube</a>.</small>
|
||||
</div>
|
||||
</div>
|
||||
<?php elseif ($hasVideo): ?>
|
||||
<div id="slot-video" class="admin-form-group admin-files-fieldgroup">
|
||||
<label for="video-files-input">Vidéo<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="video-files-input"
|
||||
name="queue_file[video][]"
|
||||
multiple
|
||||
accept="video/mp4,video/webm,video/ogg,video/quicktime,.mp4,.webm,.ogv,.mov"
|
||||
class="tfe-file-picker"
|
||||
<?= !$adminMode ? 'required' : '' ?>>
|
||||
<small class="admin-file-hint">MP4, WebM ou MOV. Max 500 MB. Glissez pour réordonner.</small>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div id="slot-video" hidden></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Slot: Audio (always visible when PeerTube enabled) -->
|
||||
<?php if ($peerTubeEnabled): ?>
|
||||
<div id="slot-audio" class="admin-form-group admin-files-fieldgroup">
|
||||
<label for="peertube-audio-input">Audio<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="peertube-audio-input"
|
||||
name="queue_file[peertube_audio][]"
|
||||
multiple
|
||||
accept="audio/mpeg,audio/ogg,audio/wav,audio/flac,audio/aac,audio/mp4,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a"
|
||||
class="tfe-file-picker"
|
||||
<?= !$adminMode ? 'required' : '' ?>>
|
||||
<small class="admin-file-hint">MP3, OGG, WAV, FLAC ou AAC. Max 500 MB. Glissez pour réordonner. Hébergé sur <a href="<?= htmlspecialchars($peerTubeSettings['instance_url']) ?>" target="_blank" rel="noopener">PeerTube</a>.</small>
|
||||
</div>
|
||||
</div>
|
||||
<?php elseif ($hasAudio): ?>
|
||||
<div id="slot-audio" class="admin-form-group admin-files-fieldgroup">
|
||||
<label for="audio-files-input">Audio<?= !$adminMode ? ' <span class="asterisk">*</span>' : '' ?></label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="audio-files-input"
|
||||
name="queue_file[audio][]"
|
||||
multiple
|
||||
accept="audio/mpeg,audio/ogg,audio/wav,audio/flac,audio/aac,audio/mp4,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a"
|
||||
class="tfe-file-picker"
|
||||
<?= !$adminMode ? 'required' : '' ?>>
|
||||
<small class="admin-file-hint">MP3, OGG, WAV, FLAC ou AAC. Max 500 MB. Glissez pour réordonner.</small>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div id="slot-audio" hidden></div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── 6. Vidéo / PeerTube ── -->
|
||||
<?php if ($peerTubeEnabled): ?>
|
||||
<div id="slot-video" class="admin-form-group admin-files-fieldgroup">
|
||||
<label for="peertube-video-input">Vidéo (optionnel)</label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="peertube-video-input"
|
||||
name="queue_file[peertube_video][]"
|
||||
multiple
|
||||
accept="video/mp4,video/webm,video/ogg,video/quicktime,.mp4,.webm,.ogv,.mov"
|
||||
class="tfe-file-picker">
|
||||
<small class="admin-file-hint">MP4, WebM ou MOV. Max 500 MB. Hébergé sur <a href="<?= htmlspecialchars($peerTubeSettings['instance_url']) ?>" target="_blank" rel="noopener">PeerTube</a>.</small>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div id="slot-video" class="admin-form-group admin-files-fieldgroup">
|
||||
<label for="video-files-input">Vidéo (optionnel)</label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="video-files-input"
|
||||
name="queue_file[video][]"
|
||||
multiple
|
||||
accept="video/mp4,video/webm,video/ogg,video/quicktime,.mp4,.webm,.ogv,.mov"
|
||||
class="tfe-file-picker">
|
||||
<small class="admin-file-hint">MP4, WebM ou MOV. Max 500 MB.</small>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- ── 7. Audio / PeerTube ── -->
|
||||
<?php if ($peerTubeEnabled): ?>
|
||||
<div id="slot-audio" class="admin-form-group admin-files-fieldgroup">
|
||||
<label for="peertube-audio-input">Audio (optionnel)</label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="peertube-audio-input"
|
||||
name="queue_file[peertube_audio][]"
|
||||
multiple
|
||||
accept="audio/mpeg,audio/ogg,audio/wav,audio/flac,audio/aac,audio/mp4,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a"
|
||||
class="tfe-file-picker">
|
||||
<small class="admin-file-hint">MP3, OGG, WAV, FLAC ou AAC. Max 500 MB. Hébergé sur <a href="<?= htmlspecialchars($peerTubeSettings['instance_url']) ?>" target="_blank" rel="noopener">PeerTube</a>.</small>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div id="slot-audio" class="admin-form-group admin-files-fieldgroup">
|
||||
<label for="audio-files-input">Audio (optionnel)</label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="audio-files-input"
|
||||
name="queue_file[audio][]"
|
||||
multiple
|
||||
accept="audio/mpeg,audio/ogg,audio/wav,audio/flac,audio/aac,audio/mp4,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a"
|
||||
class="tfe-file-picker">
|
||||
<small class="admin-file-hint">MP3, OGG, WAV, FLAC ou AAC. Max 500 MB.</small>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</fieldset><!-- /Fichiers -->
|
||||
</div><!-- #format-fichiers-block -->
|
||||
|
||||
@@ -161,7 +161,7 @@ class ThesisEditController
|
||||
* the transaction is still open, but this method rolls
|
||||
* back internally before re-throwing).
|
||||
*/
|
||||
public function save(int $thesisId, array $post, array $files): void
|
||||
public function save(int $thesisId, array $post, array $files, ?string $progressToken = null): void
|
||||
{
|
||||
if ($thesisId <= 0) {
|
||||
throw new InvalidArgumentException('ID de TFE invalide.');
|
||||
@@ -440,8 +440,8 @@ class ThesisEditController
|
||||
// ── PeerTube video / audio uploads (from FilePond queue) ──────────────
|
||||
$qPTVideo = $this->extractFilesSubArray($queueFiles, 'peertube_video');
|
||||
$qPTAudio = $this->extractFilesSubArray($queueFiles, 'peertube_audio');
|
||||
$this->handlePeerTubeQueueFiles($thesisId, trim($post['titre'] ?? ''), $qPTVideo, 'video');
|
||||
$this->handlePeerTubeQueueFiles($thesisId, trim($post['titre'] ?? ''), $qPTAudio, 'audio');
|
||||
$this->handlePeerTubeQueueFiles($thesisId, trim($post['titre'] ?? ''), $qPTVideo, 'video', $progressToken);
|
||||
$this->handlePeerTubeQueueFiles($thesisId, trim($post['titre'] ?? ''), $qPTAudio, 'audio', $progressToken);
|
||||
|
||||
// ── Website URL — add or update ──────────────────────────────────────
|
||||
$this->handleWebsiteUrl($thesisId, $post);
|
||||
@@ -583,7 +583,7 @@ class ThesisEditController
|
||||
* @param array|null $uploads Flat $_FILES-style array from extractFilesSubArray().
|
||||
* @param string $fileType 'video' or 'audio'.
|
||||
*/
|
||||
private function handlePeerTubeQueueFiles(int $thesisId, string $title, ?array $uploads, string $fileType): void
|
||||
private function handlePeerTubeQueueFiles(int $thesisId, string $title, ?array $uploads, string $fileType, ?string $progressToken = null): void
|
||||
{
|
||||
if (!$uploads || !is_array($uploads['name'] ?? null)) {
|
||||
return;
|
||||
@@ -594,17 +594,23 @@ class ThesisEditController
|
||||
return;
|
||||
}
|
||||
|
||||
$label = $fileType === 'video' ? 'Vidéo' : 'Audio';
|
||||
$count = count($uploads['name']);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
if (($uploads['error'][$i] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fileName = $uploads['name'][$i];
|
||||
if ($progressToken) {
|
||||
PeerTubeService::writeProgress($progressToken, 'peertube', 25 + (int)(($i / max($count, 1)) * 74), $label . ' : ' . $fileName);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = PeerTubeService::upload(
|
||||
$this->db,
|
||||
$uploads['tmp_name'][$i],
|
||||
$uploads['name'][$i],
|
||||
$fileName,
|
||||
$title,
|
||||
''
|
||||
);
|
||||
@@ -614,7 +620,7 @@ class ThesisEditController
|
||||
$thesisId,
|
||||
$fileType,
|
||||
$storedPath,
|
||||
basename($uploads['name'][$i]),
|
||||
basename($fileName),
|
||||
$uploads['size'][$i],
|
||||
$uploads['type'][$i] ?? 'application/octet-stream',
|
||||
null,
|
||||
|
||||
@@ -436,4 +436,30 @@ class PeerTubeService
|
||||
|
||||
return ['status' => $status, 'body' => (string)$responseBody, 'headers' => $responseHeaders];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Progress reporting (for upload-progress.js polling)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Write upload progress to a temp file polled by the progress endpoint.
|
||||
*/
|
||||
public static function writeProgress(string $token, string $stage, int $pct, string $file = ''): void
|
||||
{
|
||||
$progressFile = sys_get_temp_dir() . '/xamxam_upload_' . $token . '.json';
|
||||
file_put_contents($progressFile, json_encode([
|
||||
'stage' => $stage,
|
||||
'pct' => $pct,
|
||||
'file' => $file,
|
||||
]), LOCK_EX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the progress file for a given token.
|
||||
*/
|
||||
public static function clearProgress(string $token): void
|
||||
{
|
||||
$progressFile = sys_get_temp_dir() . '/xamxam_upload_' . $token . '.json';
|
||||
@unlink($progressFile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1013,6 +1013,58 @@
|
||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
+\\\\\\\ to: txzqmwnx fe4b8d24 "fix PeerTube upload: switch to simple multipart POST /api/v1/videos/upload with CURLFile; remove resumable protocol" (rebased revision)
|
||||
++ $linkName = $link['name'] ?? '';
|
||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: txzqmwnx fe4b8d24 "fix PeerTube upload: switch to simple multipart POST /api/v1/videos/upload with CURLFile; remove resumable protocol" (rebased revision)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
- $linkName = $link['name'] ?? '';
|
||||
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: qlvmyvuo 284872ea "Bump peertube embed audio player height + remove figure for iframes in tfe.php" (rebased revision)
|
||||
$linkName = $link['name'] ?? '';
|
||||
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
$linkLockedYear = $link['locked_year'] ?? null;
|
||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
+\\\\\\\ to: qlvmyvuo ac39e754 "Bump peertube embed audio player height + remove figure for iframes in tfe.php" (rebased revision)
|
||||
++ $linkName = $link['name'] ?? '';
|
||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: qlvmyvuo ac39e754 "Bump peertube embed audio player height + remove figure for iframes in tfe.php" (rebased revision)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
- $linkName = $link['name'] ?? '';
|
||||
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: qvnunynl f12d35f5 "upload progress bar: fieldset layout, accent colors, file name display, completion animation, 800ms delay before redirect" (rebased revision)
|
||||
$linkName = $link['name'] ?? '';
|
||||
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
$linkLockedYear = $link['locked_year'] ?? null;
|
||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
+\\\\\\\ to: qvnunynl cd701e3c "upload progress bar: fieldset layout, accent colors, file name display, completion animation, 800ms delay before redirect" (rebased revision)
|
||||
++ $linkName = $link['name'] ?? '';
|
||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: qvnunynl cd701e3c "upload progress bar: fieldset layout, accent colors, file name display, completion animation, 800ms delay before redirect" (rebased revision)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
- $linkName = $link['name'] ?? '';
|
||||
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: nwozruss 84ec9655 "decorelate formats from fichiers: no HTMX toggles, all slots always visible; progress bar 25/75 split; sticky formats fieldset" (rebased revision)
|
||||
$linkName = $link['name'] ?? '';
|
||||
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
$linkLockedYear = $link['locked_year'] ?? null;
|
||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
+\\\\\\\ to: nwozruss f8d22bfa "decorelate formats from fichiers: no HTMX toggles, all slots always visible; progress bar 25/75 split; sticky formats fieldset" (rebased revision)
|
||||
++ $linkName = $link['name'] ?? '';
|
||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: nwozruss f8d22bfa "decorelate formats from fichiers: no HTMX toggles, all slots always visible; progress bar 25/75 split; sticky formats fieldset" (rebased revision)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
- $linkName = $link['name'] ?? '';
|
||||
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: mrtoqozz 734f8a46 "server-side upload progress: poll /admin/actions/upload-progress.php via token; progress file written during PeerTube uploads" (rebased revision)
|
||||
$linkName = $link['name'] ?? '';
|
||||
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
$linkLockedYear = $link['locked_year'] ?? null;
|
||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
+\\\\\\\ to: mrtoqozz a1fbaf50 "server-side upload progress: poll /admin/actions/upload-progress.php via token; progress file written during PeerTube uploads" (rebased revision)
|
||||
++ $linkName = $link['name'] ?? '';
|
||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
?>
|
||||
<tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">
|
||||
|
||||
@@ -144,6 +144,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="<?= $formAction ?>" method="post" enctype="multipart/form-data" class="admin-form" data-beforeunload-guard data-upload-progress>
|
||||
<input type="hidden" name="progress_token" value="<?= bin2hex(random_bytes(8)) ?>">
|
||||
<?= $hiddenFields ?>
|
||||
|
||||
<?php if (!$adminMode): ?>
|
||||
@@ -484,10 +485,12 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-footer admin-form-footer">
|
||||
<div id="upload-progress-wrap" style="display:none;margin-bottom:var(--space-s);width:100%;">
|
||||
<progress id="upload-progress-bar" value="0" max="100" style="width:100%;height:1.2rem;"></progress>
|
||||
<span id="upload-progress-text" style="display:block;text-align:center;font-size:var(--step--1);color:var(--text-secondary);margin-top:var(--space-3xs);">0%</span>
|
||||
</div>
|
||||
<fieldset id="upload-progress-wrap" style="display:none;">
|
||||
<legend><span id="upload-progress-label">Téléversement en cours…</span></legend>
|
||||
<progress id="upload-progress-bar" value="0" max="100"></progress>
|
||||
<p id="upload-progress-file" style="font-size:var(--step--1);color:var(--text-secondary);margin:var(--space-2xs) 0 0 0;"></p>
|
||||
<small style="display:block;color:var(--text-tertiary);margin-top:var(--space-2xs);">Cette opération peut prendre plusieurs minutes selon la taille des fichiers. Ne fermez pas la page.</small>
|
||||
</fieldset>
|
||||
<button type="submit" name="go" class="btn btn--primary"><?= $mode === 'edit' ? 'Enregistrer' : 'Soumettre' ?></button>
|
||||
<?php if ($mode === 'add' || $mode === 'edit'): ?>
|
||||
<a href="/admin/" class="btn btn--secondary">Annuler</a>
|
||||
|
||||
@@ -465,7 +465,6 @@
|
||||
$mediaUrl = $isPeerTube ? '' : ($isExternalUrl ? htmlspecialchars($filePath) : ('/media?path=' . urlencode($filePath)));
|
||||
$fileName = htmlspecialchars($file["file_name"] ?? basename($filePath));
|
||||
?>
|
||||
<figure>
|
||||
<?php if ($isPdf): ?>
|
||||
<iframe src="<?= $mediaUrl ?>"
|
||||
width="100%" height="700px"
|
||||
@@ -517,7 +516,7 @@
|
||||
$title = $fileName;
|
||||
$instanceUrl = $_ptInstanceUrl;
|
||||
$width = 560;
|
||||
$height = 80;
|
||||
$height = 150;
|
||||
include APP_ROOT . '/templates/partials/peertube-embed.php';
|
||||
?>
|
||||
<?php else: ?>
|
||||
@@ -548,7 +547,6 @@
|
||||
<?php if ($caption !== '' && !$isOther): ?>
|
||||
<figcaption><?= htmlspecialchars($caption) ?></figcaption>
|
||||
<?php endif; ?>
|
||||
</figure>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<p class="tfe-no-files">Aucun fichier disponible pour ce TFE.</p>
|
||||
|
||||
Reference in New Issue
Block a user