feat(watch): mode cinema pleine largeur (bouton + raccourci T)
CI / build-and-test (push) Successful in 13m53s
CI / build-and-test (push) Successful in 13m53s
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user