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/css/app.css b/public/css/app.css index fe3a6fa..3d60bd2 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -164,4 +164,23 @@ 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: 26px; + height: 26px; + vertical-align: middle; + margin-left: 6px; + display: inline-block; + } + + .watched-badge { + z-index: 2; } \ No newline at end of file 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', { 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/js/app_player.js b/public/js/app_player.js index 5328bc1..de77bbc 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(); @@ -782,6 +889,8 @@ 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'); @@ -796,14 +905,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 +923,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 +947,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