feat(lecteur): menu FAB partage /watch et /shorts
CI / build-and-test (push) Successful in 14m48s

- PlayerMenuComponent : FAB circulaire translucide (haut-gauche) + panneau
  deroulant, commun aux deux pages (position, style, open/close, clic exterieur)
- /shorts : consolidation de tous les boutons (fournisseurs, langue, actions)
  dans le menu FAB ; suppression du rail a droite et de la rangee du haut
- /watch : actions (cinema, qualite, j'aime, playlist, transcription,
  telechargement, partage, ouvrir) deplacees dans le menu FAB
- AddToPlaylistComponent : variante menuItem pour s'integrer au menu
- styles .pm-item/.pm-sep/.pm-section-label dans index.css
This commit is contained in:
2026-09-30 22:50:46 -04:00
parent 2bcc7321aa
commit 01e0671de9
7 changed files with 301 additions and 240 deletions
+31
View File
@@ -572,6 +572,37 @@ body {
touch-action: none;
}
/* --- Player FAB menu (shared /watch & /shorts) ---------------------------- */
/* Items/coque du panneau du menu flottant : petits, translucides, harmonisés
avec les commandes overlay des players YouTube/Dailymotion. */
.pm-section-label {
padding: 0.375rem 0.75rem 0.25rem;
font-size: 0.6875rem; /* 11px */
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
.pm-item {
display: flex;
align-items: center;
gap: 0.625rem;
width: 100%;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
font-size: 0.8125rem; /* 13px */
color: var(--text);
text-align: left;
line-height: 1.25;
transition: background-color .15s ease;
}
.pm-item:hover { background-color: rgba(255, 255, 255, .08); }
.pm-item:disabled { opacity: .4; cursor: not-allowed; }
.pm-item svg { flex-shrink: 0; }
.pm-item .pm-item-label { flex: 1; min-width: 0; }
.pm-item .pm-item-hint { font-size: 0.6875rem; color: var(--muted); }
.pm-sep { height: 1px; margin: 0.375rem 0.5rem; background: rgba(255, 255, 255, .12); }
/* --- Drawer / overlay plumbing -------------------------------------------- */
/* Applied to <body> while a full-screen drawer or modal owns the viewport.
Note: deliberately NOT `touch-action: none` — touch-action is intersected up
@@ -0,0 +1,65 @@
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
/**
* Bouton-menu flottant (FAB) partagé entre /watch et /shorts.
*
* Pastille circulaire translucide en haut à gauche du lecteur, façon
* commandes overlay des players YouTube/Dailymotion : elle ouvre un panneau
* déroulant qui reçoit son contenu par projection (`ng-content`) — chaque page
* fournit ses propres options, la coque (position, style, open/close, clic
* extérieur) est identique partout.
*
* Usage : `<app-player-menu [(open)]="menuOpen"> …items… </app-player-menu>`
* dans un conteneur `position: relative` (le `:host` se place en absolu).
*/
@Component({
selector: 'app-player-menu',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule],
template: `
<button type="button"
(click)="toggle(); $event.stopPropagation()"
[attr.aria-expanded]="open"
aria-haspopup="menu"
aria-label="Menu"
title="Menu"
class="flex h-9 w-9 items-center justify-center rounded-full bg-black/50 text-white ring-1 ring-white/15 backdrop-blur-md transition hover:bg-black/70 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/40">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
@if (open) {
<div class="absolute left-0 top-11 max-h-[70vh] w-64 overflow-y-auto overscroll-contain rounded-xl bg-black/70 p-1.5 shadow-2xl ring-1 ring-white/10 backdrop-blur-xl"
role="menu" (click)="$event.stopPropagation()">
<ng-content></ng-content>
</div>
}
`,
styles: [
`:host { position: absolute; top: 0.5rem; left: 0.5rem; z-index: 30; display: block; }`,
],
})
export class PlayerMenuComponent {
@Input() open = false;
@Output() openChange = new EventEmitter<boolean>();
constructor(private el: ElementRef<HTMLElement>) {}
toggle(): void {
this.openChange.emit(!this.open);
}
close(): void {
if (this.open) this.openChange.emit(false);
}
@HostListener('document:click', ['$event'])
onDocumentClick(event: Event): void {
if (!this.open) return;
const target = event.target as Node | null;
if (target && this.el.nativeElement.contains(target)) return;
this.close();
}
}
@@ -12,18 +12,22 @@ import { forkJoin } from 'rxjs';
// HttpClient sans intercepteurs (plus de Bearer/refresh -> 401).
imports: [CommonModule],
template: `
<div class="relative inline-block text-left" (click)="$event.stopPropagation();">
<div [class]="menuItem ? 'relative w-full text-left' : 'relative inline-block text-left'" (click)="$event.stopPropagation();">
<button (click)="toggleOpen($event)"
[disabled]="!isAuthenticated()"
class="flex items-center space-x-2 bg-slate-700 hover:bg-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60 disabled:opacity-60 disabled:cursor-not-allowed px-4 py-2 rounded-full transition text-slate-200 font-semibold"
[class]="menuItem ? 'pm-item' : 'flex items-center space-x-2 bg-slate-700 hover:bg-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60 disabled:opacity-60 disabled:cursor-not-allowed px-4 py-2 rounded-full transition text-slate-200 font-semibold'"
[attr.aria-expanded]="open()"
[attr.aria-haspopup]="true"
[attr.title]="!isAuthenticated() ? 'Connectez-vous pour gérer vos playlists' : 'Ajouter à une playlist'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M4 6a2 2 0 012-2h12a1 1 0 010 2H6v10a1 1 0 11-2 0V6z" />
<path d="M14 11a1 1 0 011-1h5a1 1 0 110 2h-5a1 1 0 01-1-1zM14 15a1 1 0 011-1h5a1 1 0 110 2h-5a1 1 0 01-1-1zM10 11a1 1 0 112 0v1h1a1 1 0 110 2h-1v1a1 1 0 11-2 0v-1H9a1 1 0 110-2h1v-1z" />
</svg>
<span class="sr-only">Ajouter à une playlist</span>
@if (menuItem) {
<span class="pm-item-label">Ajouter à une playlist</span>
} @else {
<span class="sr-only">Ajouter à une playlist</span>
}
</button>
@if (open()) {
@@ -115,6 +119,8 @@ export class AddToPlaylistComponent implements OnInit, OnDestroy {
@Input() videoId: string = '';
@Input() title?: string;
@Input() thumbnail?: string;
/** Rendu « élément de menu » (menu FAB du lecteur) : plein largeur, icon + label. */
@Input() menuItem = false;
open = signal<boolean>(false);
loading = signal<boolean>(false);
+99 -127
View File
@@ -10,66 +10,19 @@
<!-- Retour accueil (mobile plein écran : la vidéo occupe tout, pas de header visible) -->
<a routerLink="/"
class="absolute top-2 left-2 z-20 flex h-11 w-11 items-center justify-center rounded-full bg-black/60 text-white hover:bg-black/80 sm:hidden"
class="absolute top-2 left-2 z-20 flex h-9 w-9 items-center justify-center rounded-full bg-black/50 text-white ring-1 ring-white/15 backdrop-blur-md hover:bg-black/70 sm:hidden"
title="Retour à l'accueil NewTube" aria-label="Retour à l'accueil NewTube" (click)="$event.stopPropagation()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M19 12H5m7-7l-7 7 7 7"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M19 12H5m7-7l-7 7 7 7"/></svg>
</a>
<!-- Sélecteur icon-only : Pour toi + fournisseurs (logos) + filtre de langues.
Aucun texte visible (logos + title/aria-label) ; le filtre actif se lit
dans le tooltip et l'anneau ambré du globe. -->
<div class="scroll-rail no-scrollbar shorts-chrome relative z-10 flex w-full max-w-full items-center justify-start gap-2 overflow-x-auto px-3 pt-3 sm:justify-center" role="tablist" aria-label="Fournisseur des Shorts" (click)="$event.stopPropagation()">
<button (click)="toggleForYou()"
[attr.aria-pressed]="forYou()"
aria-label="Pour toi"
title="Pour toi — Shorts personnalisés multi-fournisseurs à partir de vos abonnements, groupes, recherches, likes et téléchargements"
class="chip flex h-11 w-11 shrink-0 items-center justify-center rounded-full border transition-colors"
[class]="forYou() ? 'bg-violet-600 border-violet-500 text-white' : 'bg-black/60 border-white/15 text-slate-200 hover:bg-white/10'">
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-gradient-to-br from-violet-500 to-fuchsia-500 text-white shadow" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2c.7 4.8 3.2 7.3 8 8-4.8.7-7.3 3.2-8 8-.7-4.8-3.2-7.3-8-8 4.8-.7 7.3-3.2 8-8z"/><path d="M19 3c.3 2 1.3 3 3.3 3.3-2 .3-3 1.3-3.3 3.2-.3-1.9-1.3-2.9-3.2-3.2 1.9-.3 2.9-1.3 3.2-3.3z"/></svg>
</span>
</button>
<button *ngFor="let p of providerButtons()"
(click)="selectProvider(p.id)"
[attr.aria-pressed]="!forYou() && provider() === p.id"
[attr.aria-label]="p.label"
[title]="p.label"
class="chip flex h-11 w-11 shrink-0 items-center justify-center rounded-full border transition-colors"
[class]="!forYou() && provider() === p.id ? 'bg-red-600 border-red-500 text-white' : 'bg-black/60 border-white/15 text-slate-200 hover:bg-white/10'">
<span class="flex h-7 w-7 items-center justify-center rounded-full text-white shadow" [class]="p.disc" aria-hidden="true">
@switch (p.glyph.kind) {
@case ('play') {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
}
@case ('chat') {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="4" y="3" width="16" height="13" rx="2"/><path d="M9 16v4l4-4"/><line x1="10" y1="7" x2="10" y2="11"/><line x1="14" y1="7" x2="14" y2="11"/></svg>
}
@default {
<span class="text-[15px] font-extrabold leading-none">{{ glyphLetter(p) }}</span>
}
}
</span>
</button>
<!-- Filtre de langues préférées (globe cyclique, sans texte) : chaque cran
change le filtre de TOUS les flux ; l'anneau ambré signale un filtre
actif (non-profil), le détail est dans le tooltip. -->
<button (click)="cycleLangFilter()"
[title]="langFilterHint()"
[attr.aria-label]="langFilterHint()"
class="flex h-11 w-11 shrink-0 items-center justify-center rounded-full border transition-colors"
[class]="langFilter() !== 'profile' ? 'bg-black/60 border-amber-400/70 text-amber-200 hover:bg-white/10 ring-2 ring-amber-400/40' : 'bg-black/60 border-white/15 text-slate-200 hover:bg-white/10'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
</button>
</div>
<!-- Scène immersive : flèches (bureau) + cadre 9:16 -->
<div class="relative z-10 flex w-full min-h-0 flex-1 items-center justify-center gap-1 px-0 py-3 sm:gap-3 sm:px-4"
(wheel)="onWheel($event)" (touchstart)="onTouchStart($event)" (touchend)="onTouchEnd($event)">
<!-- Flèche précédent (bureau, façon YouTube) -->
<button type="button" (click)="prev(); $event.stopPropagation()" [disabled]="!canPrev()"
class="shorts-chrome chip hidden sm:flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20 disabled:opacity-30"
class="shorts-chrome chip hidden sm:flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-black/40 text-white ring-1 ring-white/15 hover:bg-black/60 disabled:opacity-30"
aria-label="Short précédent">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 14l5-5 5 5H7z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 14l5-5 5 5H7z"/></svg>
</button>
<!-- Colonne centrale : cadre 9:16 (sizing dans index.css : .shorts-frame) -->
@@ -93,13 +46,102 @@
}
@if (current(); as v) {
<!-- Menu FAB : toutes les options (fournisseur, langue, actions). -->
<app-player-menu [(open)]="menuOpen">
<div class="pm-section-label">Fournisseur</div>
<button type="button" (click)="toggleForYou(); closeMenu()" class="pm-item" [class.text-violet-300]="forYou()" [attr.aria-pressed]="forYou()">
<span class="flex h-5 w-5 items-center justify-center rounded-full bg-gradient-to-br from-violet-500 to-fuchsia-500 text-white" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2c.7 4.8 3.2 7.3 8 8-4.8.7-7.3 3.2-8 8-.7-4.8-3.2-7.3-8-8 4.8-.7 7.3-3.2 8-8z"/><path d="M19 3c.3 2 1.3 3 3.3 3.3-2 .3-3 1.3-3.3 3.2-.3-1.9-1.3-2.9-3.2-3.2 1.9-.3 2.9-1.3 3.2-3.3z"/></svg>
</span>
<span class="pm-item-label">Pour toi</span>
@if (forYou()) { <svg xmlns="http://www.w3.org/2000/svg" class="ml-auto h-4 w-4 text-white/80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg> }
</button>
@for (p of providerButtons(); track p.id) {
<button type="button" (click)="selectProvider(p.id); closeMenu()" class="pm-item" [attr.aria-pressed]="!forYou() && provider() === p.id">
<span class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-white" [class]="p.disc" aria-hidden="true">
@switch (p.glyph.kind) {
@case ('play') {
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
}
@case ('chat') {
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="4" y="3" width="16" height="13" rx="2"/><path d="M9 16v4l4-4"/><line x1="10" y1="7" x2="10" y2="11"/><line x1="14" y1="7" x2="14" y2="11"/></svg>
}
@default {
<span class="text-[11px] font-extrabold leading-none">{{ glyphLetter(p) }}</span>
}
}
</span>
<span class="pm-item-label">{{ p.label }}</span>
@if (!forYou() && provider() === p.id) { <svg xmlns="http://www.w3.org/2000/svg" class="ml-auto h-4 w-4 text-white/80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg> }
</button>
}
<div class="pm-sep"></div>
<button type="button" (click)="cycleLangFilter()" class="pm-item" [title]="langFilterHint()" [attr.aria-label]="langFilterHint()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<span class="pm-item-label">Langue</span>
<span class="pm-item-hint">{{ langFilterShort() }}</span>
</button>
<div class="pm-sep"></div>
<button type="button" (click)="toggleLike()" class="pm-item" [disabled]="!isLoggedIn() || likeBusy()"
[title]="isLoggedIn() ? (liked() ? 'Je n’aime plus' : 'J’aime') : 'Connectez-vous pour aimer'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" [attr.fill]="liked() ? 'currentColor' : 'none'" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M7 10v12M15 5.88L14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88z"/></svg>
<span class="pm-item-label">{{ liked() ? 'Je n’aime plus' : 'J’aime' }}</span>
</button>
<button type="button" (click)="toggleMute()" class="pm-item" [title]="muted() ? 'Activer le son' : 'Couper le son'">
@if (muted()) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M11 5L6 9H2v6h4l5 4V5z"/><line x1="23" y1="9" x2="17" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="17" y1="9" x2="23" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
} @else {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M11 5L6 9H2v6h4l5 4V5z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M19 5a9 9 0 0 1 0 14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
}
<span class="pm-item-label">{{ muted() ? 'Activer le son' : 'Couper le son' }}</span>
</button>
<button type="button" (click)="toggleFullscreen(); closeMenu()" class="pm-item" [title]="isFullscreen() ? 'Quitter le plein écran' : 'Plein écran'">
@if (isFullscreen()) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/></svg>
} @else {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>
}
<span class="pm-item-label">{{ isFullscreen() ? 'Quitter le plein écran' : 'Plein écran' }}</span>
</button>
@if (watchLink()) {
<a [routerLink]="watchLink()" [queryParams]="watchQueryParams()" (click)="closeMenu()" class="pm-item" title="Voir les commentaires dans Watch">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<span class="pm-item-label">Commentaires</span>
</a>
}
<button type="button" (click)="share()" class="pm-item" title="Partager">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.6" y1="13.5" x2="15.4" y2="17.5"/><line x1="15.4" y1="6.5" x2="8.6" y2="10.5"/></svg>
<span class="pm-item-label">{{ shareCopied() ? 'Lien copié ✓' : 'Partager' }}</span>
</button>
@if (providerUrl()) {
<a [href]="providerUrl()" target="_blank" rel="noopener" (click)="closeMenu()" class="pm-item">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><path d="M15 3h6v6"/><path d="M10 14L21 3"/></svg>
<span class="pm-item-label">Ouvrir chez le fournisseur</span>
</a>
}
@if (watchLink()) {
<a [routerLink]="watchLink()" [queryParams]="watchQueryParams()" (click)="closeMenu()" class="pm-item">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M10 9l5 3-5 3V9z"/></svg>
<span class="pm-item-label">Ouvrir dans Watch</span>
</a>
}
@if (currentSource()) {
<div class="px-3 py-1.5 text-[11px] text-slate-400">Via le catalogue • {{ currentSource() }}</div>
}
<button type="button" (click)="skipUnplayable(); closeMenu()" class="pm-item">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4 5v14l8-7zM12 5v14l8-7z"/></svg>
<span class="pm-item-label">Passer (vidéo illisible)</span>
</button>
</app-player-menu>
<!-- Capteur tactile (mobile uniquement) : l'iframe mange les
touchers, donc le swipe up/down passerait à la trappe. Cette
couche transparente capte swipe (navigue) et tap (play/pause
YouTube, ou accès direct au lecteur 6 s sinon). `touch-action`
via CSS : le navigateur ne scrolle/zoome pas, nos handlers
possèdent le geste. En mode passthrough, elle s'efface
(les boutons du fournisseur deviennent touchables). -->
YouTube, ou accès direct au lecteur 6 s sinon). -->
<div class="shorts-gesture absolute inset-0 z-[1] sm:hidden"
[class.pointer-events-none]="passthrough()"
aria-hidden="true"></div>
@@ -108,76 +150,6 @@
<div class="absolute left-1/2 top-14 z-20 -translate-x-1/2 whitespace-nowrap px-3 py-1.5 rounded-full bg-black/75 text-white text-xs" role="status">Commandes du lecteur accessibles</div>
}
<!-- Haut droite : plein écran, partage rapide, menu ⋯ -->
<div class="shorts-chrome absolute top-2 right-2 z-10 flex items-center gap-1.5" (click)="$event.stopPropagation()">
<button type="button" (click)="toggleFullscreen()" class="flex h-10 w-10 items-center justify-center rounded-full bg-black/60 text-white hover:bg-black/80" [title]="isFullscreen() ? 'Quitter le plein écran' : 'Plein écran'" aria-label="Plein écran">
@if (isFullscreen()) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/></svg>
} @else {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>
}
</button>
<button type="button" (click)="share()" class="flex h-10 w-10 items-center justify-center rounded-full bg-black/60 text-white hover:bg-black/80" title="Partager" aria-label="Partager">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.6" y1="13.5" x2="15.4" y2="17.5"/><line x1="15.4" y1="6.5" x2="8.6" y2="10.5"/></svg>
</button>
<div class="relative">
<button type="button" (click)="toggleMenu(); $event.stopPropagation()" class="flex h-10 w-10 items-center justify-center rounded-full bg-black/60 text-white hover:bg-black/80" title="Plus d'options" aria-label="Plus d'options" aria-haspopup="menu">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg>
</button>
@if (menuOpen()) {
<div class="absolute right-0 top-11 z-20 w-60 overflow-hidden rounded-xl bg-slate-900/95 py-1 shadow-2xl ring-1 ring-white/10" role="menu" (click)="$event.stopPropagation()">
<a *ngIf="providerUrl()" [href]="providerUrl()" target="_blank" rel="noopener" (click)="closeMenu()" class="block px-4 py-3 text-left text-sm text-slate-200 hover:bg-slate-800" role="menuitem">Ouvrir chez le fournisseur</a>
<a *ngIf="watchLink()" [routerLink]="watchLink()" [queryParams]="watchQueryParams()" (click)="closeMenu()" class="block px-4 py-3 text-left text-sm text-slate-200 hover:bg-slate-800" role="menuitem">Ouvrir dans Watch</a>
<button type="button" (click)="copyLink()" class="block w-full px-4 py-3 text-left text-sm text-slate-200 hover:bg-slate-800" role="menuitem">{{ shareCopied() ? 'Lien copié ✓' : 'Copier le lien' }}</button>
@if (currentSource()) {
<div class="px-4 py-2 text-left text-[11px] text-slate-400" role="note">Via le catalogue • {{ currentSource() }}</div>
}
<button type="button" (click)="skipUnplayable(); closeMenu()" class="block w-full px-4 py-3 text-left text-sm text-slate-200 hover:bg-slate-800" role="menuitem">⏭ Passer (vidéo illisible)</button>
</div>
}
</div>
</div>
<!-- Rail d'actions OVERLAY façon YouTube (dans le cadre, comme sur mobile natif) -->
<div class="shorts-chrome absolute right-1.5 sm:right-2.5 bottom-32 sm:bottom-36 z-10 flex flex-col items-center gap-2 sm:gap-2.5" (click)="$event.stopPropagation()">
<div class="flex flex-col items-center">
<button type="button" (click)="toggleLike()" [disabled]="!isLoggedIn() || likeBusy()"
class="flex h-12 w-12 items-center justify-center rounded-full bg-black/55 text-white hover:bg-black/75 disabled:opacity-40"
[title]="isLoggedIn() ? (liked() ? 'Je n’aime plus' : 'J’aime') : 'Connectez-vous pour aimer'" aria-label="J’aime">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 drop-shadow" viewBox="0 0 24 24" [attr.fill]="liked() ? 'currentColor' : 'none'" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M7 10v12M15 5.88L14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88z"/></svg>
</button>
<span class="mt-0.5 text-[11px] font-medium text-white drop-shadow">J'aime</span>
</div>
<div class="flex flex-col items-center">
<a *ngIf="watchLink()" [routerLink]="watchLink()" [queryParams]="watchQueryParams()"
class="flex h-12 w-12 items-center justify-center rounded-full bg-black/55 text-white hover:bg-black/75"
title="Voir les commentaires dans Watch" aria-label="Commentaires">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 drop-shadow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</a>
<span class="mt-0.5 text-[11px] font-medium text-white drop-shadow">Commentaires</span>
</div>
<div class="flex flex-col items-center">
<button type="button" (click)="share()"
class="flex h-12 w-12 items-center justify-center rounded-full bg-black/55 text-white hover:bg-black/75"
title="Partager" aria-label="Partager">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 drop-shadow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.6" y1="13.5" x2="15.4" y2="17.5"/><line x1="15.4" y1="6.5" x2="8.6" y2="10.5"/></svg>
</button>
<span class="mt-0.5 text-[11px] font-medium text-white drop-shadow">Partager</span>
</div>
<div class="flex flex-col items-center">
<button type="button" (click)="toggleMute()"
class="flex h-12 w-12 items-center justify-center rounded-full bg-black/55 text-white hover:bg-black/75"
[title]="muted() ? 'Activer le son' : 'Couper le son'" aria-label="Activer ou couper le son">
@if (muted()) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 drop-shadow" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M11 5L6 9H2v6h4l5 4V5z"/><line x1="23" y1="9" x2="17" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="17" y1="9" x2="23" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
} @else {
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 drop-shadow" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M11 5L6 9H2v6h4l5 4V5z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M19 5a9 9 0 0 1 0 14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
}
</button>
<span class="mt-0.5 text-[11px] font-medium text-white drop-shadow">Son</span>
</div>
</div>
<!-- Bas : avatar + titre + méta + fraîcheur + S'abonner (safe-area incluse) -->
<div class="shorts-meta shorts-chrome pointer-events-none absolute left-0 right-0 bottom-0 z-10 p-3 sm:p-4 text-white bg-gradient-to-t from-black/85 via-black/35 to-transparent">
<div class="flex items-end gap-2.5 text-sm" (click)="$event.stopPropagation()">
@@ -233,9 +205,9 @@
<!-- Flèche suivant (bureau, façon YouTube) -->
<button type="button" (click)="next(); $event.stopPropagation()" [disabled]="!canNext()"
class="shorts-chrome chip hidden sm:flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20 disabled:opacity-30"
class="shorts-chrome chip hidden sm:flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-black/40 text-white ring-1 ring-white/15 hover:bg-black/60 disabled:opacity-30"
aria-label="Short suivant">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 10l5 5 5-5H7z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 10l5 5 5-5H7z"/></svg>
</button>
</div>
+14 -4
View File
@@ -16,6 +16,7 @@ import { Video } from '../../models/video.model';
import { shortsFeedAccepts, isPlayable as isPlayablePolicy, type PlayableContext } from '../../app/shared/utils/section-policy';
import { SHORT_TO_LONG, type ProviderId } from '../../app/shared/providers/provider-ids';
import { dailymotionEmbedBase } from '../../app/shared/providers/dailymotion-player';
import { PlayerMenuComponent } from '../../app/shared/components/player-menu/player-menu.component';
import { SearchService } from '../../app/search/search.service';
import { ChannelProviderFactory } from '../../app/core/providers/channel/channel-provider.factory';
import {
@@ -87,7 +88,7 @@ const SHORTS_QUERIES: Record<string, string[]> = {
selector: 'app-watch-short',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, RouterLink],
imports: [CommonModule, RouterLink, PlayerMenuComponent],
templateUrl: './watch-short.component.html'
})
export class WatchShortComponent implements OnDestroy {
@@ -274,6 +275,18 @@ export class WatchShortComponent implements OnDestroy {
return this.langFilter() === 'profile' ? `Filtre actif : ${base} (profil)` : `Filtre actif : ${base}`;
} catch { return 'Filtre de langues'; }
});
/** Libellé court du mode de langue pour le menu FAB (« Français »…). */
readonly langFilterShort = computed(() => {
switch (this.langFilter()) {
case 'profile': return 'Profil';
case 'fr': return 'Français';
case 'en': return 'Anglais';
case 'fren': return 'FR + EN';
case 'all': return 'Toutes';
default: return this.langFilter();
}
});
readonly current = computed<Video | null>(() => {
const list = this.items();
const i = this.index();
@@ -955,9 +968,6 @@ export class WatchShortComponent implements OnDestroy {
try { this.isFullscreen.set(!!document.fullscreenElement); } catch {}
};
toggleMenu(): void {
this.menuOpen.update(o => !o);
}
closeMenu(): void {
this.menuOpen.set(false);
}
+75 -104
View File
@@ -5,24 +5,82 @@
<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="group 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="chip max-md:opacity-100 absolute right-3 top-12 z-10 inline-flex items-center gap-1.5 rounded-full px-3 py-2 text-xs font-semibold shadow-lg transition-opacity focus-visible:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400"
[class.opacity-0]="!cinemaMode()" [class.group-hover:opacity-100]="!cinemaMode()" [class.group-focus-within:opacity-100]="!cinemaMode()"
[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" />
<section aria-label="Lecteur vidéo" class="group relative rounded-2xl bg-black shadow-2xl ring-1 ring-white/10">
<!-- Menu FAB : toutes les options du lecteur (cinéma, qualité, actions). -->
<app-player-menu [(open)]="menuOpen">
<button type="button" (click)="toggleCinema(); closeMenu()" class="pm-item" [attr.aria-pressed]="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 class="pm-item-label">{{ cinemaMode() ? 'Quitter le cinéma' : 'Mode cinéma' }}</span>
</button>
@if (availableQualities().length > 0) {
<div class="pm-sep"></div>
<div class="pm-section-label">Qualité</div>
<button type="button" (click)="onSelectQuality(''); closeMenu()" class="pm-item">
<span class="pm-item-label">Auto</span>
@if (!selectedQuality()) { <svg xmlns="http://www.w3.org/2000/svg" class="ml-auto h-4 w-4 text-white/80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg> }
</button>
@for (q of availableQualities(); track q) {
<button type="button" (click)="onSelectQuality(q); closeMenu()" class="pm-item">
<span class="pm-item-label">{{ q }}</span>
@if (selectedQuality() === q) { <svg xmlns="http://www.w3.org/2000/svg" class="ml-auto h-4 w-4 text-white/80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg> }
</button>
}
</svg>
<span>{{ cinemaMode() ? 'Quitter' : 'Cinéma' }}</span>
</button>
<div class="aspect-video">
}
<div class="pm-sep"></div>
@if (isLoggedIn()) {
<button type="button" (click)="toggleLike(); closeMenu()" class="pm-item" [disabled]="likeBusy()" [attr.aria-pressed]="liked()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" [attr.fill]="liked() ? 'currentColor' : 'none'" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21s-6.716-4.248-9.193-6.725A6 6 0 0112 5.414 6 6 0 0121.193 14.275C18.716 16.752 12 21 12 21z" />
</svg>
<span class="pm-item-label">{{ liked() ? 'Ne plus aimer' : 'J’aime' }}</span>
<span class="pm-item-hint">{{ formatNumber(v.likes) }}</span>
</button>
@if (likeError(); as lerr) {
<div class="px-3 py-1.5 text-[11px] text-red-300">{{ lerr }}</div>
}
<app-add-to-playlist [provider]="provider()" [videoId]="v.videoId" [title]="v.title" [thumbnail]="v.thumbnail" [menuItem]="true"></app-add-to-playlist>
}
<button type="button" (click)="toggleTranscript(); closeMenu()" class="pm-item" [attr.aria-expanded]="transcriptOpen()">
<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="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 0v12h8V4H6zm1 2a1 1 0 011-1h4a1 1 0 110 2H8a1 1 0 01-1-1zm0 3a1 1 0 011-1h4a1 1 0 110 2H8a1 1 0 01-1-1zm0 3a1 1 0 011-1h2a1 1 0 110 2H8a1 1 0 01-1-1z" clip-rule="evenodd" />
</svg>
<span class="pm-item-label">{{ transcriptOpen() ? 'Masquer la transcription' : 'Transcription' }}</span>
</button>
@if (isLoggedIn()) {
<button type="button" (click)="openDownloadPanel(); closeMenu()" class="pm-item">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M10 3a1 1 0 011 1v7l2.293-2.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 11V4a1 1 0 011-1z" />
<path d="M4 17a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1z" />
</svg>
<span class="pm-item-label">Télécharger</span>
</button>
}
<button type="button" (click)="copyLink()" class="pm-item">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M16 1a3 3 0 013 3v10a3 3 0 01-3 3H6a3 3 0 01-3-3V4a3 3 0 013-3h10zm0 2H6a1 1 0 00-1 1v10a1 1 0 001 1h10a1 1 0 001-1V4a1 1 0 00-1-1z"/>
<path d="M18 7h2a3 3 0 013 3v10a3 3 0 01-3 3H10a3 3 0 01-3-3v-2h2v2a1 1 0 001 1h10a1 1 0 001-1V10a1 1 0 00-1-1h-2V7z"/>
</svg>
<span class="pm-item-label">{{ linkCopied() ? 'Lien copié !' : 'Partager / Copier le lien' }}</span>
</button>
<a [href]="openOnProviderUrl() || '#'" target="_blank" rel="noopener noreferrer" (click)="closeMenu()" class="pm-item" [class.opacity-60]="!openOnProviderUrl()" title="Ouvrir sur le site du fournisseur">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><path d="M15 3h6v6"/><path d="M10 14L21 3"/></svg>
<span class="pm-item-label">Ouvrir chez le fournisseur</span>
</a>
</app-player-menu>
<div class="aspect-video overflow-hidden rounded-2xl">
@if (embedUrl(); as src) {
<iframe #embedFrame class="h-full w-full" [src]="src" frameborder="0" playsinline (load)="onEmbedFrameLoad()"
referrerpolicy="origin"
@@ -57,19 +115,6 @@
</div>
</div>
} @else {
@if (availableQualities().length > 0) {
<div class="mt-3 flex items-center gap-2 text-sm text-slate-400">
<label for="qualitySel" class="font-medium">Qualité</label>
<select id="qualitySel" #qsel class="rounded-lg border border-slate-700 bg-slate-800 px-2 py-1 text-sm text-slate-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400"
(change)="onSelectQuality(qsel.value)">
<option value="">Auto</option>
@for (q of availableQualities(); track q) {
<option [value]="q">{{ q }}</option>
}
</select>
</div>
}
<!-- Titre + métadonnées -->
<section class="mt-4" aria-label="Informations vidéo">
<h1 class="text-xl font-bold leading-snug text-white sm:text-2xl">{{ v.title || 'Vidéo sans titre' }}</h1>
@@ -142,80 +187,6 @@
@if (subServiceError(); as serr) {
<div class="mt-2 text-sm text-red-300" role="alert">{{ serr }}</div>
}
<!-- Barre d'actions -->
<div class="mt-3 flex flex-wrap items-center gap-2 border-t border-slate-700/60 pt-3" role="toolbar" aria-label="Actions vidéo">
<span class="inline-flex items-center gap-1.5 rounded-full bg-slate-700/70 px-3 py-2 text-sm text-slate-200" title="Mentions J'aime">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.787l.09.044a2 2 0 002.253-.334l.09-.092l3.535-3.536a2 2 0 00.586-1.414V5a2 2 0 00-2-2H9a2 2 0 00-2 2v5.333z" /></svg>
<span class="tabular-nums">{{ formatNumber(v.likes) }}</span>
</span>
@if (isLoggedIn()) {
<button (click)="toggleLike()" [disabled]="likeBusy()"
[attr.aria-pressed]="liked()" [title]="liked() ? 'Ne plus aimer' : 'J\'aime'"
class="inline-flex items-center gap-1.5 rounded-full px-3 py-2 text-sm font-semibold transition focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400 disabled:cursor-not-allowed disabled:opacity-60"
[class.bg-red-600]="liked()" [class.text-white]="liked()"
[class.bg-slate-700]="!liked()" [class.text-slate-200]="!liked()" [class.hover:bg-slate-600]="!liked()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" [attr.fill]="liked() ? 'currentColor' : 'none'" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21s-6.716-4.248-9.193-6.725A6 6 0 0112 5.414 6 6 0 0121.193 14.275C18.716 16.752 12 21 12 21z" />
</svg>
<span>{{ liked() ? 'Aimée' : 'J\'aime' }}</span>
</button>
@if (likeError(); as lerr) {
<span class="text-xs text-red-300" role="alert">{{ lerr }}</span>
}
}
@if (isLoggedIn()) {
<app-add-to-playlist [provider]="provider()" [videoId]="v.videoId" [title]="v.title" [thumbnail]="v.thumbnail"></app-add-to-playlist>
}
<button (click)="copyLink()" title="Copier le lien de la vidéo"
class="inline-flex items-center gap-1.5 rounded-full bg-slate-700 px-3 py-2 text-sm font-semibold text-slate-200 transition hover:bg-slate-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M16 1a3 3 0 013 3v10a3 3 0 01-3 3H6a3 3 0 01-3-3V4a3 3 0 013-3h10zm0 2H6a1 1 0 00-1 1v10a1 1 0 001 1h10a1 1 0 001-1V4a1 1 0 00-1-1z"/>
<path d="M18 7h2a3 3 0 013 3v10a3 3 0 01-3 3H10a3 3 0 01-3-3v-2h2v2a1 1 0 001 1h10a1 1 0 001-1V10a1 1 0 00-1-1h-2V7z"/>
</svg>
<span>{{ linkCopied() ? 'Lien copié !' : 'Partager' }}</span>
</button>
<button (click)="toggleTranscript()" [attr.aria-expanded]="transcriptOpen()" aria-controls="transcript-panel"
[title]="transcriptOpen() ? 'Masquer la transcription' : 'Afficher la transcription'"
class="inline-flex items-center gap-1.5 rounded-full px-3 py-2 text-sm font-semibold transition focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400"
[class.bg-red-600]="transcriptOpen()" [class.text-white]="transcriptOpen()"
[class.bg-slate-700]="!transcriptOpen()" [class.text-slate-200]="!transcriptOpen()" [class.hover:bg-slate-600]="!transcriptOpen()">
<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="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 0v12h8V4H6zm1 2a1 1 0 011-1h4a1 1 0 110 2H8a1 1 0 01-1-1zm0 3a1 1 0 011-1h4a1 1 0 110 2H8a1 1 0 01-1-1zm0 3a1 1 0 011-1h2a1 1 0 110 2H8a1 1 0 01-1-1z" clip-rule="evenodd" />
</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-2 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-2 text-sm font-semibold text-slate-200 transition hover:bg-slate-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M10 3a1 1 0 011 1v7l2.293-2.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 11V4a1 1 0 011-1z" />
<path d="M4 17a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1z" />
</svg>
<span>Télécharger</span>
</button>
}
<a [href]="openOnProviderUrl() || '#'" target="_blank" rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 rounded-full bg-slate-700 px-3 py-2 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.pointer-events-none]="!openOnProviderUrl()" [class.opacity-60]="!openOnProviderUrl()"
title="Ouvrir sur le site du fournisseur">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M10 3a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 11-2 0V5.414l-7.293 7.293a1 1 0 01-1.414-1.414L14.586 4H11a1 1 0 01-1-1z"/>
<path d="M5 4a1 1 0 011-1h3a1 1 0 110 2H7v8h8v-2a1 1 0 112 0v3a1 1 0 01-1 1H6a1 1 0 01-1-1V4z"/>
</svg>
<span>Ouvrir</span>
</a>
</div>
</section>
<!-- Description -->
+7 -1
View File
@@ -18,6 +18,7 @@ import { SubscriptionsService } from '../../services/subscriptions.service';
import type { ChannelMeta, ProviderId } from '../../app/shared/models/channel.model';
import { toShortProviderId } from '../../app/shared/providers/provider-ids';
import { dailymotionEmbedBase } from '../../app/shared/providers/dailymotion-player';
import { PlayerMenuComponent } from '../../app/shared/components/player-menu/player-menu.component';
import { SubscribeButtonComponent } from '../../app/shared/components/subscribe-button/subscribe-button.component';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { IframeProgressService } from '../../services/iframe-progress.service';
@@ -33,7 +34,7 @@ import { UserService } from '../../services/user.service';
templateUrl: './watch.component.html',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, VideoPlayerComponent, RouterLink, AddToPlaylistComponent, ProviderBadgeComponent, DurationPipe, SubscribeButtonComponent, ChannelIdentityComponent]
imports: [CommonModule, VideoPlayerComponent, RouterLink, AddToPlaylistComponent, ProviderBadgeComponent, DurationPipe, SubscribeButtonComponent, ChannelIdentityComponent, PlayerMenuComponent]
})
export class WatchComponent implements OnDestroy, AfterViewInit {
private route = inject(ActivatedRoute);
@@ -161,6 +162,11 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
showFullDescription = signal(false);
/** Mode cinéma : le lecteur prend toute la largeur de la page. */
cinemaMode = signal<boolean>(false);
/** Menu FAB (options du lecteur) partagé avec /shorts. */
menuOpen = signal(false);
closeMenu(): void {
this.menuOpen.set(false);
}
// Prefer full description if available; otherwise fallback to shortDescription
descriptionText = computed(() => {
const v = this.video();