fix(twitch,rumble,shorts): replis serveur, embed Rumble autoritaire, refonte UX Shorts
CI / build-and-test (push) Successful in 14m15s

This commit is contained in:
2026-09-29 10:39:03 -04:00
parent 3fe05b3c30
commit f76a3795c2
6 changed files with 530 additions and 90 deletions
+153 -78
View File
@@ -1,6 +1,6 @@
<div class="flex w-full flex-col items-center gap-3 p-2 sm:p-4">
<!-- Sélecteur de fournisseur : la page n'est plus bloquée sur un seul provider -->
<div class="scroll-rail no-scrollbar flex max-w-full items-center justify-start gap-2 sm:justify-center" role="tablist" aria-label="Fournisseur des Shorts">
<div class="shorts-stage flex w-full flex-col items-center gap-3 rounded-2xl bg-black/60 p-2 sm:p-4" (click)="closeMenu()">
<!-- Sélecteur : Pour toi + fournisseurs -->
<div class="scroll-rail no-scrollbar flex max-w-full items-center justify-start gap-2 sm:justify-center" role="tablist" aria-label="Fournisseur des Shorts" (click)="$event.stopPropagation()">
<button (click)="toggleForYou()"
[attr.aria-pressed]="forYou()"
class="chip shrink-0 px-3 py-2 rounded-full text-sm border transition-colors"
@@ -10,84 +10,159 @@
</button>
<button *ngFor="let p of providersList()"
(click)="selectProvider(p.id)"
[attr.aria-pressed]="provider() === p.id"
[attr.aria-pressed]="!forYou() && provider() === p.id"
class="chip shrink-0 px-3 py-2 rounded-full text-sm border transition-colors"
[class]="provider() === p.id ? 'bg-red-600 border-red-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300 hover:bg-slate-700'">
[class]="!forYou() && provider() === p.id ? 'bg-red-600 border-red-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300 hover:bg-slate-700'">
{{ p.label }}
</button>
</div>
<!-- Player area: height is derived from dvh *and* vw so a 9:16 frame never
overflows either a portrait phone or a landscape one. -->
<div class="flex w-full min-h-0 items-center justify-center gap-2 sm:gap-3"
<!-- Scène immersive : flèches + cadre 9:16 + rail d'actions -->
<div class="flex w-full min-h-0 items-center justify-center gap-1 sm:gap-3"
(wheel)="onWheel($event)" (touchstart)="onTouchStart($event)" (touchend)="onTouchEnd($event)">
<!-- Center column : cadre 9:16 + rail d'actions (jamais en dehors de l'écran) -->
<div class="relative flex w-full min-w-0 items-end sm:items-center justify-center gap-2 sm:gap-3">
<!-- Video frame (9:16) -->
<div class="shorts-frame relative shrink-0 rounded-xl overflow-hidden shadow-2xl ring-1 ring-slate-700/50 bg-black/90">
@if (embedUrl(); as url) {
<iframe [src]="url" [title]="current()?.title || 'Short video'" class="absolute inset-0 w-full h-full" frameborder="0" playsinline
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe>
} @else if (loading()) {
<div class="absolute inset-0 flex flex-col gap-3 items-center justify-center bg-slate-800 text-slate-400 text-sm">
<div class="h-8 w-8 rounded-full border-2 border-slate-600 border-t-red-500 animate-spin" aria-hidden="true"></div>
<span role="status">Chargement des Shorts…</span>
</div>
} @else if (error()) {
<div class="absolute inset-0 flex flex-col gap-3 items-center justify-center text-slate-300 p-6 text-center">
<span>{{ error() }}</span>
<button (click)="retry()" class="chip px-4 py-3 rounded bg-slate-700 hover:bg-slate-600 text-white text-sm">Réessayer</button>
</div>
}
<!-- Bottom overlay: meta. pointer-events-none pour ne jamais bloquer
les boutons du lecteur (play / volume / plein écran). Seul le
lien "Ouvrir" reste cliquable. -->
@if (current(); as v) {
<div class="pointer-events-none absolute left-0 right-0 bottom-0 p-3 sm:p-4 bg-gradient-to-t from-black/70 to-transparent text-white">
<div class="flex items-center gap-3 text-sm">
<img [src]="v.uploaderAvatar" [alt]="v.uploaderName" class="h-8 w-8 shrink-0 rounded-full object-cover">
<div class="flex-1 min-w-0">
<div class="font-semibold line-clamp-1">{{ v.title }}</div>
<div class="opacity-80 line-clamp-1">@{{ v.uploaderName }}</div>
</div>
<a *ngIf="watchLink()" [routerLink]="watchLink()" [queryParams]="watchQueryParams()"
class="chip pointer-events-auto shrink-0 px-3 py-2 rounded bg-white/20 hover:bg-white/30 text-xs">Ouvrir</a>
</div>
</div>
}
</div>
<!-- Rail d'actions façon YouTube Shorts (à droite du cadre 9:16) -->
<div class="flex shrink-0 flex-col items-center gap-2 pb-1">
<a *ngIf="providerUrl()" [href]="providerUrl()" target="_blank" rel="noopener"
class="flex h-11 w-11 items-center justify-center rounded-full bg-slate-800/80 text-white hover:bg-slate-700"
title="Ouvrir chez le fournisseur" aria-label="Ouvrir chez le fournisseur">
<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="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
</a>
<button type="button" (click)="skipUnplayable()"
class="flex h-11 w-11 items-center justify-center rounded-full bg-slate-800/80 text-white hover:bg-slate-700"
title="Vidéo illisible ? Passer à la suivante" aria-label="Passer la vidéo illisible">
<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="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
</button>
</div>
@if (busyMore()) {
<div class="absolute top-2 left-1/2 -translate-x-1/2 px-3 py-2 rounded-full bg-black/70 text-white text-xs" role="status">Chargement…</div>
}
</div>
</div>
<!-- Navigation. Position indicator is always visible (it is the only progress
feedback); the prev/next arrows are inset inside the page instead of
parked outside the frame at `-right-14`, which used to overflow. -->
<div class="flex w-full max-w-lg items-center justify-between gap-3">
<button type="button" (click)="prev()" [disabled]="!canPrev()"
class="chip shrink-0 rounded-lg bg-slate-800/80 px-4 py-3 text-white disabled:opacity-40"
<!-- Flèche précédent (bureau, façon YouTube) -->
<button type="button" (click)="prev(); $event.stopPropagation()" [disabled]="!canPrev()"
class="chip hidden sm:flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-slate-800/80 text-white hover:bg-slate-700 disabled:opacity-30"
aria-label="Short précédent">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 14l5-5 5 5H7z"/></svg>
<span class="ml-1">Préc.</span>
<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>
</button>
<!-- Center column : cadre 9:16 + rail d'actions -->
<div class="relative flex w-full min-w-0 items-end sm:items-center justify-center gap-2 sm:gap-3" style="max-width: 560px;">
<!-- Video frame (9:16) -->
<div #shortsFrame class="shorts-frame relative shrink-0 rounded-xl overflow-hidden shadow-2xl ring-1 ring-slate-700/50 bg-black">
@if (embedUrl(); as url) {
<iframe [src]="url" [title]="current()?.title || 'Short video'" class="absolute inset-0 w-full h-full" frameborder="0" playsinline
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe>
} @else if (loading()) {
<div class="absolute inset-0 flex flex-col gap-3 items-center justify-center bg-slate-900 text-slate-400 text-sm">
<div class="h-8 w-8 rounded-full border-2 border-slate-600 border-t-red-500 animate-spin" aria-hidden="true"></div>
<span role="status">Chargement des Shorts…</span>
</div>
} @else if (error()) {
<div class="absolute inset-0 flex flex-col gap-3 items-center justify-center text-slate-300 p-6 text-center">
<span>{{ error() }}</span>
<button (click)="retry(); $event.stopPropagation()" class="chip px-4 py-3 rounded bg-slate-700 hover:bg-slate-600 text-white text-sm">Réessayer</button>
</div>
}
@if (current(); as v) {
<!-- Haut droite : plein écran, partage rapide, menu ⋯ -->
<div class="absolute top-2 right-2 flex items-center gap-1.5" (click)="$event.stopPropagation()">
<button type="button" (click)="toggleFullscreen()" class="flex h-9 w-9 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-9 w-9 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-9 w-9 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-10 z-20 w-56 overflow-hidden rounded-xl bg-slate-900/95 py-1 shadow-2xl ring-1 ring-slate-700" role="menu" (click)="$event.stopPropagation()">
<a *ngIf="providerUrl()" [href]="providerUrl()" target="_blank" rel="noopener" (click)="closeMenu()" class="block px-4 py-2.5 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-2.5 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-2.5 text-left text-sm text-slate-200 hover:bg-slate-800" role="menuitem">{{ shareCopied() ? 'Lien copié ✓' : 'Copier le lien' }}</button>
<button type="button" (click)="skipUnplayable(); closeMenu()" class="block w-full px-4 py-2.5 text-left text-sm text-slate-200 hover:bg-slate-800" role="menuitem">⏭ Passer (vidéo illisible)</button>
</div>
}
</div>
</div>
<!-- Bas droite : sourdine -->
<div class="absolute bottom-14 right-2" (click)="$event.stopPropagation()">
<button type="button" (click)="toggleMute()" class="flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white hover:bg-black/80" [title]="muted() ? 'Activer le son (reprend au début)' : 'Couper le son (reprend au début)'" aria-label="Activer ou 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>
}
</button>
</div>
<!-- Bas gauche : titre + chaîne + S'abonner (pointer-events ciblés) -->
<div class="pointer-events-none absolute left-0 right-0 bottom-0 p-3 sm:p-4 pb-2.5 bg-gradient-to-t from-black/80 via-black/30 to-transparent text-white">
<div class="flex items-end gap-3 text-sm" (click)="$event.stopPropagation()">
<img [src]="v.uploaderAvatar" [alt]="v.uploaderName" class="h-9 w-9 shrink-0 rounded-full object-cover ring-1 ring-white/30">
<div class="flex-1 min-w-0">
<div class="font-semibold line-clamp-2 leading-snug">{{ v.title }}</div>
<div class="opacity-80 line-clamp-1 text-[13px]">@{{ v.uploaderName }}</div>
</div>
@if (isLoggedIn() && currentChannelKey()) {
<button type="button" (click)="toggleSubscribe()" [disabled]="subBusy()"
class="chip pointer-events-auto shrink-0 px-4 py-2 rounded-full text-[13px] font-semibold transition-colors disabled:opacity-50"
[class]="isSubscribed() ? 'bg-slate-700 text-slate-200 hover:bg-slate-600' : 'bg-white text-black hover:bg-slate-200'">
{{ isSubscribed() ? 'Abonné' : "S'abonner" }}
</button>
}
</div>
<!-- Barre de progression fine (YouTube, temps réel) -->
@if (ytProgress() != null) {
<div class="mt-2 h-0.5 w-full overflow-hidden rounded bg-white/20" aria-hidden="true">
<div class="h-full bg-red-600 transition-[width]" [style.width.%]="(ytProgress() || 0) * 100"></div>
</div>
}
</div>
}
@if (shareCopied()) {
<div class="absolute top-2 left-1/2 -translate-x-1/2 px-3 py-1.5 rounded-full bg-black/70 text-white text-xs" role="status">Lien copié ✓</div>
}
</div>
<!-- Rail d'interaction (façon YouTube) -->
<div class="flex shrink-0 flex-col items-center gap-2 sm:gap-3 pb-1" (click)="$event.stopPropagation()">
<div class="flex flex-col items-center">
<button type="button" (click)="toggleLike()" [disabled]="!isLoggedIn() || likeBusy()"
class="flex h-11 w-11 sm:h-12 sm:w-12 items-center justify-center rounded-full bg-slate-800/90 text-white hover:bg-slate-700 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-5 w-5" 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] text-slate-300">J'aime</span>
</div>
<div class="flex flex-col items-center">
<a *ngIf="watchLink()" [routerLink]="watchLink()" [queryParams]="watchQueryParams()"
class="flex h-11 w-11 sm:h-12 sm:w-12 items-center justify-center rounded-full bg-slate-800/90 text-white hover:bg-slate-700"
title="Voir les commentaires dans Watch" aria-label="Commentaires">
<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="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] text-slate-300">Commentaires</span>
</div>
<div class="flex flex-col items-center">
<button type="button" (click)="share()"
class="flex h-11 w-11 sm:h-12 sm:w-12 items-center justify-center rounded-full bg-slate-800/90 text-white hover:bg-slate-700"
title="Partager" aria-label="Partager">
<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="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] text-slate-300">Partager</span>
</div>
</div>
</div>
<!-- Flèche suivant (bureau, façon YouTube) -->
<button type="button" (click)="next(); $event.stopPropagation()" [disabled]="!canNext()"
class="chip hidden sm:flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-slate-800/80 text-white hover:bg-slate-700 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>
</button>
</div>
@if (busyMore()) {
<div class="px-3 py-1.5 rounded-full bg-black/70 text-white text-xs" role="status">Chargement…</div>
}
<!-- Navigation fine (mobile + compteur) -->
<div class="flex w-full items-center justify-between gap-3" style="max-width: 560px;">
<button type="button" (click)="prev()" [disabled]="!canPrev()"
class="chip shrink-0 rounded-lg bg-slate-800/80 px-4 py-2.5 text-white text-sm disabled:opacity-40"
aria-label="Short précédent">
<span>↑ Préc.</span>
</button>
@if (items().length > 0) {
<span class="shrink-0 text-sm tabular-nums text-slate-400" role="status" aria-live="polite">
@@ -97,12 +172,12 @@
<span class="shrink-0"></span>
}
<button type="button" (click)="next()" [disabled]="!canNext()"
class="chip shrink-0 rounded-lg bg-slate-800/80 px-4 py-3 text-white disabled:opacity-40"
class="chip shrink-0 rounded-lg bg-slate-800/80 px-4 py-2.5 text-white text-sm disabled:opacity-40"
aria-label="Short suivant">
<span class="mr-1">Suiv.</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 10l5 5 5-5H7z"/></svg>
<span>Suiv. ↓</span>
</button>
</div>
@if (forYou() && (topics().length > 0 || topicsLoading())) {
<p class="max-w-lg text-center text-xs text-slate-400" role="status" aria-live="polite">
@if (topicsLoading()) {
@@ -112,5 +187,5 @@
}
</p>
}
<p class="max-w-lg text-center text-xs text-slate-500">Si une vidéo affiche une erreur (153, Forbidden, introuvable…), utilisez ⏭ Passer : elle est retirée du flux.</p>
<p class="max-w-lg text-center text-xs text-slate-500">Molette, flèches ↑ ↓ ou balayage pour zapper. Son coupé par défaut (🔇) : l'activer reprend la vidéo au début. Vidéo illisible ? Menu ⋯ → Passer.</p>
</div>