feat(channel): page details chaine multi-providers avec onglets et infinite scroll
CI / build-and-test (push) Successful in 13m34s
CI / build-and-test (push) Successful in 13m34s
- Strategy Pattern front: ChannelProviderInterface + 6 providers (yt/dm/tw/pt/od/ru) + factory (ids longs/courts) - ChannelContentService: cache par onglet/tri/recherche (TTL 5min), pagination nextPage - ChannelPage: header (banniere/avatar/subs/description ...plus), tabs dynamiques par capabilities, grilles videos/shorts 9:16/playlists/live, recherche debounced, tri recent/populaire, infinite scroll IntersectionObserver, skeletons/empty/error - Backend: GET /api/channels/:provider/:id/content (videos/shorts/playlists/live) natif par provider
This commit is contained in:
@@ -1,10 +1,22 @@
|
||||
<div class="h-[calc(100vh-4rem)] w-full flex items-center justify-center p-2 sm:p-4"
|
||||
(wheel)="onWheel($event)" (touchstart)="onTouchStart($event)" (touchend)="onTouchEnd($event)">
|
||||
<div class="w-full flex 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="flex flex-wrap items-center justify-center gap-2" role="tablist" aria-label="Fournisseur des Shorts">
|
||||
<button *ngFor="let p of providersList()"
|
||||
(click)="selectProvider(p.id)"
|
||||
[attr.aria-pressed]="provider() === p.id"
|
||||
class="px-3 py-1.5 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'">
|
||||
{{ p.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="h-[calc(100vh-8rem)] w-full flex items-center justify-center"
|
||||
(wheel)="onWheel($event)" (touchstart)="onTouchStart($event)" (touchend)="onTouchEnd($event)">
|
||||
<!-- Center column -->
|
||||
<div class="relative">
|
||||
<!-- Video frame (9:16) -->
|
||||
<div class="rounded-xl overflow-hidden shadow-2xl ring-1 ring-slate-700/50 bg-black/90"
|
||||
style="height: calc(100vh - 6rem); max-height: calc(100vh - 6rem); aspect-ratio: 9 / 16; width: auto; max-width: 92vw;">
|
||||
style="height: calc(100vh - 10rem); max-height: calc(100vh - 10rem); aspect-ratio: 9 / 16; width: auto; max-width: 92vw;">
|
||||
@if (embedUrl(); as url) {
|
||||
<iframe [src]="url" class="w-full h-full" frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
@@ -12,8 +24,9 @@
|
||||
} @else if (loading()) {
|
||||
<div class="w-full h-full animate-pulse bg-slate-800"></div>
|
||||
} @else if (error()) {
|
||||
<div class="w-full h-full flex items-center justify-center text-slate-300">
|
||||
{{ error() }}
|
||||
<div class="w-full h-full flex flex-col gap-3 items-center justify-center text-slate-300 p-6 text-center">
|
||||
<span>{{ error() }}</span>
|
||||
<button (click)="retry()" class="px-4 py-2 rounded bg-slate-700 hover:bg-slate-600 text-white text-sm">Réessayer</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -22,10 +35,12 @@
|
||||
<div class="absolute left-0 right-0 bottom-0 p-3 sm:p-4 bg-gradient-to-t from-black/60 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 rounded-full object-cover">
|
||||
<div class="flex-1">
|
||||
<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="shrink-0 px-2 py-1 rounded bg-white/15 hover:bg-white/25 text-xs">Ouvrir</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -43,10 +58,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor"><path d="M7 10l5 5 5-5H7z"/></svg>
|
||||
</button>
|
||||
|
||||
<!-- Position indicator -->
|
||||
@if (items().length > 0) {
|
||||
<div class="absolute -left-14 top-6 hidden md:flex items-center justify-center px-2 py-1 rounded-full bg-slate-800/70 text-white text-xs ring-1 ring-slate-700/60">
|
||||
{{ index() + 1 }} / {{ items().length }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Mobile next/prev (bottom) -->
|
||||
<div class="flex md:hidden items-center justify-between mt-3">
|
||||
<button (click)="prev()" [disabled]="!canPrev()" class="px-3 py-2 rounded bg-slate-800/70 text-white disabled:opacity-40">Prev</button>
|
||||
<span *ngIf="items().length > 0" class="text-slate-400 text-sm">{{ index() + 1 }} / {{ items().length }}</span>
|
||||
<button (click)="next()" [disabled]="!canNext()" class="px-3 py-2 rounded bg-slate-800/70 text-white disabled:opacity-40">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user