From c3ab04f805c4d98573a12bf60188f6110357639f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Dec 2025 10:19:35 +0000 Subject: [PATCH 1/5] Refactor checklist styling and structure Co-authored-by: mathieumoran1 --- public/checklist.html | 56 +++++++++++++++++++++++--------------- public/js/app_checklist.js | 19 +++++++------ 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/public/checklist.html b/public/checklist.html index ef66fbf..2233d28 100644 --- a/public/checklist.html +++ b/public/checklist.html @@ -44,11 +44,6 @@ .navbar-toggler-icon { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3E%3Cpath stroke='white' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); } - /* New styles for table */ - .table-hover tbody tr:hover { - background-color: #f1f1f1; - cursor: pointer; - } @@ -90,25 +85,42 @@

Checklist

-
-

Temps avant les Oscars (— —) -

-

-
-
+ + +
+
+
+
—
+
+ Temps avant les Oscars (— —) — — +
+
+
+ +
+
+
+
+ - - - - - - - - - - - -
VuTitreDate
+
+ + + + + + + + + + + + + +
VuTitreDate
Chargement…
+
+

diff --git a/public/js/app_checklist.js b/public/js/app_checklist.js index 0c74d9c..5823cdb 100644 --- a/public/js/app_checklist.js +++ b/public/js/app_checklist.js @@ -453,12 +453,9 @@ window.addEventListener('DOMContentLoaded', async function () { ${watchedDate} `; - // Set initial row color based on watched state - if (watchedMovie) { - movieRow.style.backgroundColor = 'rgba(144, 238, 144, 0.6)'; // Lighter light green with transparency - } else { - movieRow.style.backgroundColor = 'rgba(255, 99, 71, 0.6)'; // Lighter light red with transparency - } + // Use Bootstrap table state classes (matches Stats page styling better than custom backgrounds) + movieRow.classList.toggle('table-success', Boolean(watchedMovie)); + movieRow.classList.toggle('table-danger', !watchedMovie); movieTableBody.appendChild(movieRow); @@ -478,10 +475,16 @@ window.addEventListener('DOMContentLoaded', async function () { if (isChecked) { watchedDateCell.textContent = new Date().toLocaleString(); - row.style.backgroundColor = 'rgba(144, 238, 144, 0.6)'; // Apply light green with transparency + if (row) { + row.classList.add('table-success'); + row.classList.remove('table-danger'); + } } else { watchedDateCell.textContent = ''; - row.style.backgroundColor = 'rgba(255, 99, 71, 0.6)'; // Apply light red with transparency + if (row) { + row.classList.add('table-danger'); + row.classList.remove('table-success'); + } } await fetch('/api/movies/users/updateWatchedMovies', { -- 2.54.0 From b5dd58f80abb3bb80bfd2c38e3dac1f64c7837a2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Dec 2025 10:23:57 +0000 Subject: [PATCH 2/5] Refactor movie display and styling for better UI Co-authored-by: mathieumoran1 --- public/css/app.css | 17 +++++++++ public/js/app_movies.js | 35 ++++++++++------- public/movies.html | 83 ++++++++++++++--------------------------- 3 files changed, 66 insertions(+), 69 deletions(-) diff --git a/public/css/app.css b/public/css/app.css index fe3a6fa..47e7f27 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -164,4 +164,21 @@ footer { .app-version-list .list-group-item:last-child { border-bottom: 0; + } + + /* Movies page helpers (Bootstrap-first, minimal custom) */ + .movie-poster { + width: 100%; + max-height: 360px; + object-fit: contain; + } + + .imdb-icon { + width: 18px; + height: 18px; + vertical-align: text-bottom; + } + + .watched-badge { + z-index: 2; } \ No newline at end of file diff --git a/public/js/app_movies.js b/public/js/app_movies.js index 029bc32..1a3d890 100644 --- a/public/js/app_movies.js +++ b/public/js/app_movies.js @@ -359,22 +359,31 @@ window.addEventListener('DOMContentLoaded', async function () { : (hasLegacy ? movie.vod_link : (movie && movie._id ? `/player.html?id=${encodeURIComponent(movie._id)}` : '#')); const movieDiv = document.createElement('div'); - movieDiv.classList.add('col-md-4', 'mb-4', 'movie-card'); + movieDiv.classList.add('col-12', 'col-sm-6', 'col-lg-4', 'movie-card'); movieDiv.setAttribute('data-imdb-id', movie.imdb_id); movieDiv.innerHTML = ` -
- ${isChecked ? '
VISIONNÉ
' : ''} - - ${movie.title} +
+ ${isChecked ? 'VISIONNÉ' : ''} + +
+ ${movie.title} +
-
-
- ${movie.title} - ⭐ ${movie.rating} -
-

${movie.category}

-

${movie.description}

- ${isChecked ? `

Regardé le: ${watchedDate}

` : ''} +
+
+
+ ${movie.title} +
+
+ ⭐ ${movie.rating} + + IMDb + +
+
+
${movie.category}
+

${movie.description}

+ ${isChecked ? `
Regardé le: ${watchedDate}
` : ''}
`; moviesList.appendChild(movieDiv); diff --git a/public/movies.html b/public/movies.html index dd556a2..b0ce59d 100644 --- a/public/movies.html +++ b/public/movies.html @@ -21,42 +21,9 @@ color: #ddd; } - /* Style for the 'WATCHED' banner */ - .watched-banner { - background-color: green; - color: white; - text-align: center; - padding: 5px; - font-weight: bold; - position: absolute; - top: 10px; - width: 100%; - z-index: 1; - } - - /* Centering checkbox and making card look better */ - .movie-card { - position: relative; - margin-bottom: 20px; - } - .card-body { - position: relative; - } - #movies-list { margin-top: 10px; } - - /* Style for the filter button */ - #filter-button { - margin-left: 15px; - } - - /* Style for the ratio display */ - .ratio { - font-weight: bold; - margin-top: 10px; - } .navbar-toggler { border-color: white; /* Make the border white */ } @@ -104,31 +71,35 @@
-
-

Films

+

Films

+ + +
+
+
+
+ Dernière mise à jour des films: — +
+
—
+
+ Temps avant les Oscars (— —) — — +
+
+ +
+
+ + +
+
Clique sur l’affiche ou le titre pour ouvrir le lecteur.
+
+
- - - - -
- -
- -

Temps avant les Oscars (— —) -

-

- -

-
-
- - -
-
- +
+
+
Chargement…
+
-- 2.54.0 From 34fcee32531eb4b399fe7b565d12ee04462de203 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Dec 2025 10:27:30 +0000 Subject: [PATCH 3/5] feat: Add page loader component and integrate into player Co-authored-by: mathieumoran1 --- public/js/app_player.js | 119 ++++++++++++++++++++++++++++++++++++++++ public/player.html | 85 +++++++++++++++++----------- 2 files changed, 173 insertions(+), 31 deletions(-) diff --git a/public/js/app_player.js b/public/js/app_player.js index 5328bc1..6bcc9ff 100644 --- a/public/js/app_player.js +++ b/public/js/app_player.js @@ -7,6 +7,110 @@ function decodeJwt(token) { return JSON.parse(atob(padded)); } +function createPageLoader(options = {}) { + const title = String(options.title || 'Chargement…'); + let progress = 0; + let removed = false; + let navResizeObserver = null; + + // Reuse a preloaded overlay if present (prevents initial "clear" flash). + const preloadedOverlay = document.getElementById('page-loader-overlay'); + const overlay = preloadedOverlay || document.createElement('div'); + if (!preloadedOverlay) { + overlay.className = 'page-loader-overlay'; + overlay.setAttribute('role', 'status'); + overlay.setAttribute('aria-live', 'polite'); + overlay.setAttribute('aria-busy', 'true'); + + overlay.innerHTML = ` +
+ ${title} + +
+ `; + } else { + const label = overlay.querySelector('.visually-hidden'); + if (label) label.textContent = title; + } + + const barEl = () => overlay.querySelector('#page-loader-bar'); + + function clampPct(p) { + const n = Number(p); + if (!Number.isFinite(n)) return 0; + return Math.max(0, Math.min(100, n)); + } + + function setProgress(p) { + progress = clampPct(p); + const bar = barEl(); + if (bar) bar.style.width = `${progress}%`; + } + + function getNavHeightPx() { + const nav = document.querySelector('nav.navbar'); + if (!nav) return 0; + const rect = nav.getBoundingClientRect(); + const h = Number(rect?.height) || 0; + return h > 0 ? Math.round(h) : 0; + } + + function updateOverlayTopOffset() { + overlay.style.setProperty('--page-loader-top', `${getNavHeightPx()}px`); + } + + function ensureMounted() { + if (removed) return; + updateOverlayTopOffset(); + document.body.classList.add('page-loading'); + if (!overlay.isConnected) document.body.appendChild(overlay); + + if (!navResizeObserver) { + const nav = document.querySelector('nav.navbar'); + if (nav && typeof ResizeObserver !== 'undefined') { + try { + navResizeObserver = new ResizeObserver(() => updateOverlayTopOffset()); + navResizeObserver.observe(nav); + } catch (_) { + navResizeObserver = null; + } + } + } + } + + function hideAndRemoveSoon() { + if (removed) return; + overlay.classList.add('page-loader-hide'); + overlay.setAttribute('aria-busy', 'false'); + document.body.classList.remove('page-loading'); + window.setTimeout(() => { + removed = true; + if (navResizeObserver) { + try { navResizeObserver.disconnect(); } catch (_) {} + navResizeObserver = null; + } + try { overlay.remove(); } catch (_) {} + }, 220); + } + + function done() { + setProgress(100); + hideAndRemoveSoon(); + } + + function fail() { + setProgress(Math.max(progress, 95)); + window.setTimeout(() => hideAndRemoveSoon(), 200); + } + + ensureMounted(); + setProgress(8); + + return { setProgress, done, fail }; +} + function safeJsonParse(v) { try { return JSON.parse(v); } catch (_) { return null; } } @@ -754,12 +858,15 @@ function resolveMovieSource(movie) { } window.onload = async function () { + const pageLoader = createPageLoader({ title: 'Chargement du lecteur' }); + // Basic auth UX: require a valid local token (like the rest of the app). const token = localStorage.getItem('auth_token'); if (!token) { window.location.href = '/'; return; } + pageLoader.setProgress(14); // Load the active Oscar year in parallel with the movie fetch. const activeYearPromise = fetchActiveOscarYear(); @@ -796,14 +903,17 @@ window.onload = async function () { window.location.href = '/'; return; } + pageLoader.setProgress(26); const id = getQueryParam('id'); if (!id) { showAlert('Missing movie id.'); + pageLoader.fail(); return; } try { + pageLoader.setProgress(36); const res = await fetch(`/api/movies/${encodeURIComponent(id)}`, { method: 'GET', headers: token ? { 'Authorization': `Bearer ${token}` } : undefined, @@ -811,17 +921,20 @@ window.onload = async function () { const movie = await res.json().catch(() => ({})); if (!res.ok) { showAlert(movie?.message || movie?.error || `Failed to load movie (${res.status}).`); + pageLoader.fail(); return; } const activeYear = await activeYearPromise; setHeader(movie, { activeYear }); + pageLoader.setProgress(58); const resolved = resolveMovieSource(movie); if (!resolved?.src) { setSourceLabel('—'); hideAllPlayers(); showAlert('No playable source configured for this movie.'); + pageLoader.fail(); return; } @@ -832,10 +945,13 @@ window.onload = async function () { setSourceLabel('Legacy link'); hideAllPlayers(); showAlertVariant('Legacy source: this link is not playable in the in-app player.', 'warning'); + pageLoader.done(); return; } + pageLoader.setProgress(70); await playVodLink(resolved.src, { token }); + pageLoader.setProgress(82); // Only the