feat(channel): identite chaine partagee cliquable partout + logos fiabilises
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
This commit is contained in:
2026-09-25 10:37:36 -04:00
parent 97e5de74f5
commit 579354cdb3
20 changed files with 355 additions and 159 deletions
@@ -119,8 +119,8 @@
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-x-6 gap-y-8">
<div *ngFor="let v of videos(); trackBy: trackByVideo" class="group">
<a [routerLink]="['/watch', v.videoId]" [queryParams]="watchQueryParams(v)" [state]="{ video: v }" class="block bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300 transform hover:-translate-y-1 shadow-lg hover:shadow-xl">
<div *ngFor="let v of videos(); trackBy: trackByVideo" class="group bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300 transform hover:-translate-y-1 shadow-lg hover:shadow-xl">
<a [routerLink]="['/watch', v.videoId]" [queryParams]="watchQueryParams(v)" [state]="{ video: v }" class="block">
<div class="relative">
<img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full aspect-video object-cover transition-transform duration-300 group-hover:scale-105">
<div class="absolute top-2 right-2">
@@ -137,17 +137,22 @@
<app-like-button [videoId]="v.videoId" [provider]="provider()" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button>
</div>
</div>
<div class="p-3">
<div class="p-3 pb-1">
<h3 class="font-semibold text-slate-100 group-hover:text-red-400 transition-colors duration-200 text-sm line-clamp-2 pr-2">{{ v.title }}</h3>
<div class="mt-2 flex items-center space-x-2 text-xs text-slate-400">
<img [src]="v.uploaderAvatar" [alt]="v.uploaderName" class="w-6 h-6 rounded-full">
<span>{{ v.uploaderName }}</span>
</div>
<div class="mt-1.5 text-xs text-slate-400">
<span>{{ v.views | number }} visionnements</span> &bull; <span>{{ v.uploadedDate | date:'shortDate' }}</span>
</div>
</div>
</a>
<div class="px-3 pb-3">
<app-channel-identity
size="sm"
[provider]="v.provider || provider()"
[externalId]="v.channelExternalId ?? null"
[avatarUrl]="v.channelAvatarUrl || v.uploaderAvatar || null"
[name]="v.uploaderName || null"
></app-channel-identity>
<div class="mt-1.5 text-xs text-slate-400">
<span>{{ v.views | number }} visionnements</span> &bull; <span>{{ v.uploadedDate | date:'shortDate' }}</span>
</div>
</div>
</div>
</div>
@@ -10,6 +10,7 @@ import { TranslatePipe } from '../../pipes/translate.pipe';
import { LikeButtonComponent } from '../shared/components/like-button/like-button.component';
import { DurationPipe } from '../../app/shared/pipes/duration.pipe';
import { ProviderBadgeComponent } from '../../app/shared/components/provider-badge/provider-badge.component';
import { ChannelIdentityComponent } from '../../app/shared/components/channel-identity/channel-identity.component';
import { classifyVideo, isShortVideo } from '../../app/shared/utils/video-kind';
import { map, of, switchMap, catchError, Observable } from 'rxjs';
import { PagedResult } from '../../services/youtube-api.service';
@@ -46,7 +47,7 @@ const TWITCH_THEME_KEYWORDS: Record<string, string[]> = {
standalone: true,
templateUrl: './provider-theme-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, FormsModule, RouterLink, TranslatePipe, LikeButtonComponent, DurationPipe, ProviderBadgeComponent]
imports: [CommonModule, FormsModule, RouterLink, TranslatePipe, LikeButtonComponent, DurationPipe, ProviderBadgeComponent, ChannelIdentityComponent]
})
export class ProviderThemePageComponent implements OnDestroy {
private route = inject(ActivatedRoute);
+33 -23
View File
@@ -43,32 +43,42 @@
<!-- Grid -->
<div *ngIf="!b.loading && !b.error && filteredItems(b).length" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
<a *ngFor="let v of filteredItems(b)"
[routerLink]="['/watch', v.videoId]"
[queryParams]="watchQueryParams(b.provider, v)"
[state]="{ video: v }"
class="group bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300">
<div class="relative">
<img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full h-40 object-cover">
<div class="absolute top-2 right-2">
<app-provider-badge [provider]="b.provider"></app-provider-badge>
<div *ngFor="let v of filteredItems(b)"
class="group bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300">
<a [routerLink]="['/watch', v.videoId]"
[queryParams]="watchQueryParams(b.provider, v)"
[state]="{ video: v }"
class="block">
<div class="relative">
<img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full h-40 object-cover">
<div class="absolute top-2 right-2">
<app-provider-badge [provider]="b.provider"></app-provider-badge>
</div>
<div class="absolute top-2 left-2 flex gap-1">
<span *ngIf="kindOf(v, b.provider) === 'short'" class="bg-red-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">SHORT</span>
<span *ngIf="kindOf(v, b.provider) === 'live'" class="bg-purple-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">LIVE</span>
</div>
<div class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-0.5 rounded" *ngIf="v.duration && v.duration > 0">
{{ v.duration | duration }}
</div>
<div class="absolute bottom-2 left-2">
<app-like-button [videoId]="v.videoId" [provider]="b.provider" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button>
</div>
</div>
<div class="absolute top-2 left-2 flex gap-1">
<span *ngIf="kindOf(v, b.provider) === 'short'" class="bg-red-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">SHORT</span>
<span *ngIf="kindOf(v, b.provider) === 'live'" class="bg-purple-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">LIVE</span>
</div>
<div class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-0.5 rounded" *ngIf="v.duration && v.duration > 0">
{{ v.duration | duration }}
</div>
<div class="absolute bottom-2 left-2">
<app-like-button [videoId]="v.videoId" [provider]="b.provider" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button>
<div class="p-3 pb-1">
<h4 class="font-semibold line-clamp-2">{{ v.title }}</h4>
</div>
</a>
<div class="px-3 pb-3">
<app-channel-identity
size="sm"
[provider]="v.provider || b.provider"
[externalId]="v.channelExternalId ?? null"
[avatarUrl]="v.channelAvatarUrl || v.uploaderAvatar || null"
[name]="v.uploaderName || null"
></app-channel-identity>
</div>
<div class="p-3">
<h4 class="font-semibold line-clamp-2">{{ v.title }}</h4>
<div class="text-sm text-slate-400 mt-1">{{ v.uploaderName }}</div>
</div>
</a>
</div>
</div>
</section>
</div>
@@ -8,6 +8,7 @@ import { Video } from '../../models/video.model';
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 { ChannelIdentityComponent } from '../../app/shared/components/channel-identity/channel-identity.component';
import { DurationPipe } from '../../app/shared/pipes/duration.pipe';
import { classifyVideo, isShortVideo } from '../../app/shared/utils/video-kind';
@@ -24,7 +25,7 @@ interface ProviderBlock {
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './theme-page.component.html',
imports: [CommonModule, RouterLink, TranslatePipe, LikeButtonComponent, ProviderBadgeComponent, DurationPipe]
imports: [CommonModule, RouterLink, TranslatePipe, LikeButtonComponent, ProviderBadgeComponent, DurationPipe, ChannelIdentityComponent]
})
export class ThemePageComponent {
private route = inject(ActivatedRoute);