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) {
|
@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 ============ -->
|
<!-- ============ Colonne principale ============ -->
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<!-- Lecteur : monté dès que l'URL d'embed est calculable (route),
|
<!-- Lecteur : monté dès que l'URL d'embed est calculable (route),
|
||||||
sans attendre /details, suggestions ou historique. -->
|
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">
|
<div class="aspect-video">
|
||||||
@if (embedUrl(); as src) {
|
@if (embedUrl(); as src) {
|
||||||
<iframe #embedFrame class="h-full w-full" [src]="src" frameborder="0" (load)="onEmbedFrameLoad()"
|
<iframe #embedFrame class="h-full w-full" [src]="src" frameborder="0" (load)="onEmbedFrameLoad()"
|
||||||
@@ -169,6 +184,16 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Transcription</span>
|
<span>Transcription</span>
|
||||||
</button>
|
</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()) {
|
@if (isLoggedIn()) {
|
||||||
<button (click)="openDownloadPanel()" title="Télécharger la vidéo"
|
<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">
|
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>
|
</div>
|
||||||
} @else if (loading()) {
|
} @else if (loading()) {
|
||||||
<!-- Premier affichage, avant même le placeholder vidéo -->
|
<!-- 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>
|
||||||
<div class="aspect-video rounded-2xl bg-slate-800"></div>
|
<div class="aspect-video rounded-2xl bg-slate-800"></div>
|
||||||
<div class="mt-4 space-y-3">
|
<div class="mt-4 space-y-3">
|
||||||
|
|||||||
@@ -163,6 +163,8 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
|
|||||||
video = signal<VideoDetail | null>(null);
|
video = signal<VideoDetail | null>(null);
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
showFullDescription = signal(false);
|
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
|
// Prefer full description if available; otherwise fallback to shortDescription
|
||||||
descriptionText = computed(() => {
|
descriptionText = computed(() => {
|
||||||
const v = this.video();
|
const v = this.video();
|
||||||
@@ -491,6 +493,17 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
|
|||||||
const target = ev.target as HTMLElement | null;
|
const target = ev.target as HTMLElement | null;
|
||||||
if (target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
|
if (target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
|
||||||
if (ev.ctrlKey || ev.metaKey || ev.altKey) 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 (ev.key !== 's' && ev.key !== 'S') return;
|
||||||
if (!this.canSubscribe() || !this.isLoggedIn() || this.subscribeBusy()) return;
|
if (!this.canSubscribe() || !this.isLoggedIn() || this.subscribeBusy()) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
@@ -498,6 +511,15 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
|
|||||||
} catch {}
|
} 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 {
|
onAvatarError(): void {
|
||||||
this.channelAvatarBroken.set(true);
|
this.channelAvatarBroken.set(true);
|
||||||
}
|
}
|
||||||
@@ -553,6 +575,10 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
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).
|
// Écoute globale des événements du player YouTube embarqué (suivi transcript).
|
||||||
try { window.addEventListener('message', this.handlePlayerMessage); } catch {}
|
try { window.addEventListener('message', this.handlePlayerMessage); } catch {}
|
||||||
// Charge le cache d'abonnements pour que isSubscribed() soit fiable sur /watch.
|
// Charge le cache d'abonnements pour que isSubscribed() soit fiable sur /watch.
|
||||||
|
|||||||
Reference in New Issue
Block a user