Files
oscars-pool/public/player.html
T
Mathieu Moran 4308d6babf Implement en/fr localization across admin and user UIs
Introduce client-side localization helper and en/fr locale bundles.

Route admin control, picks, movies, stats, winners and settings UIs through i18n keys instead of hardcoded text.

Remove French text from the English UI and standardize shared messages (saving, network errors, confirmations).

Update .gitignore to ignore editor metadata and localization helper artifacts.
2026-02-01 20:20:02 -05:00

353 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- IMPORTANT: the player may use ?token=... for cross-origin video streaming -->
<meta name="referrer" content="no-referrer" />
<title data-i18n="player.title">Oscar Pool - Player</title>
<script src="./js/app_theme.js"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="./css/app.css" rel="stylesheet">
<link rel="preload" as="image" href="img/logo_w.png">
<style>
/* Hide translatable content until translations are ready - prevents flash during page navigation */
body:not(.i18n-ready) [data-i18n] {
opacity: 0;
transition: opacity 0.1s ease-in;
}
body.i18n-ready [data-i18n] {
opacity: 1;
}
/* Fallback: show content after 2 seconds even if JS fails */
body:not(.i18n-ready) [data-i18n] {
animation: show-after-delay 0.1s 2s forwards;
}
@keyframes show-after-delay {
to { opacity: 1; }
}
</style>
<script src="./js/localization.js" defer></script>
<script src="./js/app_header_boot.js" defer></script>
<style>
.navbar-custom {
background-color: black;
color: white;
}
.navbar-custom .navbar-brand,
.navbar-custom .nav-link {
color: white;
}
.navbar-custom .nav-link:hover {
color: #ddd;
}
.navbar-toggler {
border-color: white;
}
.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");
}
.player-wrap {
max-width: 1100px;
margin: 0 auto;
}
.player-surface {
background: #000;
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(0,0,0,0.1);
}
video {
width: 100%;
height: auto;
display: block;
background: #000;
}
iframe {
width: 100%;
aspect-ratio: 16 / 9;
border: 0;
display: block;
background: #000;
}
.meta-muted {
color: rgba(0,0,0,0.65);
}
.badge-src {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.quality-toggle {
display: inline-flex;
align-items: center;
gap: .6rem;
background: #f8f9fa;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 999px;
padding: .25rem .4rem .25rem .6rem;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.quality-label {
font-size: .8rem;
color: rgba(0,0,0,0.6);
font-weight: 600;
}
.quality-pill {
display: inline-flex;
background: #fff;
border-radius: 999px;
border: 1px solid rgba(0,0,0,0.08);
padding: 2px;
gap: 2px;
}
.quality-btn {
border: 0;
background: transparent;
color: rgba(0,0,0,0.65);
font-size: .78rem;
font-weight: 600;
border-radius: 999px;
padding: .25rem .6rem;
line-height: 1.1;
transition: background .2s ease, color .2s ease;
}
.quality-btn.is-active {
background: #111;
color: #fff;
}
.quality-btn:focus-visible {
outline: 2px solid rgba(13,110,253,.35);
outline-offset: 2px;
}
.user-rating {
display: flex;
flex-direction: column;
gap: .25rem;
align-items: center;
text-align: center;
width: 100%;
}
.rating-stars {
display: inline-flex;
align-items: center;
gap: .15rem;
justify-content: center;
}
.star-btn {
border: 0;
background: transparent;
color: rgba(0,0,0,0.5);
font-size: 1.1rem;
line-height: 1;
padding: .15rem .2rem;
}
.star-btn.is-active {
color: #f4b400;
}
html[data-theme="dark"] .star-btn {
color: rgba(255, 255, 255, 0.55);
}
html[data-theme="dark"] .star-btn.is-active {
color: #f4b400;
}
.star-btn:focus-visible {
outline: 2px solid rgba(13,110,253,.35);
outline-offset: 2px;
border-radius: 4px;
}
#user-rating-meta {
color: rgba(0,0,0,0.6);
font-weight: 500;
}
/* Progress indicator: icon-first, minimal text */
.progress-indicator {
display: inline-flex;
align-items: center;
gap: .4rem;
min-height: 1rem;
}
.progress-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1rem;
height: 1rem;
color: rgba(0,0,0,.65);
flex: 0 0 1rem;
}
.progress-icon--spinner {
border: 2px solid rgba(0,0,0,.22);
border-top-color: rgba(0,0,0,.65);
border-radius: 50%;
animation: progressSpin .8s linear infinite;
}
.progress-icon svg {
width: 1rem;
height: 1rem;
display: block;
}
.progress-text {
opacity: .8;
white-space: nowrap;
}
.progress-text.is-hidden {
display: none;
}
@keyframes progressSpin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body class="bg-light page-loading">
<!-- Preloaded loader overlay to avoid initial "clear" flash -->
<div id="page-loader-overlay" class="page-loader-overlay" data-preloaded="true" role="status" aria-live="polite" aria-busy="true">
<div class="page-loader-card page-loader-card--baronly">
<span class="visually-hidden" data-i18n="common.loading">Loading…</span>
<div class="page-loader-progress" aria-hidden="true">
<div class="page-loader-bar" id="page-loader-bar" style="width: 8%;"></div>
</div>
</div>
</div>
<!-- Black banner with buttons and user info -->
<nav class="navbar navbar-expand-lg navbar-custom">
<div class="container">
<a class="navbar-brand" href="movies.html">
<img src="img/logo_w.png" width="220" height="70" loading="eager" fetchpriority="high" decoding="async" style="max-width: 220px; height: 70px; object-fit: contain;">
</a>
<span class="navbar-text ms-auto me-2 d-lg-none navbar-user-name-mobile" data-user-name></span>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<div class="navbar-nav">
<a href="movies.html" class="nav-link btn btn-outline-light me-2" data-i18n="nav.films">Films</a>
<a href="checklist.html" class="nav-link btn btn-outline-light me-2" data-i18n="nav.checklist">Checklist</a>
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2" data-i18n="nav.stats">Stats</a>
<a href="picks.html" class="nav-link btn btn-outline-light me-2" id="nav-link-picks" style="display: none;" data-i18n="nav.myPicks">My picks</a>
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none" data-i18n="nav.admin">Admin</a>
</div>
<div class="d-flex ms-auto mt-2 mt-lg-0 align-items-center gap-2 navbar-user">
<span id="user-name" class="navbar-text" data-user-name></span>
<button class="theme-toggle" type="button" data-theme-toggle role="switch" aria-checked="false" data-i18n-aria-label="common.toggleDarkMode" aria-label="Toggle dark mode">
<span class="theme-toggle-icon theme-toggle-icon-sun" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2v2"></path>
<path d="M12 20v2"></path>
<path d="M4.93 4.93l1.41 1.41"></path>
<path d="M17.66 17.66l1.41 1.41"></path>
<path d="M2 12h2"></path>
<path d="M20 12h2"></path>
<path d="M4.93 19.07l1.41-1.41"></path>
<path d="M17.66 6.34l1.41-1.41"></path>
</svg>
</span>
<span class="theme-toggle-thumb" aria-hidden="true"></span>
<span class="theme-toggle-icon theme-toggle-icon-moon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"></path>
</svg>
</span>
</button>
<button id="log-off" class="btn btn-danger" data-i18n="nav.logout">Log out</button>
</div>
</div>
</div>
</nav>
<div class="container mt-4 pb-4 player-wrap">
<div id="alert" class="alert alert-danger d-none" role="alert"></div>
<!-- Header / metadata -->
<div class="card shadow-sm mt-3">
<div class="card-body">
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3">
<div>
<h2 id="movie-title" class="mb-1" data-i18n="common.loading">Loading…</h2>
<div class="text-muted small">
<span id="movie-category"></span><span id="movie-rating"></span>
</div>
</div>
<div class="d-flex flex-column gap-2 align-items-center align-items-lg-center">
<div id="quality-toggle" class="d-none quality-toggle">
<span class="quality-label" data-i18n="player.quality">Quality</span>
<div class="quality-pill" role="group" aria-label="Quality">
<button type="button" class="quality-btn" data-quality="high" id="quality-btn-high" aria-pressed="true" data-i18n="player.high">High</button>
<button type="button" class="quality-btn" data-quality="low" id="quality-btn-low" aria-pressed="false" data-i18n="player.low">Low</button>
</div>
</div>
<div id="user-rating" class="user-rating">
<div class="rating-stars" role="group" data-i18n-aria-label="player.rateThisFilm" aria-label="Rate this film">
<button type="button" class="star-btn" data-value="1" aria-label="1 star" aria-pressed="false">★</button>
<button type="button" class="star-btn" data-value="2" aria-label="2 stars" aria-pressed="false">★</button>
<button type="button" class="star-btn" data-value="3" aria-label="3 stars" aria-pressed="false">★</button>
<button type="button" class="star-btn" data-value="4" aria-label="4 stars" aria-pressed="false">★</button>
<button type="button" class="star-btn" data-value="5" aria-label="5 stars" aria-pressed="false">★</button>
</div>
<div id="user-rating-meta" class="text-muted small">—</div>
</div>
<!-- Admin-only debug/status (Source + Progress) -->
<div id="player-admin-status" class="d-none d-flex flex-column gap-2">
<div class="d-flex flex-wrap gap-2 align-items-center">
<span class="badge bg-secondary" data-i18n="player.source">Source</span>
<span id="source-label" class="badge bg-light text-dark badge-src">—</span>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<span class="badge bg-secondary" data-i18n="player.progress">Progress</span>
<span id="progress-status" class="text-muted small progress-indicator">
<span id="progress-status-icon" class="progress-icon" aria-hidden="true"></span>
<span id="progress-status-text" class="progress-text">—</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Player surface -->
<div class="card shadow-sm mt-3">
<div class="card-body p-0">
<div class="player-surface">
<video id="video" class="d-none" controls playsinline></video>
<iframe
id="embed"
class="d-none"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
referrerpolicy="no-referrer-when-downgrade"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-presentation"
></iframe>
</div>
</div>
</div>
</div>
<!-- Footer Section -->
<footer class="footer--player">
<p id="footer-copy"><span data-i18n="footer.oscarPool">Oscar Pool</span> <span aria-hidden="true">·</span> <a id="copy" href="https://github.com/quoije/oscars-pool">quoije</a></p>
<p id="footer-text"></p>
</footer>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<!-- HLS support (m3u8) -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js"></script>
<script src="./js/app_player.js"></script>
<script src="./js/app_version.js"></script>
</body>
</html>