From 59eb70dea96b329325e10292dfc5a4fb19c7d499 Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Mon, 22 Dec 2025 16:17:31 -0500 Subject: [PATCH] Player text visibility option (#55) * feat: Add player admin UI settings Co-authored-by: mathieumoran1 * Refactor player UI settings to combine source and progress Co-authored-by: mathieumoran1 * Refactor admin settings to use accordion layout Co-authored-by: mathieumoran1 --------- Co-authored-by: Cursor Agent --- public/control.html | 129 ++++++++++++++++++++++++++------------- public/js/app_control.js | 106 ++++++++++++++++++++++++++++++++ public/js/app_player.js | 31 ++++++++-- routes/settings.js | 53 ++++++++++++++++ 4 files changed, 273 insertions(+), 46 deletions(-) diff --git a/public/control.html b/public/control.html index b38825a..a72c8b5 100644 --- a/public/control.html +++ b/public/control.html @@ -102,51 +102,96 @@
-
-
-

Année active

-

- Cette année pilote tout le site (films affichés, checklist et statistiques). -

-
- - -
La liste est basée sur les années présentes dans les films.
-
-
- -
-
-
- -
-
-

Date des Oscars (par année)

-

- Configure la date exacte de la cérémonie pour le compte à rebours sur Films & Checklist. - Si non défini, la date par défaut reste 15 mars (comportement actuel). -

- -
-
- - -
Basé sur les années présentes dans les films.
-
-
- - -
Format: YYYY-MM-DD.
-
-
- - +
+
+ +
+

+ +

+
+
+
+ Pilote tout le site (films, checklist, statistiques). +
+
+
+ + +
Basé sur les années présentes dans les films.
+
+
+ +
+
+
-
-
Dates configurées
-
—
+ +
+

+ +

+
+
+
+
+ + +
+
+ + +
+
+
+ Contrôle l’affichage du bloc debug dans player.html (admins uniquement). +
+
+
+
+ + +
+

+ +

+
+
+
+ Configure la date de cérémonie pour le compte à rebours (Films & Checklist). Défaut: 15 mars. +
+ +
+
+ + +
Basé sur les années présentes dans les films.
+
+
+ + +
Format: YYYY-MM-DD.
+
+
+ + +
+
+ +
+
Dates configurées
+
—
+
+
+
diff --git a/public/js/app_control.js b/public/js/app_control.js index 11c1ffe..b5ade3a 100644 --- a/public/js/app_control.js +++ b/public/js/app_control.js @@ -41,6 +41,11 @@ window.onload = async function () { const activeYearInput = document.getElementById('active_oscar_year'); const saveActiveYearBtn = document.getElementById('save-active-year'); + // Player UI (settings tab): show/hide admin status blocks in player.html + const playerUiShowStatusEl = document.getElementById('player_ui_show_status'); + const playerUiReloadBtn = document.getElementById('player_ui_reload'); + const playerUiSaveBtn = document.getElementById('player_ui_save'); + // Version control tab elements const adminVersionTabBtn = document.getElementById('admin-version-tab'); const appVersionAlertEl = document.getElementById('app_version_alert'); @@ -132,6 +137,11 @@ window.onload = async function () { bodyHtml: '', }); + const DEFAULT_PLAYER_UI = Object.freeze({ + showSource: true, + showProgress: true, + }); + function showResponse(kind, message) { responseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning'); responseEl.classList.add(kind === 'success' ? 'alert-success' : kind === 'warning' ? 'alert-warning' : 'alert-danger'); @@ -147,6 +157,67 @@ window.onload = async function () { .replace(/'/g, '''); } + function normalizePlayerUi(value) { + const v = value && typeof value === 'object' && !Array.isArray(value) ? value : {}; + const showSource = v.showSource === undefined ? DEFAULT_PLAYER_UI.showSource : !!v.showSource; + const showProgress = v.showProgress === undefined ? DEFAULT_PLAYER_UI.showProgress : !!v.showProgress; + return { showSource, showProgress }; + } + + async function fetchPlayerUi() { + const res = await fetch('/api/settings/player-admin-status-ui', { method: 'GET' }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + const msg = data.message || data.error || `Erreur (${res.status})`; + throw new Error(msg); + } + return normalizePlayerUi(data); + } + + async function savePlayerUi(payload) { + const res = await fetch('/api/settings/player-admin-status-ui', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify(payload), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + const msg = data.message || data.error || `Erreur (${res.status})`; + throw new Error(msg); + } + return normalizePlayerUi(data); + } + + function setPlayerUiForm(value) { + if (!playerUiShowStatusEl) return; + // Single checkbox controls both flags. + playerUiShowStatusEl.checked = !!value.showSource && !!value.showProgress; + } + + function getPlayerUiFormValue() { + const enabled = !!playerUiShowStatusEl?.checked; + return normalizePlayerUi({ + showSource: enabled, + showProgress: enabled, + }); + } + + async function loadPlayerUiIntoUi() { + // If the section isn't present (older HTML), skip gracefully. + if (!playerUiShowStatusEl) return; + try { + const value = await fetchPlayerUi(); + setPlayerUiForm(value); + } catch (err) { + // Keep defaults so admin can still set something. + setPlayerUiForm(DEFAULT_PLAYER_UI); + showResponse('warning', err.message || 'Impossible de charger les réglages du lecteur.'); + } + } + async function copyToClipboard(text) { const t = String(text || '').trim(); if (!t) return false; @@ -2337,6 +2408,41 @@ window.onload = async function () { hookModal100Inputs(); await loadCompletionModalIntoUi(); + await loadPlayerUiIntoUi(); + + if (playerUiReloadBtn) { + playerUiReloadBtn.addEventListener('click', async () => { + playerUiReloadBtn.disabled = true; + const old = playerUiReloadBtn.textContent; + playerUiReloadBtn.textContent = 'Chargement...'; + try { + await loadPlayerUiIntoUi(); + showResponse('success', 'Réglages du lecteur rechargés.'); + } finally { + playerUiReloadBtn.disabled = false; + playerUiReloadBtn.textContent = old; + } + }); + } + + if (playerUiSaveBtn) { + playerUiSaveBtn.addEventListener('click', async () => { + playerUiSaveBtn.disabled = true; + const old = playerUiSaveBtn.textContent; + playerUiSaveBtn.textContent = 'Enregistrement...'; + try { + const payload = getPlayerUiFormValue(); + const saved = await savePlayerUi(payload); + setPlayerUiForm(saved); + showResponse('success', 'Réglages du lecteur enregistrés.'); + } catch (err) { + showResponse('danger', err.message || 'Erreur réseau'); + } finally { + playerUiSaveBtn.disabled = false; + playerUiSaveBtn.textContent = old; + } + }); + } if (saveActiveYearBtn && activeYearInput) { saveActiveYearBtn.addEventListener('click', async () => { diff --git a/public/js/app_player.js b/public/js/app_player.js index de77bbc..99d9381 100644 --- a/public/js/app_player.js +++ b/public/js/app_player.js @@ -139,6 +139,20 @@ async function fetchActiveOscarYear() { } } +async function fetchPlayerAdminStatusUi() { + try { + const res = await fetch('/api/settings/player-admin-status-ui', { method: 'GET' }); + if (!res.ok) return { showSource: true, showProgress: true }; + const data = await res.json().catch(() => ({})); + return { + showSource: data?.showSource === undefined ? true : !!data.showSource, + showProgress: data?.showProgress === undefined ? true : !!data.showProgress, + }; + } catch (_) { + return { showSource: true, showProgress: true }; + } +} + async function fetchPlaybackProgress(movieId, token) { try { const res = await fetch(`/api/movies/${encodeURIComponent(movieId)}/progress`, { @@ -868,8 +882,9 @@ window.onload = async function () { } pageLoader.setProgress(14); - // Load the active Oscar year in parallel with the movie fetch. + // Load the active Oscar year (and player UI flags) in parallel with the movie fetch. const activeYearPromise = fetchActiveOscarYear(); + const playerUiPromise = fetchPlayerAdminStatusUi(); let decoded = null; try { @@ -889,8 +904,6 @@ window.onload = async function () { if (decoded.admin) { const adminLink = document.getElementById('admin-control-link'); if (adminLink) adminLink.classList.remove('d-none'); - const adminStatus = document.getElementById('player-admin-status'); - if (adminStatus) adminStatus.classList.remove('d-none'); } const logoffBtn = document.getElementById('log-off'); @@ -927,10 +940,20 @@ window.onload = async function () { return; } - const activeYear = await activeYearPromise; + const [activeYear, playerUi] = await Promise.all([activeYearPromise, playerUiPromise]); setHeader(movie, { activeYear }); pageLoader.setProgress(58); + // Admin-only status block (Source + Progress) can be globally disabled via settings. + if (decoded?.admin) { + const enabled = !!playerUi?.showSource && !!playerUi?.showProgress; + const adminStatus = document.getElementById('player-admin-status'); + if (adminStatus) { + if (enabled) adminStatus.classList.remove('d-none'); + else adminStatus.classList.add('d-none'); + } + } + const resolved = resolveMovieSource(movie); if (!resolved?.src) { setSourceLabel('—'); diff --git a/routes/settings.js b/routes/settings.js index 4d39c99..386e1ad 100644 --- a/routes/settings.js +++ b/routes/settings.js @@ -13,6 +13,7 @@ const COMPLETION_MODAL_KEY = "completion_modal_content"; const WINNERS_BY_YEAR_KEY = "winners_by_year"; const OSCAR_DATES_BY_YEAR_KEY = "oscar_date_by_year"; const APP_VERSION_KEY = "app_version_control"; +const PLAYER_ADMIN_STATUS_UI_KEY = "player_admin_status_ui"; const crypto = require("crypto"); @@ -23,6 +24,12 @@ const DEFAULT_COMPLETION_MODAL = Object.freeze({ bodyHtml: "", }); +const DEFAULT_PLAYER_ADMIN_STATUS_UI = Object.freeze({ + // Controls the admin-only debug/status block in player.html + showSource: true, + showProgress: true, +}); + function parseOscarYear(raw) { if (raw === undefined || raw === null || raw === "") return null; const n = Number(raw); @@ -146,6 +153,13 @@ function normalizeCompletionModal(rawValue) { }; } +function normalizePlayerAdminStatusUi(rawValue) { + const v = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue) ? rawValue : {}; + const showSource = v.showSource === undefined ? DEFAULT_PLAYER_ADMIN_STATUS_UI.showSource : !!v.showSource; + const showProgress = v.showProgress === undefined ? DEFAULT_PLAYER_ADMIN_STATUS_UI.showProgress : !!v.showProgress; + return { showSource, showProgress }; +} + function createId() { try { // Node 16+ / 18+: randomUUID @@ -431,6 +445,45 @@ router.get("/app-version", async (req, res) => { } }); +// Public: get player admin status UI flags (source/progress visibility) +router.get("/player-admin-status-ui", async (req, res) => { + try { + const existing = await Setting.findOne({ key: PLAYER_ADMIN_STATUS_UI_KEY }).select("value"); + const normalized = normalizePlayerAdminStatusUi(existing?.value); + return res.status(200).json(normalized); + } catch (_) { + return res.status(200).json(DEFAULT_PLAYER_ADMIN_STATUS_UI); + } +}); + +// Admin-only: update player admin status UI flags +// Body: { showSource?: boolean, showProgress?: boolean } +router.put("/player-admin-status-ui", async (req, res) => { + const token = req.headers.authorization?.split(" ")[1]; + if (!token) return res.status(401).json({ message: "Authentication token is required" }); + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + if (!decoded.admin) { + return res.status(403).json({ message: "You do not have admin privileges" }); + } + + const normalized = normalizePlayerAdminStatusUi(req.body); + const updated = await Setting.findOneAndUpdate( + { key: PLAYER_ADMIN_STATUS_UI_KEY }, + { $set: { value: normalized } }, + { upsert: true, new: true } + ).select("value"); + + return res.status(200).json(normalizePlayerAdminStatusUi(updated?.value)); + } catch (err) { + if (err instanceof jwt.JsonWebTokenError) { + return res.status(401).json({ message: "Invalid or expired token" }); + } + return res.status(500).json({ error: err.message }); + } +}); + // Admin-only: create a new version entry (optionally activates if none or if activate=true) // Body: { version: "1.2.3", message?: "..." } router.post("/app-version", async (req, res) => {