mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
feat: multi-type file upload with sort order, labels, and expanded MIME support
- DB migration 007: add sort_order + display_label to thesis_files - Database: getThesisFiles ordered by sort_order; insertThesisFile accepts label/order; new reorderThesisFiles() and updateThesisFileLabel() methods - ThesisCreateController + ThesisEditController: expand allowed MIME/exts to include audio (mp3/ogg/wav/flac/aac/m4a), video (webm/mov/ogv), image (gif/webp), archives (tar/gz), any-ext via octet-stream; max size raised to 500 MB; accept file_labels[] and file_orders[] POST fields; detectFileType() helper - MediaController: expanded MIME allowlist; HTTP Range support for audio/video; force-download for unknown types; inline for known displayable types - fieldset-files.php: sortable queue UI with SortableJS, per-file labels, 500 MB hint - templates/admin/edit.php: existing files as sortable list with drag handles, type icons, label inputs, delete checkboxes, hidden sort-order fields - file-upload-queue.js: new JS replacing file-preview.js — sortable new-file queue, per-file labels, hidden order fields on submit, backward-compat legacy preview - tfe.php: renders audio (<audio>), all video formats, images, PDF, and download-only 'other' files; reads display_label; sorted by sort_order - tfe.css + form.css: styles for audio player, download files, sortable queue, drag handles, file type badges, label inputs - .htaccess + .user.ini: upload_max_filesize=512M / post_max_size=520M
This commit is contained in:
@@ -94,27 +94,56 @@
|
||||
<?php endif; ?>
|
||||
<input type="file" id="couverture" name="couverture" accept="image/jpeg,image/png" data-preview="fp-couverture">
|
||||
<div id="fp-couverture" class="file-preview-list" aria-live="polite"></div>
|
||||
<small><?= empty($currentCover) ? 'JPG, PNG. Max 10 MB.' : 'Laisser vide pour conserver la couverture actuelle. JPG, PNG. Max 10 MB.' ?></small>
|
||||
<small><?= empty($currentCover) ? 'JPG, PNG. Max 20 MB.' : 'Laisser vide pour conserver la couverture actuelle. JPG, PNG. Max 20 MB.' ?></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing thesis files -->
|
||||
<?php $thesisFilesList = array_filter($currentFiles, fn($f) => $f['file_type'] !== 'cover'); ?>
|
||||
<!-- Existing thesis files — sortable, with labels -->
|
||||
<?php $thesisFilesList = array_values(array_filter($currentFiles, fn($f) => $f['file_type'] !== 'cover')); ?>
|
||||
<?php if (!empty($thesisFilesList)): ?>
|
||||
<div class="admin-form-group">
|
||||
<label>Fichiers du TFE existants :</label>
|
||||
<ul class="admin-file-list">
|
||||
<?php foreach ($thesisFilesList as $f): ?>
|
||||
<li class="admin-file-list-item">
|
||||
<small style="display:block;margin-bottom:var(--space-2xs);color:var(--text-tertiary)">
|
||||
Glissez-déposez les lignes pour réordonner les fichiers sur la page publique.
|
||||
</small>
|
||||
<ul id="existing-files-sortable" class="admin-file-list sortable-list">
|
||||
<?php foreach ($thesisFilesList as $f):
|
||||
$fExt = strtolower(pathinfo($f['file_path'] ?? '', PATHINFO_EXTENSION));
|
||||
$fType = $f['file_type'] ?? 'other';
|
||||
$fIcon = match(true) {
|
||||
$fType === 'main' || $fExt === 'pdf' => '📄',
|
||||
in_array($fExt, ['jpg','jpeg','png','gif','webp']) => '🖼️',
|
||||
$fType === 'video' || in_array($fExt, ['mp4','webm','mov','ogv']) => '🎬',
|
||||
$fType === 'audio' || in_array($fExt, ['mp3','ogg','wav','flac','aac','m4a']) => '🔊',
|
||||
$fType === 'caption' || $fExt === 'vtt' => '💬',
|
||||
default => '📎',
|
||||
};
|
||||
?>
|
||||
<li class="admin-file-list-item" data-file-id="<?= (int)$f['id'] ?>">
|
||||
<!-- Hidden field carries sort order (updated by JS) -->
|
||||
<input type="hidden" name="file_sort_order[]" value="<?= (int)$f['id'] ?>">
|
||||
|
||||
<span class="admin-file-drag-handle" title="Réordonner">⠿</span>
|
||||
|
||||
<span class="admin-file-icon-col"><?= $fIcon ?></span>
|
||||
|
||||
<span class="admin-file-info">
|
||||
<span class="admin-file-type">[<?= htmlspecialchars($f['file_type']) ?>]</span>
|
||||
<a href="/media.php?path=<?= urlencode($f['file_path']) ?>" target="_blank" rel="noopener">
|
||||
<a href="/media.php?path=<?= urlencode($f['file_path']) ?>" target="_blank" rel="noopener" class="admin-file-name">
|
||||
<?= htmlspecialchars($f['file_name'] ?? basename($f['file_path'])) ?>
|
||||
</a>
|
||||
<?php if (!empty($f['file_size'])): ?>
|
||||
<small>(<?= number_format($f['file_size'] / 1024 / 1024, 2) ?> MB)</small>
|
||||
<?php endif; ?>
|
||||
<span class="admin-file-meta-row">
|
||||
<span class="admin-file-type-badge"><?= htmlspecialchars($fType) ?></span>
|
||||
<?php if (!empty($f['file_size'])): ?>
|
||||
<span class="admin-file-size"><?= number_format($f['file_size'] / 1024 / 1024, 2) ?> MB</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<input type="text"
|
||||
name="file_label[<?= (int)$f['id'] ?>]"
|
||||
value="<?= htmlspecialchars($f['display_label'] ?? '') ?>"
|
||||
placeholder="Légende / description (optionnel)"
|
||||
class="admin-file-label-input">
|
||||
</span>
|
||||
|
||||
<label class="admin-checkbox-label admin-file-delete">
|
||||
<input type="checkbox" name="delete_files[]" value="<?= (int)$f['id'] ?>">
|
||||
Supprimer
|
||||
@@ -126,14 +155,18 @@
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- New thesis files -->
|
||||
<div class="admin-form-group">
|
||||
<label for="files">Ajouter des fichiers du TFE :</label>
|
||||
<div class="admin-form-group admin-files-fieldgroup">
|
||||
<label>Ajouter des fichiers du TFE :</label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="files" name="files[]" multiple
|
||||
accept=".pdf,.jpg,.jpeg,.png,.mp4,.zip,.vtt"
|
||||
data-preview="fp-files">
|
||||
<div id="fp-files" class="file-preview-list" aria-live="polite"></div>
|
||||
<small>PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier. Pour les vidéos, un fichier .vtt de sous-titres peut être joint.</small>
|
||||
<input type="file" id="tfe-files-input"
|
||||
name="files[]" multiple
|
||||
accept=".pdf,.jpg,.jpeg,.png,.gif,.webp,.mp4,.webm,.mov,.ogv,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a,.zip,.tar,.gz,.vtt"
|
||||
class="tfe-file-picker">
|
||||
<small class="admin-file-hint">
|
||||
Types acceptés : PDF · JPG/PNG/GIF/WEBP · MP4/WebM/MOV (vidéo) · MP3/OGG/WAV/FLAC (audio) · ZIP/TAR (archives) · autres fichiers (téléchargement uniquement). Max 500 MB par fichier.
|
||||
</small>
|
||||
<ul id="tfe-file-queue" class="tfe-file-queue sortable-list" aria-label="Nouveaux fichiers (réordonnable)"></ul>
|
||||
<p id="tfe-file-queue-empty" class="tfe-queue-empty">Aucun nouveau fichier sélectionné.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -152,7 +185,7 @@
|
||||
<?php endif; ?>
|
||||
<input type="file" name="banner" id="banner" accept="image/jpeg,image/png,image/webp" data-preview="fp-banner">
|
||||
<div id="fp-banner" class="file-preview-list" aria-live="polite"></div>
|
||||
<small><?= empty($thesis['banner_path']) ? 'JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.' : 'Laisser vide pour conserver la bannière actuelle. JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.' ?></small>
|
||||
<small><?= empty($thesis['banner_path']) ? 'JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 20 MB.' : 'Laisser vide pour conserver la bannière actuelle. JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 20 MB.' ?></small>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
/**
|
||||
* Shared partial — "Fichiers" fieldset (add / student submission mode).
|
||||
*
|
||||
* This renders simple upload inputs with no existing-file management (that is
|
||||
* handled by the edit-specific template). For the edit form, include the
|
||||
* edit-specific files section directly in the template instead of this partial.
|
||||
* Renders upload inputs for cover image, banner image, and TFE files.
|
||||
* TFE files support multiple file types (PDF, image, audio, video, other),
|
||||
* drag-to-reorder via SortableJS, and per-file label input.
|
||||
*
|
||||
* For the edit form, the existing-files management is inline in edit.php.
|
||||
*
|
||||
* Variables consumed: none beyond APP_ROOT (always available).
|
||||
*/
|
||||
@@ -12,7 +14,41 @@
|
||||
<fieldset>
|
||||
<legend>Fichiers</legend>
|
||||
|
||||
<?php $name = 'couverture'; $label = 'Image de couverture :'; $accept = 'image/jpeg,image/png'; $hint = 'JPG, PNG. Taille max : 10 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||
<?php $name = 'banner'; $label = 'Image bannière (accueil) :'; $accept = 'image/jpeg,image/png,image/webp'; $hint = 'JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||
<?php $name = 'files'; $label = 'Fichiers du TFE :'; $accept = '.pdf,.jpg,.jpeg,.png,.mp4,.zip,.vtt'; $hint = 'PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier. Pour les vidéos, un fichier .vtt de sous-titres peut être joint (il sera associé automatiquement à la vidéo correspondante).'; $multiple = true; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||
<?php
|
||||
$name = 'couverture';
|
||||
$label = 'Image de couverture :';
|
||||
$accept = 'image/jpeg,image/png';
|
||||
$hint = 'JPG, PNG. Taille max : 20 MB.';
|
||||
include APP_ROOT . '/templates/partials/form/file-field.php';
|
||||
?>
|
||||
|
||||
<?php
|
||||
$name = 'banner';
|
||||
$label = 'Image bannière (accueil) :';
|
||||
$accept = 'image/jpeg,image/png,image/webp';
|
||||
$hint = 'JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 20 MB.';
|
||||
include APP_ROOT . '/templates/partials/form/file-field.php';
|
||||
?>
|
||||
|
||||
<!-- TFE files — multi-file, sortable, with per-file labels -->
|
||||
<div class="admin-form-group admin-files-fieldgroup">
|
||||
<label>Fichiers du TFE :</label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="tfe-files-input"
|
||||
name="files[]" multiple
|
||||
accept=".pdf,.jpg,.jpeg,.png,.gif,.webp,.mp4,.webm,.mov,.ogv,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a,.zip,.tar,.gz,.vtt"
|
||||
class="tfe-file-picker">
|
||||
<small class="admin-file-hint">
|
||||
Types acceptés : PDF · JPG/PNG/GIF/WEBP · MP4/WebM/MOV (vidéo) · MP3/OGG/WAV/FLAC (audio) · ZIP/TAR (archives) · autres fichiers (téléchargement uniquement).
|
||||
Max 500 MB par fichier.
|
||||
Les fichiers <code>.vtt</code> sont des sous-titres et seront associés automatiquement à la vidéo précédente.
|
||||
</small>
|
||||
|
||||
<!-- Sortable file queue — populated by JS -->
|
||||
<ul id="tfe-file-queue" class="tfe-file-queue sortable-list" aria-label="Fichiers sélectionnés (réordonnable)">
|
||||
<!-- Items injected by file-upload-queue.js -->
|
||||
</ul>
|
||||
<p id="tfe-file-queue-empty" class="tfe-queue-empty">Aucun fichier sélectionné.</p>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -371,80 +371,78 @@
|
||||
<?php elseif (!empty($data["files"])): ?>
|
||||
<?php foreach ($data["files"] as $file): ?>
|
||||
<?php
|
||||
$ext = strtolower(
|
||||
pathinfo($file["file_path"], PATHINFO_EXTENSION),
|
||||
);
|
||||
$fileType = $file["file_type"] ?? "";
|
||||
if ($ext === "vtt") {
|
||||
continue;
|
||||
}
|
||||
if ($fileType === "cover") {
|
||||
continue;
|
||||
$ext = strtolower(pathinfo($file["file_path"] ?? '', PATHINFO_EXTENSION));
|
||||
$fileType = $file["file_type"] ?? '';
|
||||
|
||||
// Skip helper/internal types
|
||||
if ($ext === 'vtt' || $fileType === 'caption') continue;
|
||||
if ($fileType === 'cover') continue;
|
||||
|
||||
// Determine display category
|
||||
$isImage = in_array($ext, ['jpg','jpeg','png','gif','bmp','webp'], true) || $fileType === 'image';
|
||||
$isVideo = in_array($ext, ['mp4','webm','mov','ogv'], true) || $fileType === 'video';
|
||||
$isAudio = in_array($ext, ['mp3','ogg','oga','wav','flac','aac','m4a'], true) || $fileType === 'audio';
|
||||
$isPdf = ($ext === 'pdf') || $fileType === 'main';
|
||||
$isOther = !($isImage || $isVideo || $isAudio || $isPdf);
|
||||
|
||||
$_vttPath = null;
|
||||
if ($isVideo) {
|
||||
$_vttPath = $captionFiles[$_videoIndex] ?? null;
|
||||
$_videoIndex++;
|
||||
}
|
||||
|
||||
$caption = !empty($file["display_label"]) ? $file["display_label"] : ($file["description"] ?? '');
|
||||
$mediaUrl = '/media?path=' . urlencode($file["file_path"]);
|
||||
$fileName = htmlspecialchars($file["file_name"] ?? basename($file["file_path"]));
|
||||
?>
|
||||
<figure>
|
||||
<?php if ($ext === "pdf"): ?>
|
||||
<iframe src="/media?path=<?= urlencode(
|
||||
$file["file_path"],
|
||||
) ?>"
|
||||
<?php if ($isPdf): ?>
|
||||
<iframe src="<?= $mediaUrl ?>"
|
||||
width="100%" height="700px"
|
||||
style="border:none"
|
||||
title="<?= htmlspecialchars(
|
||||
$file["original_name"] ??
|
||||
basename($file["file_path"]),
|
||||
) ?>">
|
||||
title="<?= $fileName ?>">
|
||||
</iframe>
|
||||
<p class="tfe-pdf-fallback">
|
||||
<a href="/media?path=<?= urlencode(
|
||||
$file["file_path"],
|
||||
) ?>&download=1">
|
||||
Télécharger le PDF
|
||||
</a>
|
||||
<a href="<?= $mediaUrl ?>&download=1">Télécharger le PDF</a>
|
||||
</p>
|
||||
<?php elseif (
|
||||
in_array($ext, [
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"bmp",
|
||||
"webp",
|
||||
])
|
||||
): ?>
|
||||
<img src="/media?path=<?= urlencode(
|
||||
$file["file_path"],
|
||||
) ?>"
|
||||
alt="<?= htmlspecialchars(
|
||||
!empty($file["description"])
|
||||
? $file["description"]
|
||||
: $data["title"] .
|
||||
" — " .
|
||||
($data["authors"] ?? ""),
|
||||
) ?>">
|
||||
<?php elseif ($ext === "mp4"): ?>
|
||||
<?php
|
||||
$_vttPath = $captionFiles[$_videoIndex] ?? null;
|
||||
$_videoIndex++;
|
||||
?>
|
||||
<?php elseif ($isImage): ?>
|
||||
<img src="<?= $mediaUrl ?>"
|
||||
alt="<?= htmlspecialchars($caption !== '' ? $caption : $data['title'] . ' — ' . ($data['authors'] ?? '')) ?>">
|
||||
<?php elseif ($isVideo): ?>
|
||||
<video width="100%" controls>
|
||||
<source src="/media?path=<?= urlencode(
|
||||
$file["file_path"],
|
||||
) ?>" type="video/mp4">
|
||||
<source src="<?= $mediaUrl ?>" type="video/<?= htmlspecialchars($ext === 'mov' ? 'mp4' : $ext) ?>">
|
||||
<?php if ($_vttPath): ?>
|
||||
<track kind="captions"
|
||||
src="/media?path=<?= urlencode(
|
||||
$_vttPath,
|
||||
) ?>"
|
||||
srclang="fr"
|
||||
label="Sous-titres"
|
||||
default>
|
||||
src="/media?path=<?= urlencode($_vttPath) ?>"
|
||||
srclang="fr" label="Sous-titres" default>
|
||||
<?php endif; ?>
|
||||
</video>
|
||||
<?php elseif ($isAudio): ?>
|
||||
<audio controls class="tfe-audio">
|
||||
<source src="<?= $mediaUrl ?>" type="audio/<?= htmlspecialchars(match($ext) {
|
||||
'mp3' => 'mpeg',
|
||||
'ogg', 'oga' => 'ogg',
|
||||
'wav' => 'wav',
|
||||
'flac' => 'flac',
|
||||
'aac' => 'aac',
|
||||
'm4a' => 'mp4',
|
||||
default => $ext,
|
||||
}) ?>">
|
||||
Votre navigateur ne supporte pas la lecture audio.
|
||||
</audio>
|
||||
<?php else: /* other — download only */ ?>
|
||||
<div class="tfe-download-file">
|
||||
<a href="<?= $mediaUrl ?>&download=1" class="tfe-download-link">
|
||||
<span class="tfe-download-icon">📎</span>
|
||||
<span><?= $fileName ?></span>
|
||||
</a>
|
||||
<?php if (!empty($file['file_size'])): ?>
|
||||
<small class="tfe-download-size"><?= number_format($file['file_size'] / 1024 / 1024, 2) ?> MB</small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($file["description"])): ?>
|
||||
<figcaption><?= htmlspecialchars(
|
||||
$file["description"],
|
||||
) ?></figcaption>
|
||||
<?php if ($caption !== '' && !$isOther): ?>
|
||||
<figcaption><?= htmlspecialchars($caption) ?></figcaption>
|
||||
<?php endif; ?>
|
||||
</figure>
|
||||
<?php endforeach; ?>
|
||||
|
||||
Reference in New Issue
Block a user