From ba7085c3211fd90c03ab25092e1375311552daf5 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 2 Oct 2026 14:55:39 -0400 Subject: [PATCH] =?UTF-8?q?feat(shorts):=20le=20panneau=20=E2=93=98=20dema?= =?UTF-8?q?nde=20la=20description=20et=20les=20vues=20=C3=A0=20la=20source?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le flux Shorts ne porte pas de description (et pas toujours de vues) : le panneau affichait « le fournisseur ne transmet pas de description » alors que `/api/details` la connaît. Il l'interroge maintenant à l'OUVERTURE, une fois par vidéo (chargement squelette, échec silencieux + retentative au clic suivant), et le serveur sert le tout depuis son cache 6 h. - `detailsEnriched()` + `detailsLoading()` : description et vues réelles, `panelDescription()`/`panelMeta()` préfèrent le flux puis complètent. - Paramètres par fournisseur réutilisés (`instance` PeerTube via `peerTubeHostOf`, `slug` Odysee via `odyseeSlugOf`) : mêmes règles que le reste du composant, pas de second jeu de conventions. - Rafraîchi si le panneau reste ouvert pendant un changement de short. --- .../shorts/watch-short.component.html | 11 ++- .../shorts/watch-short.component.ts | 69 ++++++++++++++++++- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/components/shorts/watch-short.component.html b/src/components/shorts/watch-short.component.html index df2b286..475e6bd 100644 --- a/src/components/shorts/watch-short.component.html +++ b/src/components/shorts/watch-short.component.html @@ -312,7 +312,7 @@ -
{{ providerLabel() }} • {{ currentMeta() }}
+
{{ providerLabel() }} • {{ panelMeta() }}
@@ -351,8 +351,13 @@
- @if (currentDescription()) { -

{{ currentDescription() }}

+ @if (panelDescription()) { +

{{ panelDescription() }}

+ } @else if (detailsLoading()) { +
+
+
+
} @else {

Le fournisseur ne transmet pas de description pour ce short — « Ouvrir chez le fournisseur » ci-dessous donne la version complète.

} diff --git a/src/components/shorts/watch-short.component.ts b/src/components/shorts/watch-short.component.ts index 4043f15..425b284 100644 --- a/src/components/shorts/watch-short.component.ts +++ b/src/components/shorts/watch-short.component.ts @@ -174,6 +174,15 @@ export class WatchShortComponent implements OnDestroy { isFullscreen = signal(false); /** Panneau « informations » (colonne au bureau, tiroir sur mobile). */ infoOpen = signal(false); + /** + * Détails enrichis du panneau ⓘ (description + vues) : chargés À L'OUVERTURE + * via `/api/details` (côté serveur : 6 h de cache). Le flux Shorts ne porte + * pas de description chez la plupart des fournisseurs — le panneau la + * demandait donc à la source, mais une fois, à la demande. + */ + readonly detailsEnriched = signal<{ description?: string; views?: number } | null>(null); + detailsLoading = signal(false); + private detailsFetchedFor = ''; /** * Lecture auto (avance en fin de vidéo) : activée par défaut, pilotée par * le bouton du rail et persistée. `scheduleAutoAdvance` ET l'événement de @@ -189,6 +198,22 @@ export class WatchShortComponent implements OnDestroy { const v = this.current() as (Video & { shortDescription?: string | null }) | null; try { return String(v?.shortDescription || '').trim(); } catch { return ''; } }); + /** Description du panneau ⓘ : celle du flux, sinon celle enrichie à la demande. */ + readonly panelDescription = computed(() => { + const fromFeed = this.currentDescription(); + if (fromFeed) return fromFeed; + return String(this.detailsEnriched()?.description || '').trim(); + }); + + /** Méta du panneau ⓘ (vues + date) : complétée par l'enrichissement. */ + readonly panelMeta = computed(() => { + const v = this.current(); + if (!v) return ''; + const feedViews = (typeof v.views === 'number' && v.views > 0) ? v.views : undefined; + const views = feedViews ?? this.detailsEnriched()?.views; + try { return formatShortMeta(views, v.uploadedDate); } catch { return ''; } + }); + /** Dernier index vu par l'effet de transition (glissade du cadre). */ private slideFrom = -1; /** Fin de l'animation de sortie en cours : l'entrée attend son tour. */ @@ -725,6 +750,10 @@ export class WatchShortComponent implements OnDestroy { this.ytPlaying.set(true); this.menuOpen.set(false); if (!v) return; + // Le panneau ⓘ avait les détails de la vidéo précédente : on les jette, + // et on les redemande tout de suite s'il est resté ouvert (bureau). + this.detailsEnriched.set(null); + if (this.infoOpen()) this.loadDetailsOnce(); const nx = (i >= 0 && i + 1 < list.length) ? list[i + 1] : null; this.queueRumbleResolves([v, nx]); // Chaque nouvelle vidéo réveille les contrôles (flèches + menu) pendant @@ -1104,7 +1133,45 @@ export class WatchShortComponent implements OnDestroy { /** Panneau d'informations (bureau : colonne à droite ; mobile : tiroir). */ toggleInfo(): void { - this.infoOpen.update(o => !o); + const open = !this.infoOpen(); + this.infoOpen.set(open); + if (open) this.loadDetailsOnce(); + } + + /** + * Charge (une fois par vidéo) la description et les vues via `/api/details`. + * Échec silencieux : le panneau reste utilisable avec les données du flux, + * et le prochain clic sur ⓘ retente (clé non marquée comme chargée). + */ + private loadDetailsOnce(): void { + try { + const v = this.current(); + if (!v?.videoId) return; + const p = this.providerOf(v); + const key = `${p}:${v.videoId}`; + if (key === this.detailsFetchedFor) return; + this.detailsFetchedFor = key; + this.detailsLoading.set(true); + const params: Record = {}; + if (p === 'peertube') { + const inst = this.peerTubeHostOf(v) || this.instances.activePeerTubeInstance(); + if (inst) params['instance'] = inst; + } + if (p === 'odysee') { + const slug = this.odyseeSlugOf(v); + if (slug) params['slug'] = slug; + } + this.http.get(`/api/details/${encodeURIComponent(p)}/${encodeURIComponent(v.videoId)}`, { params }).subscribe({ + next: (d) => { + this.detailsLoading.set(false); + this.detailsEnriched.set({ + description: String(d?.description || '').trim(), + views: typeof d?.views === 'number' && d.views > 0 ? d.views : undefined, + }); + }, + error: () => { this.detailsLoading.set(false); this.detailsFetchedFor = ''; }, + }); + } catch { this.detailsLoading.set(false); } } /** Interrupteur « lecture auto » du rail (persisté). */