style(ui): harmonisation des couleurs par thème + contour fournisseur 1 px
CI / build-and-test (push) Successful in 15m1s
CI / build-and-test (push) Successful in 15m1s
Balayage de la couche d'overrides de index.css : les 75 jetons neutres slate/zinc/gray qui n'étaient pas couverts (36 fichiers) sont mappés sur les variables du thème — fonds, textes, bordures, anneaux, divide, placeholders, dégradés + variants hover/focus/focus-within/disabled/group-hover, alpha via color-mix. Barre de recherche, menus, panneaux, cartes et résultats de recherche suivent donc le thème appliqué (dark/light/blue/black) au lieu de la palette slate figée. Volontairement exclus : bg-white/text-black (boutons inversés), pastilles « inversées » conditionnelles au thème clair, chrome au-dessus des vidéos, rouges sémantiques. Panneau d'informations ⓘ des Shorts re-colorié dans son propre scope (fond/bordures/textes/pastilles d'action → panel/text/muted), les mêmes tokens restant blancs ailleurs (rail, méta). Marqueur border-l-4 des titres de page sur --accent. Cartes vidéo harmonisées : titre text-sm font-semibold leading-snug (résultats de recherche, chaîne, thèmes), méta text-xs, rayon rounded-lg sur les cadres, pastille de durée sans font-mono, titre « Abonnements » avec le marqueur d'accent. Contour fournisseur 1 px autour de chaque cadre vidéo, à la couleur du premier stop du dégradé de la pastille (table unique en CSS, ids longs + courts) : résultats de recherche, grille chaîne, pages thèmes, watch (lecteur + « à suivre »), shorts, historique, aimés/à regarder plus tard, détail de playlist. Tests verts (search, filters, kind, section, highlight, shorts-catalog, api 41/41) ; vérif UI Playwright desktop 1280x900 + mobile 390x844 sur 4 thèmes : 250 assertions + 13 couleurs de contour au vert.
This commit is contained in:
@@ -245,6 +245,7 @@ Ajoutez au besoin `log-driver`, `log-opts`, `default-address-pools`, etc.
|
||||
* ✅ Liste **« À regarder plus tard »** (watch later) — même stockage « tags » que les likes (aucune migration), bouton horloge à côté du cœur sur toutes les cartes, entrée « Vous » de la barre latérale, page `/#/library/watch-later` avec recherche serveur
|
||||
* ✅ PeerTube **multi-instances** (activer/désactiver, set active)
|
||||
* ✅ Thème (système / dark / light / blue / black)
|
||||
* ✅ **Harmonisation visuelle** — tous les utilitaires neutres (`slate`/`zinc`/`gray`, fonds, textes, bordures, anneaux, placeholders, états `hover:`/`disabled:`) sont mappés sur les variables du thème (**75 jetons, 36 fichiers**) : barre de recherche, menus, panneaux et cartes suivent enfin le thème appliqué ; panneau **ⓘ des Shorts** re-colorié dans son périmètre (le chrome au-dessus de la vidéo reste blanc) ; marqueur `border-l-4` des titres de page sur l'accent du thème ; cartes vidéo harmonisées (`text-sm` / `font-semibold` / méta `text-xs` / rayon `rounded-lg`, pastille de durée sans `font-mono`) ; **contour fournisseur 1 px** à la couleur de la pastille autour de chaque cadre vidéo (grilles, recherche, channel, watch — lecteur et « à suivre » —, shorts, historique, aimés, playlist)
|
||||
* ✅ Préférences (langue, thème, région, qualité par défaut)
|
||||
* ✅ Auth légère (JWT), rate-limit API
|
||||
* ✅ **Recherche unifiée multi-providers** — panneau de filtres (sources / type / période / durée / tri, Ctrl/⌘+Maj+F), autocomplete `@` + opérateurs `live:`/`today:`, deep-links `?providers=…&type=…&period=…`, préférence `defaultProviders` persistée, filtres serveur, télémétrie minimale, a11y (focus trap, Esc, clavier complet sur le panneau de suggestions)
|
||||
|
||||
@@ -459,6 +459,291 @@ body {
|
||||
color: #94a3b8; /* slate-400 */
|
||||
}
|
||||
|
||||
/* --- Harmonisation : TOUS les utilitaires neutres suivent le thème -------- */
|
||||
/* Le bloc ci-dessus ne couvrait que 15 classes ; le reste de l'app parlait
|
||||
« slate/zinc/gray » quand le thème parlait --panel/--text (36 fichiers, 75
|
||||
jetons). Ici : familles slate/zinc/gray -> variables du thème, alpha ->
|
||||
color-mix, variants hover/focus/disabled/placeholders/ring/divide inclus.
|
||||
Règle : !important gagne sur l'utilitaire Tailwind (injecté après ou avant
|
||||
selon le CDN). VOLONTAIREMENT exclus — ils fonctionnent déjà dans tous les
|
||||
thèmes et/ou sont conditionnels au thème clair : bg-white/text-black (boutons
|
||||
inversés), bg-slate-50/100/200 + text-slate-700/800/900 (pastilles
|
||||
« inversées » de la barre de filtres, bouton Google, avatar clair, barre de
|
||||
thèmes en thème clair), bg-black/* + text-white au-dessus des vidéos. */
|
||||
|
||||
.bg-gray-900,
|
||||
.bg-slate-950,
|
||||
.bg-zinc-900,
|
||||
.bg-zinc-950 {
|
||||
background-color: var(--bg) !important;
|
||||
}
|
||||
|
||||
.bg-zinc-800 {
|
||||
background-color: var(--panel) !important;
|
||||
}
|
||||
|
||||
.bg-slate-600 {
|
||||
background-color: var(--panel-2) !important;
|
||||
}
|
||||
|
||||
.disabled\:bg-slate-800:disabled {
|
||||
background-color: var(--panel) !important;
|
||||
}
|
||||
|
||||
.bg-slate-900\/40 {
|
||||
background-color: color-mix(in srgb, var(--bg) 40%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-slate-900\/50,
|
||||
.bg-zinc-900\/50 {
|
||||
background-color: color-mix(in srgb, var(--bg) 50%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-slate-900\/60 {
|
||||
background-color: color-mix(in srgb, var(--bg) 60%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-slate-900\/95,
|
||||
.bg-slate-950\/95,
|
||||
.bg-zinc-950\/95 {
|
||||
background-color: color-mix(in srgb, var(--bg) 95%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-slate-800\/40 {
|
||||
background-color: color-mix(in srgb, var(--panel) 40%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-slate-800\/60 {
|
||||
background-color: color-mix(in srgb, var(--panel) 60%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-slate-800\/70 {
|
||||
background-color: color-mix(in srgb, var(--panel) 70%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-slate-800\/80,
|
||||
.bg-zinc-800\/80 {
|
||||
background-color: color-mix(in srgb, var(--panel) 80%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-slate-800\/90 {
|
||||
background-color: color-mix(in srgb, var(--panel) 90%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-slate-100\/10 {
|
||||
background-color: color-mix(in srgb, var(--text) 10%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-slate-700\/50 {
|
||||
background-color: color-mix(in srgb, var(--panel-2) 50%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-slate-700\/60 {
|
||||
background-color: color-mix(in srgb, var(--panel-2) 60%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-slate-700\/70 {
|
||||
background-color: color-mix(in srgb, var(--panel-2) 70%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-zinc-700\/80 {
|
||||
background-color: color-mix(in srgb, var(--panel-2) 80%, transparent) !important;
|
||||
}
|
||||
|
||||
.bg-slate-700\/90 {
|
||||
background-color: color-mix(in srgb, var(--panel-2) 90%, transparent) !important;
|
||||
}
|
||||
|
||||
.focus-within\:bg-slate-800\/60:focus-within {
|
||||
background-color: color-mix(in srgb, var(--panel) 60%, transparent) !important;
|
||||
}
|
||||
|
||||
.hover\:bg-slate-800\/60:hover {
|
||||
background-color: color-mix(in srgb, var(--panel) 60%, transparent) !important;
|
||||
}
|
||||
|
||||
.hover\:bg-slate-700\/40:hover {
|
||||
background-color: color-mix(in srgb, var(--panel-2) 40%, transparent) !important;
|
||||
}
|
||||
|
||||
.hover\:bg-slate-700\/50:hover {
|
||||
background-color: color-mix(in srgb, var(--panel-2) 50%, transparent) !important;
|
||||
}
|
||||
|
||||
.hover\:bg-slate-700\/60:hover {
|
||||
background-color: color-mix(in srgb, var(--panel-2) 60%, transparent) !important;
|
||||
}
|
||||
|
||||
.hover\:bg-slate-700\/80:hover {
|
||||
background-color: color-mix(in srgb, var(--panel-2) 80%, transparent) !important;
|
||||
}
|
||||
|
||||
.hover\:bg-slate-500:hover,
|
||||
.hover\:bg-slate-600:hover,
|
||||
.hover\:bg-slate-700:hover,
|
||||
.hover\:bg-slate-800:hover,
|
||||
.hover\:bg-zinc-600:hover {
|
||||
background-color: color-mix(in srgb, var(--panel-2) 90%, var(--text)) !important;
|
||||
}
|
||||
|
||||
.text-gray-400,
|
||||
.text-slate-500,
|
||||
.text-slate-600,
|
||||
.text-zinc-400,
|
||||
.text-zinc-500,
|
||||
.text-zinc-600 {
|
||||
color: var(--muted) !important;
|
||||
}
|
||||
|
||||
.placeholder-slate-400::placeholder,
|
||||
.placeholder-slate-500::placeholder,
|
||||
.placeholder\:text-slate-500::placeholder,
|
||||
.placeholder\:text-zinc-500::placeholder {
|
||||
color: var(--muted) !important;
|
||||
}
|
||||
|
||||
.disabled\:text-slate-400:disabled {
|
||||
color: var(--muted) !important;
|
||||
}
|
||||
|
||||
.group:hover .group-hover\:text-slate-200,
|
||||
.text-zinc-100,
|
||||
.text-zinc-200,
|
||||
.text-zinc-300 {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.hover\:text-slate-100:hover,
|
||||
.hover\:text-slate-200:hover,
|
||||
.hover\:text-zinc-400:hover {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.border-slate-700\/60 {
|
||||
border-color: color-mix(in srgb, var(--border) 60%, transparent) !important;
|
||||
}
|
||||
|
||||
.border-slate-500,
|
||||
.border-slate-600,
|
||||
.border-slate-800,
|
||||
.border-zinc-700,
|
||||
.border-zinc-800,
|
||||
.border-zinc-950,
|
||||
.divide-slate-700,
|
||||
.divide-slate-800 {
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
|
||||
.hover\:border-slate-400:hover,
|
||||
.hover\:border-slate-500:hover,
|
||||
.hover\:border-slate-600:hover,
|
||||
.hover\:border-zinc-500:hover,
|
||||
.hover\:border-zinc-600:hover {
|
||||
border-color: var(--muted) !important;
|
||||
}
|
||||
|
||||
.ring-slate-500\/50,
|
||||
.ring-slate-600\/50 {
|
||||
--tw-ring-color: color-mix(in srgb, var(--border) 50%, transparent) !important;
|
||||
}
|
||||
|
||||
.focus-visible\:ring-slate-400\/60:focus-visible {
|
||||
--tw-ring-color: var(--accent) !important;
|
||||
}
|
||||
|
||||
.from-slate-800 {
|
||||
--tw-gradient-from: var(--panel) !important; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
|
||||
}
|
||||
|
||||
.from-slate-700 {
|
||||
--tw-gradient-from: var(--panel-2) !important; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
|
||||
}
|
||||
|
||||
.to-slate-800\/50 {
|
||||
--tw-gradient-to: color-mix(in srgb, var(--panel) 50%, transparent) !important;
|
||||
}
|
||||
|
||||
.to-slate-800 {
|
||||
--tw-gradient-to: var(--panel) !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:bg-slate-900\/70 { background-color: color-mix(in srgb, var(--bg) 70%, transparent) !important; }
|
||||
}
|
||||
|
||||
/* Marqueur des titres de page (barre `border-l-4`) : suit l'accent du thème
|
||||
au lieu d'être figé en rouge (et rattrape les titres qui n'avaient pas de
|
||||
couleur explicite, ex. Bibliothèque → Aimés). */
|
||||
.border-l-4 { border-left-color: var(--accent) !important; }
|
||||
|
||||
/* --- Shorts : le panneau d'informations (ⓘ) suit le thème ----------------- */
|
||||
/* Ses couleurs sont du « verre blanc » (bg-white/*, text-white/*) : les mêmes
|
||||
jetons servent AU-DESSUS de la vidéo (rail, méta) et doivent y rester
|
||||
blancs — donc pas de reprise globale, uniquement un scope sur l'aside. */
|
||||
[data-theme] aside.shorts-info {
|
||||
background-color: color-mix(in srgb, var(--panel) 94%, transparent) !important;
|
||||
border-color: var(--border) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
[data-theme] aside.shorts-info [class*="border-white/"] { border-color: var(--border) !important; }
|
||||
[data-theme] aside.shorts-info [class*="bg-white/"] {
|
||||
background-color: color-mix(in srgb, var(--panel-2) 75%, transparent) !important;
|
||||
}
|
||||
[data-theme] aside.shorts-info .hover\:bg-white\/20:hover,
|
||||
[data-theme] aside.shorts-info .hover\:bg-white\/25:hover {
|
||||
background-color: color-mix(in srgb, var(--panel-2) 90%, var(--text)) !important;
|
||||
}
|
||||
[data-theme] aside.shorts-info .bg-white { background-color: var(--text) !important; }
|
||||
[data-theme] aside.shorts-info .text-black { color: var(--bg) !important; }
|
||||
[data-theme] aside.shorts-info .hover\:bg-slate-200:hover {
|
||||
background-color: color-mix(in srgb, var(--text) 85%, var(--bg)) !important;
|
||||
}
|
||||
[data-theme] aside.shorts-info [class*="ring-white/"] { --tw-ring-color: var(--border) !important; }
|
||||
[data-theme] aside.shorts-info .text-white { color: var(--text) !important; }
|
||||
[data-theme] aside.shorts-info .text-white\/40,
|
||||
[data-theme] aside.shorts-info .text-white\/45,
|
||||
[data-theme] aside.shorts-info .text-white\/50,
|
||||
[data-theme] aside.shorts-info .text-white\/60 { color: var(--muted) !important; }
|
||||
[data-theme] aside.shorts-info .text-white\/75,
|
||||
[data-theme] aside.shorts-info .text-white\/80 {
|
||||
color: color-mix(in srgb, var(--text) 85%, transparent) !important;
|
||||
}
|
||||
/* Pastilles d'action du panneau (définies en dur en rgba blanc plus haut). */
|
||||
[data-theme] aside.shorts-info .shorts-info-action {
|
||||
color: var(--muted) !important;
|
||||
background-color: color-mix(in srgb, var(--panel-2) 70%, transparent) !important;
|
||||
}
|
||||
[data-theme] aside.shorts-info .shorts-info-action:hover:not(:disabled) {
|
||||
color: var(--text) !important;
|
||||
background-color: color-mix(in srgb, var(--panel-2) 95%, var(--text)) !important;
|
||||
}
|
||||
[data-theme] aside.shorts-info .shorts-info-action--on {
|
||||
color: var(--text) !important;
|
||||
background-color: color-mix(in srgb, var(--panel-2) 90%, var(--text)) !important;
|
||||
}
|
||||
|
||||
/* --- Contour fournisseur autour de chaque cadre vidéo --------------------- */
|
||||
/* Contour fin (1 px) à la couleur de la pastille du fournisseur, sur toutes les
|
||||
présentations de vidéo. La couleur = le PREMIER stop du dégradé de
|
||||
app-provider-badge : c'est la seule table à tenir si une marque change.
|
||||
`data-video-provider` accepte l'id long ET l'id court (les deux circulent :
|
||||
VideoItem.provider est long, ChannelMeta.provider est court).
|
||||
`outline` plutôt que `border` : zéro effet sur la boîte (les cadres sont
|
||||
dimensionnés en aspect-ratio) et le rayon suit automatiquement. */
|
||||
[data-video-provider] { outline: 1px solid var(--prov, transparent); outline-offset: -1px; }
|
||||
[data-video-provider="youtube"],
|
||||
[data-video-provider="yt"] { --prov: #dc2626; }
|
||||
[data-video-provider="dailymotion"],
|
||||
[data-video-provider="dm"] { --prov: #0284c7; }
|
||||
[data-video-provider="twitch"],
|
||||
[data-video-provider="tw"] { --prov: #7c3aed; }
|
||||
[data-video-provider="peertube"],
|
||||
[data-video-provider="pt"] { --prov: #f59e0b; }
|
||||
[data-video-provider="rumble"],
|
||||
[data-video-provider="ru"] { --prov: #059669; }
|
||||
[data-video-provider="odysee"],
|
||||
[data-video-provider="od"] { --prov: #ec4899; }
|
||||
|
||||
/* Hide scrollbars on horizontal lists where we still want scrolling */
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Phase 7.2 : la frame prend le ratio reel quand width/height sont connus
|
||||
([style] porte aspect-ratio + largeur bornee), 16:9 sinon. Le
|
||||
`object-cover` reste car une verticale peut deja etre rognee en amont. -->
|
||||
<div class="relative w-full overflow-hidden rounded-lg bg-zinc-800" [style]="frameStyle">
|
||||
<div class="relative w-full overflow-hidden rounded-lg bg-zinc-800" [style]="frameStyle" [attr.data-video-provider]="video.provider">
|
||||
<!-- Frame en cours de chargement : shimmer + spinner (plus de frame vide) -->
|
||||
<div *ngIf="!thumbLoaded() && !thumbFailed()" class="absolute inset-0 animate-pulse bg-zinc-800/80" aria-hidden="true">
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
@@ -58,7 +58,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-base font-semibold text-zinc-100 line-clamp-2" [title]="video.title">
|
||||
<h3 class="text-sm font-semibold leading-snug text-zinc-100 line-clamp-2" [title]="video.title">
|
||||
{{ video.title }}
|
||||
</h3>
|
||||
<div class="text-xs text-zinc-400 flex flex-wrap gap-x-2 gap-y-0.5">
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
<li *ngFor="let t of filteredTranscripts()" class="bg-slate-700/50 rounded-lg overflow-hidden">
|
||||
<div class="p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0 w-20 h-12 bg-slate-700 rounded overflow-hidden flex items-center justify-center">
|
||||
<div class="flex-shrink-0 w-20 h-12 bg-slate-700 rounded overflow-hidden flex items-center justify-center" [attr.data-video-provider]="t.provider">
|
||||
@if (t.thumbnail) { <img [src]="t.thumbnail" alt="" class="w-full h-full object-cover" /> }
|
||||
@else { <span class="text-slate-500 text-xs">TXT</span> }
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<img [src]="video.thumbnailUrl" [alt]="video.title" loading="lazy" class="h-full w-full object-cover transition-transform group-hover:scale-105" />
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<h3 class="line-clamp-2 text-sm font-medium text-zinc-100">{{ video.title }}</h3>
|
||||
<h3 class="line-clamp-2 text-sm font-semibold text-zinc-100">{{ video.title }}</h3>
|
||||
<p *ngIf="video.viewCount" class="mt-1 text-xs text-zinc-400">{{ video.viewCount | number }} vues</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-x-6 gap-y-8">
|
||||
@for (video of trendingVideos(); track video.videoId) {
|
||||
<a [routerLink]="routeForVideo(video)" [queryParams]="watchQueryParams(video)" [state]="{ video }" class="group flex flex-col bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300 transform hover:-translate-y-1">
|
||||
<div class="relative">
|
||||
<div class="relative rounded-lg overflow-hidden" [attr.data-video-provider]="video.provider || instances.selectedProvider()">
|
||||
<img [src]="video.thumbnail" [alt]="video.title" class="w-full h-48 object-cover transition-transform duration-300 group-hover:scale-105">
|
||||
<div class="absolute top-2 right-2">
|
||||
<app-provider-badge [provider]="video.provider || instances.selectedProvider()"></app-provider-badge>
|
||||
@@ -37,13 +37,13 @@
|
||||
</div>
|
||||
<div class="p-4 flex-grow flex flex-col">
|
||||
<div class="flex justify-between items-start">
|
||||
<h3 class="font-semibold text-slate-100 group-hover:text-red-400 transition-colors duration-200 line-clamp-2 pr-2">{{ video.title }}</h3>
|
||||
<h3 class="text-sm font-semibold leading-snug text-slate-100 group-hover:text-red-400 transition-colors duration-200 line-clamp-2 pr-2">{{ video.title }}</h3>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center space-x-3 text-sm text-slate-400">
|
||||
<img [src]="video.uploaderAvatar" [alt]="video.uploaderName" class="w-8 h-8 rounded-full">
|
||||
<span>{{ video.uploaderName }}</span>
|
||||
</div>
|
||||
<div class="mt-auto pt-2 text-sm text-slate-400">
|
||||
<div class="mt-auto pt-2 text-xs text-slate-400">
|
||||
<span>{{ formatViews(video.views) }} visionnements</span> • <span>{{ formatRelative(video.uploadedDate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<a [routerLink]="['/watch', it.video_id]" [queryParams]="{ p: it.provider }" class="block p-4 hover:no-underline">
|
||||
<div class="flex items-center">
|
||||
<!-- Miniature de la vidéo -->
|
||||
<div class="relative flex-shrink-0 w-24 h-16 bg-slate-800 rounded overflow-hidden mr-4">
|
||||
<div class="relative flex-shrink-0 w-24 h-16 bg-slate-800 rounded overflow-hidden mr-4" [attr.data-video-provider]="it.provider">
|
||||
@if (it.thumbnail) {
|
||||
<img [src]="it.thumbnail" alt="" class="w-full h-full object-cover" />
|
||||
} @else {
|
||||
|
||||
@@ -59,7 +59,7 @@ import { AuthService } from '../../../services/auth.service';
|
||||
<div class="space-y-3">
|
||||
<div *ngFor="let it of items(); let i = index" class="flex flex-wrap items-center gap-3 p-2 rounded border border-slate-700 bg-slate-800/60 sm:flex-nowrap">
|
||||
<div class="w-28 flex-shrink-0">
|
||||
<img [src]="it.thumbnail || DEFAULT_PLAYLIST_IMAGE" [alt]="it.title || 'Video thumbnail'" class="w-28 h-16 object-cover rounded">
|
||||
<img [src]="it.thumbnail || DEFAULT_PLAYLIST_IMAGE" [alt]="it.title || 'Video thumbnail'" class="w-28 h-16 object-cover rounded" [attr.data-video-provider]="it.provider">
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 basis-[calc(100%_-_7rem)]">
|
||||
<a [routerLink]="['/watch', it.videoId]" [queryParams]="{ p: it.provider }" class="break-anywhere font-semibold text-slate-100 hover:text-red-400 line-clamp-2">{{ it.title || it.videoId }}</a>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="container mx-auto p-4 sm:p-6">
|
||||
<div class="mb-6 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-slate-100 sm:text-3xl">Abonnements</h2>
|
||||
<h2 class="border-l-4 border-red-500 pl-4 text-2xl font-bold text-slate-100 sm:text-3xl">Abonnements</h2>
|
||||
<p class="text-sm text-slate-400">Organisez vos chaînes suivies en groupes thématiques</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<!-- Video frame (9:16). `translateY` pendant un glissement tactile : tout
|
||||
le chrome (rail, méta, FAB) reste solidaire de l'image. -->
|
||||
<div #shortsFrame class="shorts-frame relative shrink-0 overflow-hidden bg-black rounded-none ring-0 shadow-none sm:rounded-2xl sm:ring-1 sm:ring-white/10 sm:shadow-2xl"
|
||||
[attr.data-video-provider]="providerOf(current())"
|
||||
[style.transform]="dragOffset() ? 'translateY(' + dragOffset() + 'px)' : null">
|
||||
@if (embedUrl(); as url) {
|
||||
<iframe [src]="url" [title]="current()?.title || 'Short video'" class="absolute inset-0 w-full h-full" [class.embed-wide]="wideEmbed()" frameborder="0" playsinline
|
||||
|
||||
@@ -396,8 +396,9 @@ export class WatchShortComponent implements OnDestroy {
|
||||
this.index() < this.items().length - 1 || (!this.forYou() && (!!this.nextCursor() || this.hasMoreQueries()))
|
||||
);
|
||||
|
||||
/** Provider effectif d'une vidéo : en mode "Pour toi", chaque item garde son fournisseur. */
|
||||
private providerOf(v: Video | null): Provider {
|
||||
/** Provider effectif d'une vidéo : en mode "Pour toi", chaque item garde son fournisseur.
|
||||
* Exposé au template (contour de couleur du cadre sur /shorts). */
|
||||
providerOf(v: Video | null): Provider {
|
||||
if (this.forYou() && v) {
|
||||
const p = String((v as any).provider || '').toLowerCase();
|
||||
if (p === 'youtube' || p === 'dailymotion' || p === 'twitch' || p === 'peertube' || p === 'rumble' || p === 'odysee') {
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
<div *ngIf="!showTwitchSectionsAll()" 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 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">
|
||||
<div class="relative rounded-lg overflow-hidden" [attr.data-video-provider]="provider()">
|
||||
<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">
|
||||
<app-provider-badge [provider]="provider()"></app-provider-badge>
|
||||
@@ -116,7 +116,7 @@
|
||||
<span *ngIf="!isLiveCard(v) && v._kind === 'short' && !isClipCard(v)" class="bg-red-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">SHORT</span>
|
||||
<span *ngIf="isChannelCard(v)" class="bg-slate-700/90 text-slate-200 text-[11px] font-bold px-1.5 py-0.5 rounded ring-1 ring-slate-500/50">CHAÎNE</span>
|
||||
</div>
|
||||
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md font-mono">
|
||||
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md">
|
||||
{{ v.duration | duration }}
|
||||
</div>
|
||||
<div *ngIf="isLiveCard(v) && v.views > 0" class="absolute bottom-2 left-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md">
|
||||
@@ -159,7 +159,7 @@
|
||||
<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 twitchLives(); 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">
|
||||
<div class="relative rounded-lg overflow-hidden" [attr.data-video-provider]="provider()">
|
||||
<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"><app-provider-badge [provider]="provider()"></app-provider-badge></div>
|
||||
<div class="absolute top-2 left-2"><span class="bg-purple-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">LIVE</span></div>
|
||||
@@ -181,10 +181,10 @@
|
||||
<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 twitchVods(); 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">
|
||||
<div class="relative rounded-lg overflow-hidden" [attr.data-video-provider]="provider()">
|
||||
<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"><app-provider-badge [provider]="provider()"></app-provider-badge></div>
|
||||
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md font-mono">{{ v.duration | duration }}</div>
|
||||
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md">{{ v.duration | duration }}</div>
|
||||
<div class="absolute bottom-2 left-2 flex items-center gap-1"><app-like-button [videoId]="v.videoId" [provider]="provider()" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button><app-like-button [list]="'watch-later'" [videoId]="v.videoId" [provider]="provider()" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button></div>
|
||||
</div>
|
||||
<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>
|
||||
@@ -205,11 +205,11 @@
|
||||
<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 twitchClips(); 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">
|
||||
<div class="relative rounded-lg overflow-hidden" [attr.data-video-provider]="provider()">
|
||||
<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"><app-provider-badge [provider]="provider()"></app-provider-badge></div>
|
||||
<div class="absolute top-2 left-2"><span class="bg-red-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">CLIP</span></div>
|
||||
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md font-mono">{{ v.duration | duration }}</div>
|
||||
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md">{{ v.duration | duration }}</div>
|
||||
<div class="absolute bottom-2 left-2 flex items-center gap-1"><app-like-button [videoId]="v.videoId" [provider]="provider()" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button><app-like-button [list]="'watch-later'" [videoId]="v.videoId" [provider]="provider()" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button></div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
[queryParams]="watchQueryParams(b.provider, v)"
|
||||
[state]="{ video: v }"
|
||||
class="block">
|
||||
<div class="relative">
|
||||
<div class="relative rounded-lg overflow-hidden" [attr.data-video-provider]="b.provider">
|
||||
<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>
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 pb-1">
|
||||
<h4 class="font-semibold line-clamp-2">{{ v.title }}</h4>
|
||||
<h4 class="text-sm font-semibold leading-snug line-clamp-2">{{ v.title }}</h4>
|
||||
</div>
|
||||
</a>
|
||||
<div class="px-3 pb-3">
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<span class="pm-item-label">Ouvrir chez le fournisseur</span>
|
||||
</a>
|
||||
</app-player-menu>
|
||||
<div class="aspect-video overflow-hidden rounded-2xl">
|
||||
<div class="aspect-video overflow-hidden rounded-2xl" [attr.data-video-provider]="provider()">
|
||||
@if (embedUrl(); as src) {
|
||||
<iframe #embedFrame class="h-full w-full" [src]="src" frameborder="0" playsinline (load)="onEmbedFrameLoad()"
|
||||
referrerpolicy="origin"
|
||||
@@ -507,7 +507,7 @@
|
||||
@for (related of v.relatedStreams; track related.videoId) {
|
||||
<div class="group rounded-xl p-2 transition hover:bg-slate-800/60 focus-within:bg-slate-800/60">
|
||||
<a [routerLink]="['/watch', related.videoId]" [queryParams]="watchQueryParams(related)" [state]="{ video: related }" class="flex items-start gap-3 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400">
|
||||
<div class="relative w-32 shrink-0 sm:w-40">
|
||||
<div class="relative w-32 shrink-0 sm:w-40 rounded-xl" [attr.data-video-provider]="related.provider">
|
||||
<img [src]="related.thumbnail" [alt]="related.title" class="h-20 w-full rounded-xl object-cover sm:h-24" loading="lazy">
|
||||
<div class="absolute right-1.5 top-1.5">
|
||||
<app-provider-badge [provider]="related.provider"></app-provider-badge>
|
||||
|
||||
Reference in New Issue
Block a user