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,11 +1,101 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DatePipe, DecimalPipe } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { SubscriptionsService, type SubscriptionItem } from '../../../services/subscriptions.service';
|
||||
import type { ProviderId } from '../../../app/shared/models/channel.model';
|
||||
import { ProviderBadgeComponent } from '../../../app/shared/components/provider-badge/provider-badge.component';
|
||||
import { SubscribeButtonComponent } from '../../../app/shared/components/subscribe-button/subscribe-button.component';
|
||||
|
||||
interface FilterChip {
|
||||
id: 'all' | ProviderId;
|
||||
label: string;
|
||||
}
|
||||
|
||||
type SortKey = 'alpha' | 'recent' | 'subs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-library-subscriptions',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, DatePipe, DecimalPipe, ProviderBadgeComponent, SubscribeButtonComponent],
|
||||
templateUrl: './subscriptions.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SubscriptionsComponent { }
|
||||
export class SubscriptionsComponent {
|
||||
private subs = inject(SubscriptionsService);
|
||||
private router = inject(Router);
|
||||
|
||||
filters: FilterChip[] = [
|
||||
{ id: 'all', label: 'Tous' },
|
||||
{ id: 'yt', label: 'YouTube' },
|
||||
{ id: 'dm', label: 'Dailymotion' },
|
||||
{ id: 'tw', label: 'Twitch' },
|
||||
{ id: 'pt', label: 'PeerTube' },
|
||||
{ id: 'od', label: 'Odysee' },
|
||||
{ id: 'ru', label: 'Rumble' },
|
||||
];
|
||||
|
||||
sorts: { key: SortKey; label: string }[] = [
|
||||
{ key: 'alpha', label: 'A → Z' },
|
||||
{ key: 'recent', label: 'Récents' },
|
||||
{ key: 'subs', label: 'Abonnés' },
|
||||
];
|
||||
|
||||
activeFilter = signal<FilterChip['id']>('all');
|
||||
activeSort = signal<SortKey>('alpha');
|
||||
|
||||
readonly items = computed(() => this.subs.items());
|
||||
readonly filteredItems = computed(() => this.applyFilters(this.items()));
|
||||
readonly hasCache = computed(() => this.items().length > 0);
|
||||
|
||||
constructor() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.subs.list().subscribe();
|
||||
}
|
||||
|
||||
setFilter(id: FilterChip['id']): void {
|
||||
this.activeFilter.set(id);
|
||||
}
|
||||
|
||||
setSort(key: SortKey): void {
|
||||
this.activeSort.set(key);
|
||||
}
|
||||
|
||||
openChannel(item: SubscriptionItem): void {
|
||||
if (!item?.channel?.provider || !item?.channel?.externalId) {
|
||||
return;
|
||||
}
|
||||
this.router.navigate(['/channel', item.channel.provider, item.channel.externalId]);
|
||||
}
|
||||
|
||||
private applyFilters(list: SubscriptionItem[]): SubscriptionItem[] {
|
||||
const filter = this.activeFilter();
|
||||
const sort = this.activeSort();
|
||||
let filtered = list;
|
||||
if (filter !== 'all') {
|
||||
filtered = filtered.filter((item) => item.channel?.provider === filter);
|
||||
}
|
||||
switch (sort) {
|
||||
case 'recent':
|
||||
filtered = [...filtered].sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
||||
break;
|
||||
case 'subs':
|
||||
filtered = [...filtered].sort(
|
||||
(a, b) => (b.channel?.subsCount ?? 0) - (a.channel?.subsCount ?? 0)
|
||||
);
|
||||
break;
|
||||
case 'alpha':
|
||||
default:
|
||||
filtered = [...filtered].sort((a, b) => {
|
||||
const ta = (a.channel?.title || a.channel?.handle || '').toLowerCase();
|
||||
const tb = (b.channel?.title || b.channel?.handle || '').toLowerCase();
|
||||
return ta.localeCompare(tb);
|
||||
});
|
||||
break;
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user