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>
|
<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>
|
</button>
|
||||||
</div>
|
</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 -->
|
<!-- Barre d'engagement -->
|
||||||
<div class="mt-3 flex items-center gap-2 border-y border-white/10 py-2">
|
<div class="mt-3 flex items-center gap-2 border-y border-white/10 py-2">
|
||||||
@@ -351,8 +351,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Détails et description -->
|
<!-- Détails et description -->
|
||||||
@if (currentDescription()) {
|
@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">{{ currentDescription() }}</p>
|
<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 {
|
} @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>
|
<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);
|
isFullscreen = signal(false);
|
||||||
/** Panneau « informations » (colonne au bureau, tiroir sur mobile). */
|
/** Panneau « informations » (colonne au bureau, tiroir sur mobile). */
|
||||||
infoOpen = signal(false);
|
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
|
* 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
|
* 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;
|
const v = this.current() as (Video & { shortDescription?: string | null }) | null;
|
||||||
try { return String(v?.shortDescription || '').trim(); } catch { return ''; }
|
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). */
|
/** Dernier index vu par l'effet de transition (glissade du cadre). */
|
||||||
private slideFrom = -1;
|
private slideFrom = -1;
|
||||||
/** Fin de l'animation de sortie en cours : l'entrée attend son tour. */
|
/** 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.ytPlaying.set(true);
|
||||||
this.menuOpen.set(false);
|
this.menuOpen.set(false);
|
||||||
if (!v) return;
|
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;
|
const nx = (i >= 0 && i + 1 < list.length) ? list[i + 1] : null;
|
||||||
this.queueRumbleResolves([v, nx]);
|
this.queueRumbleResolves([v, nx]);
|
||||||
// Chaque nouvelle vidéo réveille les contrôles (flèches + menu) pendant
|
// 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). */
|
/** Panneau d'informations (bureau : colonne à droite ; mobile : tiroir). */
|
||||||
toggleInfo(): void {
|
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é). */
|
/** Interrupteur « lecture auto » du rail (persisté). */
|
||||||
|
|||||||
Reference in New Issue
Block a user