feat(preferences): section langues a telecharger (defaut fr+en), transcripts filtres cote serveur et watch
CI / build-and-test (push) Successful in 13m50s
CI / build-and-test (push) Successful in 13m50s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- Loading initial : squelettes -->
|
||||
<div *ngIf="loading" class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<!-- Loading initial : squelettes (uniquement si rien n'est encore affiché) -->
|
||||
<div *ngIf="loading && videos.length === 0" class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<app-video-card-skeleton *ngFor="let _ of [0,1,2,3,4,5,6,7]"></app-video-card-skeleton>
|
||||
</div>
|
||||
|
||||
@@ -13,19 +13,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Vide -->
|
||||
<div *ngIf="!loading && !error && videos.length === 0" class="flex flex-col items-center gap-2 rounded-xl border border-zinc-800 bg-zinc-900/50 p-10 text-center">
|
||||
<div *ngIf="!loading && !loadingMore && !error && videos.length === 0" class="flex flex-col items-center gap-2 rounded-xl border border-zinc-800 bg-zinc-900/50 p-10 text-center">
|
||||
<h3 class="text-lg font-semibold text-white">{{ emptyMessage }}</h3>
|
||||
<p class="text-sm text-zinc-400">Essayez un autre onglet ou revenez plus tard.</p>
|
||||
</div>
|
||||
|
||||
<!-- Grille vidéos classiques -->
|
||||
<div *ngIf="!loading && !error && videos.length > 0 && layout === 'video'" class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<app-video-card *ngFor="let video of videos" [video]="video"></app-video-card>
|
||||
<!-- Grille vidéos classiques (reste montée pendant le chargement suivant) -->
|
||||
<div *ngIf="!error && videos.length > 0 && layout === 'video'" class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<app-video-card *ngFor="let video of videos; trackBy: trackById" [video]="video"></app-video-card>
|
||||
</div>
|
||||
|
||||
<!-- Grille shorts verticale 9:16 -->
|
||||
<div *ngIf="!loading && !error && videos.length > 0 && layout === 'short'" class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
|
||||
<a *ngFor="let video of videos" [routerLink]="['/watch', video.id]" [queryParams]="{ p: video.provider }"
|
||||
<div *ngIf="!error && videos.length > 0 && layout === 'short'" class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
|
||||
<a *ngFor="let video of videos; trackBy: trackById" [routerLink]="['/watch', video.id]" [queryParams]="{ p: video.provider }"
|
||||
class="group overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900">
|
||||
<div class="aspect-[9/16] w-full overflow-hidden bg-zinc-800">
|
||||
<img [src]="video.thumbnailUrl" [alt]="video.title" loading="lazy" class="h-full w-full object-cover transition-transform group-hover:scale-105" />
|
||||
@@ -37,10 +37,14 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Sentinelle infinite scroll -->
|
||||
<div *ngIf="hasMore && !loading && !error" #sentinel class="flex justify-center py-6" aria-hidden="true">
|
||||
<div *ngIf="loadingMore" class="flex items-center gap-2 text-sm text-zinc-400">
|
||||
<!-- Standard infinite scroll : sentinelle partagée + spinner + fin -->
|
||||
<div *ngIf="loadingMore && hasMore && !error" class="flex justify-center py-6" aria-hidden="true">
|
||||
<div class="flex items-center gap-2 text-sm text-zinc-400">
|
||||
<span class="h-5 w-5 animate-spin rounded-full border-2 border-zinc-700 border-t-red-500"></span>
|
||||
Chargement…
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!loading && !loadingMore && !hasMore && !error && videos.length > 0" class="flex justify-center py-4 text-xs text-zinc-500">
|
||||
Fin des résultats
|
||||
</div>
|
||||
<app-infinite-anchor *ngIf="hasMore && !loading && !error" [busy]="loadingMore" (loadMore)="loadMore.emit()"></app-infinite-anchor>
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import type { VideoItem } from '../../../app/shared/models/video-item.model';
|
||||
import { VideoCardComponent } from '../../../app/shared/components/video-card/video-card.component';
|
||||
import { VideoCardSkeletonComponent } from '../../../app/shared/components/video-card-skeleton/video-card-skeleton.component';
|
||||
import { InfiniteAnchorComponent } from '../../shared/infinite-anchor/infinite-anchor.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-channel-video-grid',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink, VideoCardComponent, VideoCardSkeletonComponent],
|
||||
imports: [CommonModule, RouterLink, VideoCardComponent, VideoCardSkeletonComponent, InfiniteAnchorComponent],
|
||||
templateUrl: './channel-video-grid.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ChannelVideoGridComponent implements AfterViewInit, OnChanges, OnDestroy {
|
||||
export class ChannelVideoGridComponent {
|
||||
@Input() videos: VideoItem[] = [];
|
||||
@Input() loading = false;
|
||||
@Input() loadingMore = false;
|
||||
@@ -36,34 +31,7 @@ export class ChannelVideoGridComponent implements AfterViewInit, OnChanges, OnDe
|
||||
@Output() loadMore = new EventEmitter<void>();
|
||||
@Output() retry = new EventEmitter<void>();
|
||||
|
||||
@ViewChild('sentinel', { static: false }) sentinel?: ElementRef<HTMLDivElement>;
|
||||
private observer: IntersectionObserver | null = null;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.observe();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['hasMore'] || changes['videos']) {
|
||||
// Ré-observe après chaque rendu (sentinelle recréée par *ngIf)
|
||||
queueMicrotask(() => this.observe());
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
|
||||
private observe(): void {
|
||||
this.observer?.disconnect();
|
||||
const el = this.sentinel?.nativeElement;
|
||||
if (!el) return;
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries.some((e) => e.isIntersecting)) this.loadMore.emit();
|
||||
},
|
||||
{ rootMargin: '600px' },
|
||||
);
|
||||
this.observer.observe(el);
|
||||
trackById(_index: number, v: VideoItem): string {
|
||||
return String(v?.id ?? _index);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user