From 474de0570c712180cd13b4982bcb6ebc8533e233 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Thu, 1 Oct 2026 23:09:28 -0400 Subject: [PATCH] fix(shorts): embed PeerTube sur l'instance d'origine + FAB glissable + layout bureau MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PeerTube : l'adaptateur pt.ts reconstruisait l'item SANS `url` et le pont catalogue ne conservait l'URL que pour Odysee → /shorts reconstruisait l'embed sur l'instance ACTIVE → « Sorry this video does not exist » sur toute vidéo fédérée. `url` est désormais miroitée dans VideoItem (contrat v2), propagée par PtAdapter et gardée par catalogShortToVideo : peerTubeHostOf() dérive enfin le host d'origine. Vérifié de bout en bout : embed tube.blueben.net → 200 + vrai titre, cas de test dédié dans shorts-catalog.spec. - FAB du menu : déplaçable au pointeur (seuil 6 px, position en % du cadre, persistée en localStorage). Le clic qui suit un glissement n'ouvre pas le menu, et le panneau s'ouvre TOUJOURS vers l'intérieur du cadre (classes pm-opens-left / pm-opens-right / pm-opens-up posées par applyAnchor) : sinon overflow:hidden le rognait dès qu'on déplaçait le bouton. - Bureau : --shorts-chrome 9rem → 4rem (la vidéo passe de 691 à 771 px de haut en 1280x900, 389 → 434 de large) et menu compact au-delà de 768 px (FAB 36 → 30 px, panneau 233 → 202 px). Mobile inchangé (36 / 636 px). Tests : test:shorts-catalog (nouveau cas PeerTube), test:kind, test:section, test:search verts ; build ng OK ; sondage Playwright 19/19 sur l'instance locale (layout, glissement + restauration, embed PeerTube 200 sans erreur). --- index.css | 28 +++- src/app/search/adapters/pt.ts | 4 + src/app/search/shorts-catalog.spec.ts | 9 ++ src/app/search/shorts-catalog.ts | 4 +- .../player-menu/player-menu.component.ts | 125 +++++++++++++++++- src/app/shared/models/video-item.model.ts | 3 + 6 files changed, 167 insertions(+), 6 deletions(-) diff --git a/index.css b/index.css index b208ecb..c71d584 100644 --- a/index.css +++ b/index.css @@ -502,8 +502,11 @@ body { min-height: 0; } @media (min-width: 768px) { + /* Bureau : pas de barre du bas, les flèches sont sur les côtés — on ne + réserve que la scène (py-3) + la charge « Chargement… ». La vidéo gagne + ~80 px de hauteur (et autant de largeur, 9:16 oblige). */ .shorts-frame { - --shorts-chrome: 9rem; + --shorts-chrome: 4rem; } } @media (max-width: 767px) { @@ -581,6 +584,8 @@ body { relief (ombre + highlight interne) et agrandissement au survol. */ .pm-fab { display: inline-flex; + touch-action: none; /* glisser le FAB ne défile pas la page */ + cursor: grab; align-items: center; justify-content: center; width: 2.25rem; /* 36px */ @@ -601,7 +606,7 @@ body { border-color: rgba(255, 255, 255, .32); transform: scale(1.06); } -.pm-fab:active { transform: scale(.94); } +.pm-fab:active { transform: scale(.94); cursor: grabbing; } .pm-fab-open { background: linear-gradient(180deg, rgba(51, 65, 85, .95), rgba(2, 6, 23, .95)); border-color: rgba(255, 255, 255, .32); @@ -730,6 +735,25 @@ body { .shorts-frame app-player-menu { left: auto; right: 0.5rem; } .shorts-frame app-player-menu .pm-panel { left: auto; right: 0; transform-origin: top right; } +/* FAB glissé ailleurs que son coin d'origine : le panneau s'ouvre TOUJOURS + vers l'intérieur du conteneur (sinon `overflow: hidden` le rogne) et vers + le haut quand le FAB est en bas. Les classes sont posées par + PlayerMenuComponent.applyAnchor(). */ +app-player-menu.pm-opens-left .pm-panel { left: auto; right: 0; transform-origin: top right; } +app-player-menu.pm-opens-right .pm-panel { left: 0; right: auto; transform-origin: top left; } +app-player-menu.pm-opens-up .pm-panel { top: auto; bottom: calc(100% + 0.5rem); transform-origin: bottom left; } +app-player-menu.pm-opens-left.pm-opens-up .pm-panel { transform-origin: bottom right; } + +/* Bureau : menu plus compact pour laisser la place à la vidéo (shorts). */ +@media (min-width: 768px) { + .shorts-frame app-player-menu .pm-fab { width: 1.875rem; height: 1.875rem; } + .shorts-frame app-player-menu .pm-fab svg { width: 0.875rem; height: 0.875rem; } + .shorts-frame app-player-menu .pm-panel { width: 13rem; max-height: min(60vh, 380px); } + .shorts-frame app-player-menu .pm-item { padding: 0.375rem 0.5rem; gap: 0.5rem; font-size: 0.75rem; } + .shorts-frame app-player-menu .pm-section-label { padding: 0.375rem 0.5rem 0.125rem; font-size: 0.625rem; } + .shorts-frame app-player-menu .pm-panel-header { padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem; } +} + /* --- Drawer / overlay plumbing -------------------------------------------- */ /* Applied to while a full-screen drawer or modal owns the viewport. Note: deliberately NOT `touch-action: none` — touch-action is intersected up diff --git a/src/app/search/adapters/pt.ts b/src/app/search/adapters/pt.ts index cfc083b..3ec61cd 100644 --- a/src/app/search/adapters/pt.ts +++ b/src/app/search/adapters/pt.ts @@ -45,6 +45,10 @@ export class PtAdapter implements ProviderAdapter { // Le serveur envoie `publishedAt` (ISO) quand l'instance l'expose. publishedAt: typeof it.publishedAt === 'string' ? it.publishedAt : undefined, channelExternalId, + // URL canonnique : instance d'origine (vidéos fédérées). Sans elle, + // /shorts reconstruit l'embed sur l'instance ACTIVE → « Sorry this + // video does not exist ». + url: typeof it.url === 'string' ? it.url : undefined, // Signaux de type pour le raffinement client des filtres. type: typeof it.type === 'string' ? it.type : undefined, isShort: it.isShort === true ? true : undefined, diff --git a/src/app/search/shorts-catalog.spec.ts b/src/app/search/shorts-catalog.spec.ts index bbf99b9..c7752d0 100644 --- a/src/app/search/shorts-catalog.spec.ts +++ b/src/app/search/shorts-catalog.spec.ts @@ -50,6 +50,15 @@ assertEqual( assertEqual(v?.views, 0, 'vues inconnues → 0 (pas de devinette)'); assertEqual(v?.duration, 0, 'durée inconnue → 0'); } +{ + // PeerTube fédéré : l'URL porte l'instance d'origine. Sans elle, /shorts + // reconstruit l'embed sur l'instance ACTIVE → « Sorry this video does not exist ». + const v: any = catalogShortToVideo({ + id: 'p1', provider: 'peertube', title: 'P', thumbnailUrl: '', channelName: 'C', + url: 'https://makertube.net/videos/watch/p1', + } as any); + assertEqual(v?.url, 'https://makertube.net/videos/watch/p1', 'url fournisseur conservée (host PeerTube)'); +} { const v: any = catalogShortToVideo({ id: 'y1', provider: 'youtube', title: 'Y', thumbnailUrl: '', channelName: 'C', diff --git a/src/app/search/shorts-catalog.ts b/src/app/search/shorts-catalog.ts index 215dbbb..7e979a7 100644 --- a/src/app/search/shorts-catalog.ts +++ b/src/app/search/shorts-catalog.ts @@ -56,7 +56,9 @@ export function catalogShortToVideo(item: VideoItem | null | undefined): Video | const embeddableRaw = (item as { embeddable?: unknown }).embeddable; const allowEmbed = typeof embeddableRaw === 'boolean' ? embeddableRaw : undefined; return { - url: provider === 'odysee' && slug ? `https://odysee.com/${slug}` : '', + // Odysee : URL normalisée depuis son slug ; ailleurs on conserve l'URL + // du fournisseur (PeerTube fédéré en a besoin pour choisir le bon host). + url: provider === 'odysee' && slug ? `https://odysee.com/${slug}` : strOrEmpty(item.url), type: item.isLive === true ? 'live' : (item.isShort === true ? 'short' : 'video'), title: strOrEmpty(item.title), thumbnail: strOrEmpty(item.thumbnailUrl), diff --git a/src/app/shared/components/player-menu/player-menu.component.ts b/src/app/shared/components/player-menu/player-menu.component.ts index 10ef1f1..1560fa1 100644 --- a/src/app/shared/components/player-menu/player-menu.component.ts +++ b/src/app/shared/components/player-menu/player-menu.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core'; import { CommonModule } from '@angular/common'; /** @@ -30,10 +30,14 @@ import { CommonModule } from '@angular/common'; template: `