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:
Pontoporeia
2026-04-30 13:07:09 +02:00
parent 2188ff5479
commit a83dc1c74e
17 changed files with 1026 additions and 274 deletions

View File

@@ -188,10 +188,11 @@ class Database {
}
/**
* Get files associated with a thesis
* Get files associated with a thesis, ordered by sort_order then upload time.
* Covers the new sort_order column added in migration 007.
*/
public function getThesisFiles($thesisId) {
$sql = "SELECT * FROM thesis_files WHERE thesis_id = :thesis_id ORDER BY file_type, uploaded_at";
$sql = "SELECT * FROM thesis_files WHERE thesis_id = :thesis_id ORDER BY sort_order ASC, uploaded_at ASC";
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(':thesis_id', $thesisId, PDO::PARAM_INT);
$stmt->execute();
@@ -1733,17 +1734,48 @@ class Database {
}
/**
* Insert a thesis file record
* Insert a thesis file record.
* sort_order defaults to (max existing sort_order + 1) for the thesis.
*/
public function insertThesisFile($thesisId, $fileType, $filePath, $fileName, $fileSize, $mimeType) {
public function insertThesisFile($thesisId, $fileType, $filePath, $fileName, $fileSize, $mimeType, ?string $displayLabel = null, ?int $sortOrder = null) {
if ($sortOrder === null) {
$maxStmt = $this->pdo->prepare(
"SELECT COALESCE(MAX(sort_order), 0) FROM thesis_files WHERE thesis_id = ?"
);
$maxStmt->execute([$thesisId]);
$sortOrder = (int)$maxStmt->fetchColumn() + 1;
}
$stmt = $this->pdo->prepare("
INSERT INTO thesis_files (thesis_id, file_type, file_path, file_name, file_size, mime_type)
VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO thesis_files (thesis_id, file_type, file_path, file_name, file_size, mime_type, display_label, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([$thesisId, $fileType, $filePath, $fileName, $fileSize, $mimeType]);
$stmt->execute([$thesisId, $fileType, $filePath, $fileName, $fileSize, $mimeType, $displayLabel, $sortOrder]);
return $this->pdo->lastInsertId();
}
/**
* Persist a new sort order for thesis files.
* $order is an array of file IDs in the desired order.
* Only files belonging to $thesisId are updated (safety guard).
*/
public function reorderThesisFiles(int $thesisId, array $order): void {
$stmt = $this->pdo->prepare(
"UPDATE thesis_files SET sort_order = ? WHERE id = ? AND thesis_id = ?"
);
foreach ($order as $i => $fileId) {
$stmt->execute([$i + 1, (int)$fileId, $thesisId]);
}
}
/**
* Update the display_label for a thesis file.
*/
public function updateThesisFileLabel(int $fileId, int $thesisId, ?string $label): void {
$this->pdo->prepare(
"UPDATE thesis_files SET display_label = ? WHERE id = ? AND thesis_id = ?"
)->execute([$label ?: null, $fileId, $thesisId]);
}
/**
* Delete a single thesis file record by its ID and optionally remove the
* file from disk. Returns the file_path that was deleted (or null if not