feat(shorts): le panneau ⓘ demande la description et les vues à la source
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
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.
This commit is contained in:
@@ -312,7 +312,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-1 text-[12px] text-white/60">{{ providerLabel() }}<span> • </span>{{ currentMeta() }}</div>
|
||||
<div class="mt-1 text-[12px] text-white/60">{{ providerLabel() }}<span> • </span>{{ panelMeta() }}</div>
|
||||
|
||||
<!-- Barre d'engagement -->
|
||||
<div class="mt-3 flex items-center gap-2 border-y border-white/10 py-2">
|
||||
@@ -351,8 +351,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Détails et description -->
|
||||
@if (currentDescription()) {
|
||||
<p class="mt-3 whitespace-pre-line break-words rounded-xl bg-white/5 p-3 text-[12px] leading-relaxed text-white/75">{{ currentDescription() }}</p>
|
||||
@if (panelDescription()) {
|
||||
<p class="mt-3 whitespace-pre-line break-words rounded-xl bg-white/5 p-3 text-[12px] leading-relaxed text-white/75">{{ panelDescription() }}</p>
|
||||
} @else if (detailsLoading()) {
|
||||
<div class="mt-3 space-y-1.5 rounded-xl bg-white/5 p-3" role="status" aria-label="Chargement des informations">
|
||||
<div class="h-2.5 w-4/5 animate-pulse rounded bg-white/15"></div>
|
||||
<div class="h-2.5 w-3/5 animate-pulse rounded bg-white/15"></div>
|
||||
</div>
|
||||
} @else {
|
||||
<p class="mt-3 text-[12px] leading-relaxed text-white/40">Le fournisseur ne transmet pas de description pour ce short — « Ouvrir chez le fournisseur » ci-dessous donne la version complète.</p>
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {};
|
||||
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<any>(`/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é). */
|
||||
|
||||
Reference in New Issue
Block a user