CI / build-and-test (push) Successful in 13m25s
- Nouveau app-channel-identity (avatar+nom -> /channel, fallback resolve /api/channels, initiales sinon) - ProviderBadge: accepte les ids courts yt/dm/tw/pt/od/ru - Search adapters yt/dm/pt/od: conservent channelExternalId/channelAvatarUrl (pt derive instance|channel) - video-card/themes/watch/related/suggestions/subscriptions/channel-header uniformises sur l'identite partagee
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
import { Component, Input, computed } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { RouterLink } from '@angular/router';
|
|
import { PROVIDERS, PROVIDER_MAP, type ProviderId } from '../../app/core/providers/provider-registry';
|
|
import type { SuggestionItem } from '../../app/search/search.service';
|
|
import { ChannelIdentityComponent } from '../../app/shared/components/channel-identity/channel-identity.component';
|
|
|
|
interface SuggestionSection {
|
|
id: ProviderId;
|
|
name: string;
|
|
shortLabel: string;
|
|
colorClass: string;
|
|
items: SuggestionItem[];
|
|
supported: boolean;
|
|
deepLinkProviders: string;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'app-search-suggestions',
|
|
standalone: true,
|
|
imports: [CommonModule, RouterLink, ChannelIdentityComponent],
|
|
templateUrl: './search-suggestions.component.html'
|
|
})
|
|
export class SearchSuggestionsComponent {
|
|
@Input() groups: Record<ProviderId, SuggestionItem[]> = {
|
|
yt: [], dm: [], tw: [], pt: [], od: [], ru: []
|
|
} as any;
|
|
@Input() query: string = '';
|
|
// Providers that were actually requested; drives which sections render and
|
|
// which unsupported providers get an explicit row (Step 6)
|
|
@Input() activeProviders: ProviderId[] | 'all' = 'all';
|
|
|
|
readonly sections = computed<SuggestionSection[]>(() => { const groups = this.groups || ({} as Record<ProviderId, SuggestionItem[]>);
|
|
const active = this.activeProviders === 'all'
|
|
? PROVIDERS.map(p => p.id)
|
|
: (this.activeProviders || []).filter(id => PROVIDER_MAP.has(id));
|
|
return active.map(id => {
|
|
const spec = PROVIDER_MAP.get(id)!;
|
|
return {
|
|
id,
|
|
name: spec.displayName,
|
|
shortLabel: spec.shortLabel,
|
|
colorClass: spec.colorClass,
|
|
items: Array.isArray(groups[id]) ? groups[id] : [],
|
|
supported: !!spec.supports?.search,
|
|
deepLinkProviders: id,
|
|
} as SuggestionSection;
|
|
});
|
|
});
|
|
|
|
/** ExternalId chaîne pour le lien /channel (null = non cliquable). */
|
|
channelExternalId(section: { id: ProviderId }, item: SuggestionItem): string | null {
|
|
const raw = item as unknown as Record<string, unknown>;
|
|
if (section.id === 'pt') {
|
|
// PeerTube : `instance|channel`, instance dérivée de l'URL vidéo.
|
|
try {
|
|
const url = String(raw?.['url'] || '');
|
|
const host = url ? new URL(url).hostname : '';
|
|
const channel = String(raw?.['channelId'] || '');
|
|
if (host && channel) return `${host}|${channel}`;
|
|
} catch {}
|
|
return null;
|
|
}
|
|
if (section.id === 'od') {
|
|
const name = String(raw?.['uploaderName'] || '');
|
|
return name || null;
|
|
}
|
|
const ext = String(raw?.['channelExternalId'] || raw?.['channelId'] || '');
|
|
return ext || null;
|
|
}
|
|
|
|
channelAvatar(item: SuggestionItem): string | null {
|
|
const raw = item as unknown as Record<string, unknown>;
|
|
return String(raw?.['uploaderAvatar'] || '') || null;
|
|
}
|
|
}
|