From 4d1d44f4ee5966f6a31eec52bd11c47acd52071a Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Thu, 1 Oct 2026 23:37:10 -0400 Subject: [PATCH] =?UTF-8?q?feat(shorts):=20fl=C3=A8ches=20Pr=C3=A9c./Suiva?= =?UTF-8?q?nt=20SUR=20la=20vid=C3=A9o=20+=20embed=20Rumble=20en=2016:9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Flèches : elles vivaient EN DEHORS du cadre (et se fadaient avec le chrome auto-hide). Elles sont désormais posées dans le cadre, à mi-hauteur sur les bords, toujours visibles (classe `shorts-chrome` retirée), sur mobile comme sur bureau, et le stopPropagation protège le geste tactile. - Rumble : la plateforme est16:9 mais n'expose ni largeur ni hauteur → l'embed tombait dans un cadre 9:16 (étiré / réduit). `isWideEmbed()` (pur + testé) décide de la boîte : dimensions connues => orientation réelle, sinon repli « rumble = paysage » ; la CSS `.embed-wide` rend une iframe 16:9 centrée (434x244 dans un cadre 434x771, mesuré). Les vidéos verticales (dimensions connues) gardent le cadre pleine hauteur. - État vide : un fournisseur sans résultat laissait un cadre NOIR muet (ni squelette, ni message) — il affiche désormais « Aucun short à afficher pour ce fournisseur » + Réessayer. Tests : test:shorts-catalog (3 cas isWideEmbed), test:kind, test:section, test:search verts ; build ng OK ; sondage Playwright local : flèches dans le cadre + navigation suivant/précédent réelle, boîte embed-wide 16:9 centrée, short vertical inchangé (iframe pleine hauteur), état vide affiché. --- index.css | 10 ++++++ src/app/search/shorts-catalog.spec.ts | 8 +++++ src/app/search/shorts-catalog.ts | 18 ++++++++++ .../shorts/watch-short.component.html | 35 +++++++++++-------- .../shorts/watch-short.component.ts | 7 ++++ 5 files changed, 64 insertions(+), 14 deletions(-) diff --git a/index.css b/index.css index c71d584..3cb751b 100644 --- a/index.css +++ b/index.css @@ -517,6 +517,16 @@ body { } } +/* Embed PAYSAGE dans le cadre vertical : la boîte de l'iframe suit le16:9 + (sinon le lecteur étire ou réduit la vidéo dans un cadre 9:16). */ +.shorts-frame iframe.embed-wide { + top: 50%; + bottom: auto; + height: auto; + aspect-ratio: 16 / 9; + transform: translateY(-50%); +} + /* --- Shorts immersive stage (façon YouTube) -------------------------------- */ /* Plein écran vertical : le cadre 9:16 flotte sur un fond d'ambiance tiré de la vignette courante. `100dvh` (barres navigateur mobiles) avec repli. */ diff --git a/src/app/search/shorts-catalog.spec.ts b/src/app/search/shorts-catalog.spec.ts index c7752d0..cc0db62 100644 --- a/src/app/search/shorts-catalog.spec.ts +++ b/src/app/search/shorts-catalog.spec.ts @@ -1,5 +1,6 @@ import { catalogShortToVideo, + isWideEmbed, formatShortMeta, formatCapturedAgo, formatCompactFr, @@ -32,6 +33,13 @@ function assertEqual(actual: unknown, expected: unknown, message: string): void const NOW = 1700000000000; +console.log('--- isWideEmbed ---'); +assertEqual(isWideEmbed({ width: 1920, height: 1080 }, 'rumble'), true, '16:9 connu → paysage'); +assertEqual(isWideEmbed({ width: 720, height: 1280 }, 'youtube'), false, 'vertical connu → pas paysage'); +assertEqual(isWideEmbed({}, 'rumble'), true, 'Rumble sans dimensions → paysage (repli plateforme)'); +assertEqual(isWideEmbed({}, 'youtube'), false, 'dimensions inconnues hors Rumble → on ne tranche pas'); +assertEqual(isWideEmbed(null, 'rumble'), true, 'item nul → repli plateforme'); + console.log('--- catalogShortToVideo ---'); assertEqual(catalogShortToVideo(null), null, 'null → null'); assertEqual(catalogShortToVideo({} as any), null, 'sans id → null (pas de fantôme)'); diff --git a/src/app/search/shorts-catalog.ts b/src/app/search/shorts-catalog.ts index 7e979a7..1c590a7 100644 --- a/src/app/search/shorts-catalog.ts +++ b/src/app/search/shorts-catalog.ts @@ -91,6 +91,24 @@ export function catalogShortToVideo(item: VideoItem | null | undefined): Video | }; } +/** + * La vidéo est-elle PAYSAGE dans le cadre vertical (9:16) de /shorts ? + * + * Rumble n'expose ni largeur ni hauteur et sa plateforme est16:9 : sans cela + * son embed est écrasé dans un cadre portrait (étiré ou réduit à un tiers de + * l'écran). Dimensions connues => orientation réelle ; inconnues => repli sur + * la nature de la plateforme (rumble = paysage, les autres = on ne tranche pas). + */ +export function isWideEmbed( + v: { width?: number | null; height?: number | null } | null | undefined, + provider: string, +): boolean { + const w = Number(v?.width) || 0; + const h = Number(v?.height) || 0; + if (w > 0 && h > 0) return w > h; + return provider === 'rumble'; +} + /** Nombre compact fr (« 12 k », « 3,4 M ») — `Intl` plutôt qu'un format maison. */ export function formatCompactFr(n: number): string { if (!Number.isFinite(n) || n < 0) return ''; diff --git a/src/components/shorts/watch-short.component.html b/src/components/shorts/watch-short.component.html index 5d1c163..2c6bc5a 100644 --- a/src/components/shorts/watch-short.component.html +++ b/src/components/shorts/watch-short.component.html @@ -18,19 +18,12 @@
- - -
@if (embedUrl(); as url) { - } @else if (embedWaiting()) { @@ -52,6 +45,13 @@ {{ error() }}
+ } @else { + +
+ Aucun short à afficher pour ce fournisseur. + +
} @if (current(); as v) { @@ -204,6 +204,19 @@
}
+ + + + } @if (shareCopied()) { @@ -212,12 +225,6 @@ - - @if (busyMore()) { diff --git a/src/components/shorts/watch-short.component.ts b/src/components/shorts/watch-short.component.ts index 6cc0c75..a885d1d 100644 --- a/src/components/shorts/watch-short.component.ts +++ b/src/components/shorts/watch-short.component.ts @@ -21,6 +21,7 @@ import { SearchService } from '../../app/search/search.service'; import { ChannelProviderFactory } from '../../app/core/providers/channel/channel-provider.factory'; import { catalogShortToVideo, + isWideEmbed, formatShortMeta, formatCapturedAgo, normalizeShortsLangMode, @@ -505,6 +506,12 @@ export class WatchShortComponent implements OnDestroy { return null; }); + /** Encadré 16:9 de l'embed (voir `isWideEmbed`). */ + readonly wideEmbed = computed(() => { + const v = this.current(); + return v ? isWideEmbed(v, this.providerOf(v)) : false; + }); + /** Host PeerTube déduit de l'URL vidéo (fédération), sinon null. */ private peerTubeHostOf(v: Video): string | null { try {