feat(preferences): section langues a telecharger (defaut fr+en), transcripts filtres cote serveur et watch
CI / build-and-test (push) Successful in 13m50s

This commit is contained in:
2026-09-26 11:49:56 -04:00
parent 1b0a7eb3ce
commit 655061ba2c
46 changed files with 2018 additions and 380 deletions
@@ -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);
}
}