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:
@@ -9,6 +9,7 @@ import { TranslatePipe } from '../../pipes/translate.pipe';
|
||||
import { LikeButtonComponent } from '../shared/components/like-button/like-button.component';
|
||||
import { ProviderBadgeComponent } from '../../app/shared/components/provider-badge/provider-badge.component';
|
||||
import { DurationPipe } from '../../app/shared/pipes/duration.pipe';
|
||||
import { classifyVideo, isShortVideo } from '../../app/shared/utils/video-kind';
|
||||
|
||||
interface ProviderBlock {
|
||||
provider: Provider;
|
||||
@@ -39,6 +40,30 @@ export class ThemePageComponent {
|
||||
|
||||
blocks = signal<ProviderBlock[]>([]);
|
||||
|
||||
// Filtre d'affichage Vidéos / Shorts (les Shorts ne sont plus mélangés sans contrôle).
|
||||
readonly contentFilter = signal<'all' | 'videos' | 'shorts'>('all');
|
||||
|
||||
setContentFilter(v: 'all' | 'videos' | 'shorts'): void {
|
||||
this.contentFilter.set(v);
|
||||
}
|
||||
|
||||
/** Items du bloc après application du filtre Vidéos/Shorts. */
|
||||
filteredItems(b: ProviderBlock): Video[] {
|
||||
const f = this.contentFilter();
|
||||
if (f === 'all') return b.items;
|
||||
return b.items.filter(v =>
|
||||
f === 'shorts' ? isShortVideo({ ...v, provider: (v.provider as string) || b.provider }) : !isShortVideo({ ...v, provider: (v.provider as string) || b.provider })
|
||||
);
|
||||
}
|
||||
|
||||
kindOf(v: Video, provider: Provider): string {
|
||||
return classifyVideo({ ...v, provider: (v.provider as string) || provider });
|
||||
}
|
||||
|
||||
shortsCount(b: ProviderBlock): number {
|
||||
return b.items.filter(v => isShortVideo({ ...v, provider: (v.provider as string) || b.provider })).length;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.route.paramMap.subscribe(pm => {
|
||||
const slug = pm.get('theme') || pm.get('slug') || 'trending';
|
||||
@@ -115,6 +140,35 @@ export class ThemePageComponent {
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Twitch / live : les top streams live garantissent un bloc rempli
|
||||
// (la recherche de chaînes "live" est trop restrictive).
|
||||
if (block.provider === 'twitch' && slug === 'live') {
|
||||
this.api.getTwitchTopStreamsPage(null).subscribe({
|
||||
next: res => {
|
||||
block.items = (res.items || []).slice(0, max);
|
||||
block.loading = false;
|
||||
if (!block.items.length) console.info('[ThemePage] Empty block page', { provider: block.provider, theme: slug, mode: 'twitch_top_streams' });
|
||||
this.blocks.update(a => [...a]);
|
||||
},
|
||||
error: () => { block.error = 'Erreur de chargement'; block.loading = false; this.blocks.update(a => [...a]); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Twitch hors live/trending : VODs + clips + fallback top streams.
|
||||
if (block.provider === 'twitch') {
|
||||
const q = tokens.join(' ');
|
||||
const call = q ? this.api.searchTwitchThemePage(q, null) : this.api.getTwitchTopStreamsPage(null);
|
||||
call.subscribe({
|
||||
next: res => {
|
||||
block.items = (res.items || []).slice(0, max);
|
||||
block.loading = false;
|
||||
if (!block.items.length) console.info('[ThemePage] Empty block page', { provider: block.provider, theme: slug, query: q });
|
||||
this.blocks.update(a => [...a]);
|
||||
},
|
||||
error: () => { block.error = 'Erreur de chargement'; block.loading = false; this.blocks.update(a => [...a]); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
const q = tokens.join(' ');
|
||||
if (!q) {
|
||||
// fallback to trending
|
||||
|
||||
Reference in New Issue
Block a user