Files
NewTube/src/components/search/search-suggestions.component.ts
T
bruno 579354cdb3
CI / build-and-test (push) Successful in 13m25s
feat(channel): identite chaine partagee cliquable partout + logos fiabilises
- 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
2026-09-25 10:37:36 -04:00

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;
}
}