diff --git a/app/public/admin/actions/edit.php b/app/public/admin/actions/edit.php index 5de4fce..f5e9bca 100644 --- a/app/public/admin/actions/edit.php +++ b/app/public/admin/actions/edit.php @@ -32,12 +32,7 @@ require_once APP_ROOT . '/src/ErrorHandler.php'; try { $ctrl = ThesisEditController::create(); - $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); + $ctrl->save($thesisId, $_POST, $_FILES); // Regenerate CSRF token after successful save $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); diff --git a/app/public/admin/actions/upload-progress.php b/app/public/admin/actions/upload-progress.php deleted file mode 100644 index b7e6c2d..0000000 --- a/app/public/admin/actions/upload-progress.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * 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'; -// No AdminAuth check here — this endpoint is called by client-side JS during -// both admin and partage (student) form uploads. Access is guarded by the -// progress token (64 bits of entropy, fresh per form render) which must match -// a temp file that only exists during an active upload. - -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); diff --git a/app/public/admin/add.php b/app/public/admin/add.php index ef9e80a..a0e664e 100644 --- a/app/public/admin/add.php +++ b/app/public/admin/add.php @@ -54,7 +54,8 @@ function wasSelected($key, $value) { $isAdmin = true; $bodyClass = 'admin-body'; -$extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; +$extraCss = ['/assets/css/form-base.css']; +$extraCssAdmin = ['/assets/css/form-admin.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; $extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js', '/assets/js/app/beforeunload-guard.js', '/assets/js/app/pill-search.js', '/assets/js/app/jury-autocomplete.js']; require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; diff --git a/app/public/admin/edit.php b/app/public/admin/edit.php index 8e38f4c..97d8a80 100644 --- a/app/public/admin/edit.php +++ b/app/public/admin/edit.php @@ -39,7 +39,8 @@ try { } $isAdmin = true; $bodyClass = 'admin-body'; -$extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; +$extraCss = ['/assets/css/form-base.css']; +$extraCssAdmin = ['/assets/css/form-admin.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; $extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js', '/assets/js/app/beforeunload-guard.js', '/assets/js/app/pill-search.js', '/assets/js/app/jury-autocomplete.js']; require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; diff --git a/app/public/admin/index.php b/app/public/admin/index.php index cdfd5f4..a30c71d 100644 --- a/app/public/admin/index.php +++ b/app/public/admin/index.php @@ -515,7 +515,7 @@ if ($isHtmx) { include APP_ROOT . '/templates/admin/index-table.php'; } } else { - $extraCss = ['/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; + $extraCssAdmin = ['/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; $extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js']; require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; diff --git a/app/public/admin/parametres.php b/app/public/admin/parametres.php index 586e264..8b0bb47 100644 --- a/app/public/admin/parametres.php +++ b/app/public/admin/parametres.php @@ -74,7 +74,7 @@ if (empty($_SESSION['csrf_token'])) { } $isAdmin = true; $bodyClass = 'admin-body'; -$extraCss = ['/assets/css/system.css']; +$extraCssAdmin = ['/assets/css/system.css']; require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/admin/parametres.php'; diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css index 57e7537..aad0f2d 100644 --- a/app/public/assets/css/admin.css +++ b/app/public/assets/css/admin.css @@ -115,7 +115,7 @@ fill: currentColor; } -/* ── Form styles → see form.css ─────────────────────────────────────────── */ +/* ── Form styles → see form-base.css + form-admin.css ───────────────────── */ /* ── Buttons ────────────────────────────────────────────────────────────── */ .admin-form-footer { @@ -763,7 +763,7 @@ th.admin-ap-col { padding: 0; } -/* ── Jury fieldset → see form.css ───────────────────────────────────────── */ +/* ── Jury fieldset → see form-base.css ──────────────────────────────────── */ /* ── Inline form (tags page) ────────────────────────────────────────────── */ .admin-inline-form { @@ -1679,7 +1679,7 @@ th.admin-ap-col { } -/* ── Form group, student mode, thanks page → see form.css ───────────────── */ +/* ── Form group, student mode, thanks page → see form-base.css ──────────── */ /* ── Utility ─────────────────────────────────────────────────────────────── */ diff --git a/app/public/assets/css/form-admin.css b/app/public/assets/css/form-admin.css new file mode 100644 index 0000000..a583011 --- /dev/null +++ b/app/public/assets/css/form-admin.css @@ -0,0 +1,234 @@ +/* ============================================================ + FORM — Admin-only extensions + Styles used exclusively by admin pages (not the student partage form). + Loaded after form-base.css. + ============================================================ */ + +/* ── Recap files table ─────────────────────────────────────────── */ + +.recap-files-table { + width: 100%; + border-collapse: collapse; + font-size: var(--step--1); +} + +.recap-files-table thead th { + text-align: left; + font-weight: 600; + color: var(--text-secondary); + font-size: var(--step--2); + text-transform: uppercase; + letter-spacing: 0.05em; + padding: var(--space-xs) var(--space-s); + border-bottom: 2px solid var(--border-primary); +} + +.recap-files-table tbody td { + padding: var(--space-xs) var(--space-s); + border-bottom: 1px solid var(--border-secondary); + vertical-align: middle; +} + +.recap-files-table tbody tr:last-child td { + border-bottom: none; +} + +.recap-files-icon { + width: 1px; + white-space: nowrap; +} + +.recap-files-icon-emoji { + font-size: 1.3rem; + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.2rem; + height: 2.2rem; + background: var(--accent-muted); + border-radius: var(--radius); +} + +.recap-files-thumb { + max-width: 80px; + max-height: 60px; + object-fit: cover; + border-radius: var(--radius); +} + +.recap-files-name { + font-weight: 500; +} + +.recap-files-name a { + color: var(--text-primary); + text-decoration: none; +} + +.recap-files-name a:hover { + color: var(--accent-primary); +} + +.recap-files-peertube-id { + display: block; + font-size: var(--step--2); + color: var(--text-tertiary); + font-family: monospace; +} + +.recap-files-label { + display: block; + font-size: var(--step--2); + color: var(--text-secondary); + font-style: italic; +} + +.recap-files-type { + font-size: var(--step--2); + color: var(--text-tertiary); + white-space: nowrap; +} + +.recap-files-size { + font-size: var(--step--2); + color: var(--text-tertiary); + white-space: nowrap; + font-variant-numeric: tabular-nums; +} + +.recap-files-date { + font-size: var(--step--2); + color: var(--text-tertiary); + white-space: nowrap; +} + +/* ── File browser (relink) ──────────────────────────────────────── */ + +.file-browser-trigger { + margin-top: var(--space-2xs); + font-size: var(--step--2); +} + +.relink-modal { + width: min(90vw, 700px); + max-height: 80vh; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-primary); + padding: 0; + overflow: hidden; + color: var(--text-primary); +} +.relink-modal::backdrop { + background: rgba(0,0,0,0.5); +} +.relink-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-s) var(--space-m); + border-bottom: 1px solid var(--border); + background: var(--bg-secondary); +} +.relink-modal-header h3 { + margin: 0; + font-size: var(--step-0); +} +.relink-modal-footer { + padding: var(--space-xs) var(--space-m); + border-top: 1px solid var(--border); + background: var(--bg-secondary); +} +#relink-modal-body { + padding: var(--space-s) var(--space-m); + overflow-y: auto; + max-height: 60vh; +} + +/* ── File browser tree ─────────────────────────────────────────── */ + +.file-browser { + font-size: var(--step--1); +} +.file-browser-hint, +.file-browser-empty, +.file-browser-loading, +.file-browser-error { + color: var(--text-secondary); + text-align: center; + padding: var(--space-m); +} +.file-browser-error { + color: var(--error); +} +.file-browser-breadcrumb { + display: flex; + align-items: center; + gap: var(--space-2xs); + flex-wrap: wrap; + padding-bottom: var(--space-s); + margin-bottom: var(--space-s); + border-bottom: 1px solid var(--border); + font-size: var(--step--2); +} +.file-browser-breadcrumb a { + color: var(--accent-blue, var(--link)); + text-decoration: none; +} +.file-browser-breadcrumb a:hover { + color: var(--accent-primary); +} +.file-browser-sep { + color: var(--text-tertiary); +} + +.file-browser-list { + list-style: none; + margin: 0; + padding: 0; +} +.file-browser-entry { + border-bottom: 1px solid var(--border-subtle); +} +.file-browser-entry a, +.file-browser-select-btn { + display: flex; + align-items: center; + gap: var(--space-xs); + width: 100%; + padding: var(--space-xs) var(--space-2xs); + text-decoration: none; + color: var(--text-primary); + background: none; + border: none; + cursor: pointer; + font-size: var(--step--1); + text-align: left; + transition: background 0.15s; +} +.file-browser-entry a:hover, +.file-browser-select-btn:hover { + background: var(--bg-secondary); +} +.file-browser-icon { + flex-shrink: 0; + font-size: var(--step-0); +} +.file-browser-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.file-browser-size { + flex-shrink: 0; + color: var(--text-tertiary); + font-size: var(--step--2); + font-variant-numeric: tabular-nums; +} +.file-browser-file .file-browser-select-btn { + color: var(--accent-green, var(--success)); +} +.file-browser-file .file-browser-select-btn:hover { + background: var(--bg-secondary); +} diff --git a/app/public/assets/css/form.css b/app/public/assets/css/form-base.css similarity index 79% rename from app/public/assets/css/form.css rename to app/public/assets/css/form-base.css index 6cdf86c..4cd77ff 100644 --- a/app/public/assets/css/form.css +++ b/app/public/assets/css/form-base.css @@ -1,6 +1,7 @@ /* ============================================================ - FORM — TFE submission form + FORM — TFE submission form (base styles) Shared between admin add/edit pages and the student partage form. + Admin-only extensions live in form-admin.css. Variables loaded via style.css (colors.css + typography.css). ============================================================ */ @@ -1156,61 +1157,7 @@ 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 ──────────────────────────────── */ @@ -1266,324 +1213,6 @@ legend { padding: 0 var(--space-2xs); } -/* ── File figures (recap + edit existing files) ──────────── */ - -.admin-file-list, -.recap-files-list { - list-style: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - gap: var(--space-s); -} - -.admin-file-list-item, -.recap-files-list-item { - list-style: none; - display: flex; - align-items: center; - gap: var(--space-s); -} - -.admin-file-figure { - display: flex; - align-items: flex-start; - gap: var(--space-s); - background: var(--bg-secondary); - border-radius: var(--radius); - padding: var(--space-s); - margin: 0; - flex: 1; -} - -.admin-file-icon { - font-size: 1.5rem; - flex-shrink: 0; - width: 2.5rem; - height: 2.5rem; - display: flex; - align-items: center; - justify-content: center; - background: var(--accent-muted); - border-radius: var(--radius); -} - -.admin-file-thumb { - max-width: 120px; - max-height: 90px; - object-fit: cover; - border-radius: var(--radius); - flex-shrink: 0; -} - -.admin-file-caption { - flex: 1; - display: flex; - flex-direction: column; - gap: var(--space-3xs); -} - -.admin-file-name-row { - display: flex; - align-items: center; - gap: var(--space-2xs); -} - -.admin-file-name { - font-weight: 600; - font-size: var(--step--1); - color: var(--text-primary); -} - -.admin-file-peertube-id { - font-size: var(--step--2); - color: var(--text-tertiary); - font-family: monospace; -} - -.admin-file-meta-row { - display: flex; - gap: var(--space-xs); - align-items: center; - font-size: var(--step--2); - color: var(--text-tertiary); -} - -.admin-file-label { - font-size: var(--step--1); - color: var(--text-secondary); - font-style: italic; -} - -/* ── Recap files table ─────────────────────────────────────────── */ - -.recap-files-table { - width: 100%; - border-collapse: collapse; - font-size: var(--step--1); -} - -.recap-files-table thead th { - text-align: left; - font-weight: 600; - color: var(--text-secondary); - font-size: var(--step--2); - text-transform: uppercase; - letter-spacing: 0.05em; - padding: var(--space-xs) var(--space-s); - border-bottom: 2px solid var(--border-primary); -} - -.recap-files-table tbody td { - padding: var(--space-xs) var(--space-s); - border-bottom: 1px solid var(--border-secondary); - vertical-align: middle; -} - -.recap-files-table tbody tr:last-child td { - border-bottom: none; -} - -.recap-files-icon { - width: 1px; - white-space: nowrap; -} - -.recap-files-icon-emoji { - font-size: 1.3rem; - display: inline-flex; - align-items: center; - justify-content: center; - width: 2.2rem; - height: 2.2rem; - background: var(--accent-muted); - border-radius: var(--radius); -} - -.recap-files-thumb { - max-width: 80px; - max-height: 60px; - object-fit: cover; - border-radius: var(--radius); -} - -.recap-files-name { - font-weight: 500; -} - -.recap-files-name a { - color: var(--text-primary); - text-decoration: none; -} - -.recap-files-name a:hover { - color: var(--accent-primary); -} - -.recap-files-peertube-id { - display: block; - font-size: var(--step--2); - color: var(--text-tertiary); - font-family: monospace; -} - -.recap-files-label { - display: block; - font-size: var(--step--2); - color: var(--text-secondary); - font-style: italic; -} - -.recap-files-type { - font-size: var(--step--2); - color: var(--text-tertiary); - white-space: nowrap; -} - -.recap-files-size { - font-size: var(--step--2); - color: var(--text-tertiary); - white-space: nowrap; - font-variant-numeric: tabular-nums; -} - -.recap-files-date { - font-size: var(--step--2); - color: var(--text-tertiary); - white-space: nowrap; -} - -/* ── File browser (relink) ──────────────────────────────────────── */ - -.file-browser-trigger { - margin-top: var(--space-2xs); - font-size: var(--step--2); -} - -.relink-modal { - width: min(90vw, 700px); - max-height: 80vh; - border: 1px solid var(--border); - border-radius: var(--radius); - background: var(--bg-primary); - padding: 0; - overflow: hidden; - color: var(--text-primary); -} -.relink-modal::backdrop { - background: rgba(0,0,0,0.5); -} -.relink-modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-s) var(--space-m); - border-bottom: 1px solid var(--border); - background: var(--bg-secondary); -} -.relink-modal-header h3 { - margin: 0; - font-size: var(--step-0); -} -.relink-modal-footer { - padding: var(--space-xs) var(--space-m); - border-top: 1px solid var(--border); - background: var(--bg-secondary); -} -#relink-modal-body { - padding: var(--space-s) var(--space-m); - overflow-y: auto; - max-height: 60vh; -} - -/* ── File browser tree ─────────────────────────────────────────── */ - -.file-browser { - font-size: var(--step--1); -} -.file-browser-hint, -.file-browser-empty, -.file-browser-loading, -.file-browser-error { - color: var(--text-secondary); - text-align: center; - padding: var(--space-m); -} -.file-browser-error { - color: var(--error); -} -.file-browser-breadcrumb { - display: flex; - align-items: center; - gap: var(--space-2xs); - flex-wrap: wrap; - padding-bottom: var(--space-s); - margin-bottom: var(--space-s); - border-bottom: 1px solid var(--border); - font-size: var(--step--2); -} -.file-browser-breadcrumb a { - color: var(--accent-blue, var(--link)); - text-decoration: none; -} -.file-browser-breadcrumb a:hover { - color: var(--accent-primary); -} -.file-browser-sep { - color: var(--text-tertiary); -} - -.file-browser-list { - list-style: none; - margin: 0; - padding: 0; -} -.file-browser-entry { - border-bottom: 1px solid var(--border-subtle); -} -.file-browser-entry a, -.file-browser-select-btn { - display: flex; - align-items: center; - gap: var(--space-xs); - width: 100%; - padding: var(--space-xs) var(--space-2xs); - text-decoration: none; - color: var(--text-primary); - background: none; - border: none; - cursor: pointer; - font-size: var(--step--1); - text-align: left; - transition: background 0.15s; -} -.file-browser-entry a:hover, -.file-browser-select-btn:hover { - background: var(--bg-secondary); -} -.file-browser-icon { - flex-shrink: 0; - font-size: var(--step-0); -} -.file-browser-name { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.file-browser-size { - flex-shrink: 0; - color: var(--text-tertiary); - font-size: var(--step--2); - font-variant-numeric: tabular-nums; -} -.file-browser-file .file-browser-select-btn { - color: var(--accent-green, var(--success)); -} -.file-browser-file .file-browser-select-btn:hover { - background: var(--bg-secondary); -} /* ── Mobile-responsive layout ──────────────────────────────────────────── */ /* Below 600px: labels stack above inputs, single-column form layout. diff --git a/app/public/assets/js/app/upload-progress.js b/app/public/assets/js/app/upload-progress.js deleted file mode 100644 index 1488d24..0000000 --- a/app/public/assets/js/app/upload-progress.js +++ /dev/null @@ -1,221 +0,0 @@ -/** - * upload-progress.js - * - * 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). - * - * 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 - */ -(() => { - 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 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; - - function collectFileNames() { - const names = []; - // Check raw elements (non-FilePond) - 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); - } - } - } - // Read processed file names from FilePond instances (async mode) - if (typeof FilePond !== "undefined") { - const pondInputs = form.querySelectorAll(".tfe-file-picker"); - for (const pi of pondInputs) { - const pond = FilePond.find(pi); - if (pond) { - const pondFiles = pond.getFiles(); - for (const pf of pondFiles) { - // Only count successfully uploaded files (have serverId) - if (pf.serverId) { - const name = pf.filename || pf.file?.name || pf.serverId; - if (name) names.push(name); - } - } - } - } - } - return names; - } - - form.addEventListener("submit", (e) => { - // ── Guard: block submit if any FilePond item is still uploading ── - if (typeof FilePond !== "undefined") { - let stillUploading = false; - const pondInputs = form.querySelectorAll(".tfe-file-picker"); - for (const pi of pondInputs) { - const pond = FilePond.find(pi); - if (pond) { - const pondFiles = pond.getFiles(); - for (const pf of pondFiles) { - if ( - pf.status === FilePond.FileStatus.PROCESSING || - pf.status === FilePond.FileStatus.IDLE - ) { - stillUploading = true; - break; - } - } - } - if (stillUploading) break; - } - if (stillUploading) { - e.preventDefault(); - progressLabel.textContent = - "Veuillez attendre la fin du téléversement…"; - progressWrap.style.display = ""; - return; - } - } - - const fileNames = collectFileNames(); - if (!fileNames.length) return; - - e.preventDefault(); - - const token = tokenInput ? tokenInput.value : ""; - - progressWrap.style.display = ""; - progressBar.value = 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; - - const fd = new FormData(form); - 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(() => { - fetch( - "/admin/actions/upload-progress.php?token=" + - encodeURIComponent(token), - ) - .then((r) => r.json()) - .then((data) => { - if (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(() => { - /* 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", (evt) => { - if (evt.lengthComputable) { - 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.upload.addEventListener("loadend", () => { - _uploadDone = true; - progressBar.value = UPLOAD_CAP; - startPolling(); - }); - - // ── Response handling ── - xhr.addEventListener("readystatechange", () => { - if (xhr.readyState !== XMLHttpRequest.DONE) return; - - stopPolling(); - - if (xhr.status >= 200 && xhr.status < 300) { - finishSuccess(); - - setTimeout(() => { - 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 { - progressLabel.textContent = "Erreur"; - progressFile.textContent = "Échec du téléversement"; - document.open(); - document.write(xhr.responseText); - document.close(); - } - }); - - xhr.addEventListener("error", () => { - stopPolling(); - progressLabel.textContent = "Erreur réseau"; - progressFile.textContent = ""; - if (submitBtn) submitBtn.disabled = false; - }); - - xhr.addEventListener("abort", () => { - stopPolling(); - progressWrap.style.display = "none"; - if (submitBtn) submitBtn.disabled = false; - }); - - xhr.open("POST", form.action, true); - xhr.send(fd); - }); - } -})(); diff --git a/app/public/partage/recapitulatif.php b/app/public/partage/recapitulatif.php index 7cd9332..b0ea3a8 100644 --- a/app/public/partage/recapitulatif.php +++ b/app/public/partage/recapitulatif.php @@ -58,7 +58,7 @@ $pageTitle = 'Merci — TFE enregistré'; - +
diff --git a/app/public/partage/retry-email.php b/app/public/partage/retry-email.php index 6d544cc..a8ae389 100644 --- a/app/public/partage/retry-email.php +++ b/app/public/partage/retry-email.php @@ -71,7 +71,7 @@ $pageTitle = 'Corriger l\'adresse e-mail'; - +
diff --git a/app/src/Controllers/ThesisEditController.php b/app/src/Controllers/ThesisEditController.php index a304b8c..2703438 100644 --- a/app/src/Controllers/ThesisEditController.php +++ b/app/src/Controllers/ThesisEditController.php @@ -163,7 +163,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, ?string $progressToken = null): void + public function save(int $thesisId, array $post, array $files): void { if ($thesisId <= 0) { throw new InvalidArgumentException('ID de TFE invalide.'); diff --git a/app/src/PeerTubeService.php b/app/src/PeerTubeService.php index c0e008b..1a52a09 100644 --- a/app/src/PeerTubeService.php +++ b/app/src/PeerTubeService.php @@ -455,30 +455,4 @@ class PeerTubeService throw new \RuntimeException('Erreur réseau PeerTube : ' . $e->getMessage(), 0, $e); } } - - // ------------------------------------------------------------------------- - // 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); - } } diff --git a/app/templates/head.php b/app/templates/head.php index 1db35b5..e0bea35 100644 --- a/app/templates/head.php +++ b/app/templates/head.php @@ -7,7 +7,7 @@ // Admin: append suffix to title and prepend admin.css if (!empty($isAdmin)) { $pageTitle = isset($pageTitle) ? $pageTitle . ' – Admin' : 'Admin'; - $extraCss = array_merge(['/assets/css/admin.css'], $extraCss ?? []); + $extraCss = array_merge(['/assets/css/admin.css'], $extraCssAdmin ?? [], $extraCss ?? []); } ?> <?= htmlspecialchars($pageTitle ?? 'XAMXAM') ?>