feat(watch): mode cinema pleine largeur (bouton + raccourci T)
CI / build-and-test (push) Successful in 13m53s

This commit is contained in:
2026-09-26 12:21:38 -04:00
parent 655061ba2c
commit 9b4fffd3c8
2 changed files with 55 additions and 4 deletions
+29 -4
View File
@@ -1,11 +1,26 @@
<div class="mx-auto w-full max-w-7xl px-4 py-4 sm:px-6 lg:px-8 lg:py-6">
<div class="mx-auto w-full px-4 py-4 sm:px-6 lg:px-8 lg:py-6" [ngClass]="cinemaMode() ? 'max-w-none' : 'max-w-7xl'">
@if (video(); as v) {
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_360px] xl:gap-8">
<div class="grid gap-6 xl:gap-8" [ngClass]="cinemaMode() ? 'lg:grid-cols-1' : 'lg:grid-cols-[minmax(0,1fr)_360px]'">
<!-- ============ Colonne principale ============ -->
<div class="min-w-0">
<!-- Lecteur : monté dès que l'URL d'embed est calculable (route),
sans attendre /details, suggestions ou historique. -->
<section aria-label="Lecteur vidéo" class="overflow-hidden rounded-2xl bg-black shadow-2xl ring-1 ring-white/10">
<section aria-label="Lecteur vidéo" class="relative overflow-hidden rounded-2xl bg-black shadow-2xl ring-1 ring-white/10">
<button (click)="toggleCinema()" [attr.aria-pressed]="cinemaMode()"
[title]="cinemaMode() ? 'Quitter le mode cinéma (Échap)' : 'Mode cinéma : élargir la vidéo (T)'"
[attr.aria-label]="cinemaMode() ? 'Quitter le mode cinéma' : 'Activer le mode cinéma'"
class="absolute right-3 top-3 z-10 inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-semibold shadow-lg transition focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400"
[class.bg-red-600]="cinemaMode()" [class.text-white]="cinemaMode()"
[class.bg-black/70]="!cinemaMode()" [class.text-slate-200]="!cinemaMode()" [class.hover:bg-black/90]="!cinemaMode()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
@if (cinemaMode()) {
<path fill-rule="evenodd" d="M5 4a1 1 0 00-1 1v4a1 1 0 102 0V6.414l3.293 3.293a1 1 0 001.414-1.414L7.414 5H10a1 1 0 100-2H5zm10 0a1 1 0 011 1v4a1 1 0 11-2 0V6.414l-3.293 3.293a1 1 0 11-1.414-1.414L12.586 5H10a1 1 0 110-2h5zM5 16a1 1 0 01-1-1v-4a1 1 0 112 0v2.586l3.293-3.293a1 1 0 111.414 1.414L7.414 15H10a1 1 0 110 2H5zm10 0a1 1 0 001-1v-4a1 1 0 10-2 0v2.586l-3.293-3.293a1 1 0 10-1.414 1.414l3.293 3.293H10a1 1 0 101 1v1a1 1 0 001 1h4z" clip-rule="evenodd" />
} @else {
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 110 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 11-2 0V4zm11-1a1 1 0 011 1v4a1 1 0 11-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L11.586 5H10a1 1 0 010-2h4zM3 16a1 1 0 011-1h1.586l-2.293-2.293a1 1 0 111.414-1.414L6.414 13H8a1 1 0 110 2H4a1 1 0 01-1-1zm13-1a1 1 0 01-1 1h-4a1 1 0 110-2h1.586l-2.293-2.293a1 1 0 111.414-1.414l2.293 2.293V12a1 1 0 112 0v4z" clip-rule="evenodd" />
}
</svg>
<span>{{ cinemaMode() ? 'Quitter' : 'Cinéma' }}</span>
</button>
<div class="aspect-video">
@if (embedUrl(); as src) {
<iframe #embedFrame class="h-full w-full" [src]="src" frameborder="0" (load)="onEmbedFrameLoad()"
@@ -169,6 +184,16 @@
</svg>
<span>Transcription</span>
</button>
<button (click)="toggleCinema()" [attr.aria-pressed]="cinemaMode()"
[title]="cinemaMode() ? 'Quitter le mode cinéma (Échap)' : 'Mode cinéma : la vidéo prend la largeur de la page (T)'"
class="inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-semibold transition focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400"
[class.bg-red-600]="cinemaMode()" [class.text-white]="cinemaMode()"
[class.bg-slate-700]="!cinemaMode()" [class.text-slate-200]="!cinemaMode()" [class.hover:bg-slate-600]="!cinemaMode()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 110 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 11-2 0V4zm11-1a1 1 0 011 1v4a1 1 0 11-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L11.586 5H10a1 1 0 010-2h4zM3 16a1 1 0 011-1h1.586l-2.293-2.293a1 1 0 111.414-1.414L6.414 13H8a1 1 0 110 2H4a1 1 0 01-1-1zm13-1a1 1 0 01-1 1h-4a1 1 0 110-2h1.586l-2.293-2.293a1 1 0 111.414-1.414l2.293 2.293V12a1 1 0 112 0v4z" clip-rule="evenodd" />
</svg>
<span>{{ cinemaMode() ? 'Quitter le cinéma' : 'Cinéma' }}</span>
</button>
@if (isLoggedIn()) {
<button (click)="openDownloadPanel()" title="Télécharger la vidéo"
class="inline-flex items-center gap-1.5 rounded-full bg-slate-700 px-3 py-1.5 text-sm font-semibold text-slate-200 transition hover:bg-slate-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400">
@@ -526,7 +551,7 @@
</div>
} @else if (loading()) {
<!-- Premier affichage, avant même le placeholder vidéo -->
<div class="grid animate-pulse gap-6 lg:grid-cols-[minmax(0,1fr)_360px]" aria-hidden="true">
<div class="grid animate-pulse gap-6" [ngClass]="cinemaMode() ? 'lg:grid-cols-1' : 'lg:grid-cols-[minmax(0,1fr)_360px]'" aria-hidden="true">
<div>
<div class="aspect-video rounded-2xl bg-slate-800"></div>
<div class="mt-4 space-y-3">
+26
View File
@@ -163,6 +163,8 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
video = signal<VideoDetail | null>(null);
loading = signal(true);
showFullDescription = signal(false);
/** Mode cinéma : le lecteur prend toute la largeur de la page. */
cinemaMode = signal<boolean>(false);
// Prefer full description if available; otherwise fallback to shortDescription
descriptionText = computed(() => {
const v = this.video();
@@ -491,6 +493,17 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
const target = ev.target as HTMLElement | null;
if (target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
// Échap : quitter le mode cinéma.
if (ev.key === 'Escape' && this.cinemaMode()) {
this.toggleCinema(false);
return;
}
// Touche "t" (comme YouTube) : basculer le mode cinéma.
if (ev.key === 't' || ev.key === 'T') {
ev.preventDefault();
this.toggleCinema();
return;
}
if (ev.key !== 's' && ev.key !== 'S') return;
if (!this.canSubscribe() || !this.isLoggedIn() || this.subscribeBusy()) return;
ev.preventDefault();
@@ -498,6 +511,15 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
} catch {}
}
/** Bascule le mode cinéma (vidéo pleine largeur). Persiste le choix. */
toggleCinema(force?: boolean): void {
const next = typeof force === 'boolean' ? force : !this.cinemaMode();
this.cinemaMode.set(next);
try {
localStorage.setItem('newtube:cinema', next ? '1' : '0');
} catch {}
}
onAvatarError(): void {
this.channelAvatarBroken.set(true);
}
@@ -553,6 +575,10 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
}
constructor() {
// Restaure le mode cinéma choisi précédemment.
try {
this.cinemaMode.set(localStorage.getItem('newtube:cinema') === '1');
} catch {}
// Écoute globale des événements du player YouTube embarqué (suivi transcript).
try { window.addEventListener('message', this.handlePlayerMessage); } catch {}
// Charge le cache d'abonnements pour que isSubscribed() soit fiable sur /watch.