feat(channel): page details chaine multi-providers avec onglets et infinite scroll
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:
2026-09-25 10:15:12 -04:00
parent fe35340948
commit 97e5de74f5
147 changed files with 7954 additions and 77497 deletions
+21 -3
View File
@@ -11,7 +11,8 @@ import { TranslatePipe } from '../../pipes/translate.pipe';
import { SearchResultGridComponent } from 'src/app/shared/components/search-result-grid/search-result-grid.component';
import { VideoItem } from 'src/app/shared/models/video-item.model';
import { SearchService, type SearchResponse } from '../../app/search/search.service';
import type { ProviderId } from '../../app/core/providers/provider-registry';
import { UserService, type UserPreferences } from '../../services/user.service';
import { PROVIDERS, type ProviderId } from '../../app/core/providers/provider-registry';
import { Observable, Subscription } from 'rxjs';
@Component({
@@ -29,6 +30,7 @@ export class SearchComponent implements AfterViewInit {
private history = inject(HistoryService);
private title = inject(Title);
private unified = inject(SearchService);
private users = inject(UserService);
q = signal<string>('');
loading = signal<boolean>(false);
@@ -36,6 +38,8 @@ export class SearchComponent implements AfterViewInit {
providerParam = signal<Provider | null>(null);
themeParam = signal<string | null>(null);
providersListParam = signal<ProviderId[] | 'all' | null>(null);
// Step 7: default providers loaded from user preferences (server), applied when URL has none
private defaultProvidersPref = signal<ProviderId[] | null>(null);
pageParam = signal<number>(1);
sortParam = signal<'relevance' | 'date' | 'views'>('relevance');
@@ -258,6 +262,20 @@ export class SearchComponent implements AfterViewInit {
this.title.setTitle(this.pageHeading());
});
// Step 7: load default providers preference once (server) for deep-link fallback
try {
this.users.loadPreferences().subscribe({
next: (prefs: UserPreferences) => {
const list = (prefs as any)?.defaultProviders;
if (Array.isArray(list)) {
const valid = list.filter((id: string) => PROVIDERS.some(p => p.id === id)) as ProviderId[];
if (valid.length > 0) this.defaultProvidersPref.set(valid);
}
},
error: () => {} // anonymous users: no preference
});
} catch {}
// Subscribe once to unified multi-provider results and reflect to UI
const subscription: Subscription = this.unified.request$
.pipe(takeUntilDestroyed())
@@ -330,8 +348,8 @@ export class SearchComponent implements AfterViewInit {
const provider = prov || this.instances.selectedProvider();
this.filterTag.set(provider === 'twitch' ? 'twitch_all' : 'all');
// Use unified search in all cases
const providersToUse = providersList || [provider as ProviderId];
// Use unified search in all cases (URL providers > user default preference > single provider)
const providersToUse = providersList || this.defaultProvidersPref() || [provider as ProviderId];
this.unified.setQuery(q);
this.unified.setProviders(providersToUse);