Feat : Preview on streamers
All checks were successful
release / build (push) Successful in 27s
release / verify-windows (push) Successful in 1m27s

This commit is contained in:
jeanotx32
2026-08-12 16:23:43 +02:00
parent eae694d49a
commit 58aadee97d
7 changed files with 159 additions and 4 deletions

View File

@@ -3,7 +3,13 @@ import type { AgentView, PreemptionVerdict, WatchTarget } from '@stream-control/
import { DEFAULT_PRIORITY, priorityLabel } from '@stream-control/shared';
import { api } from '../api';
import { formatDateTime, formatDuration, formatRelative } from '../format';
import { preemption, recordingContext, stateLabel, type RecordingContext } from '../streamers';
import {
preemption,
previewSrc,
recordingContext,
stateLabel,
type RecordingContext,
} from '../streamers';
import { StreamerDetail } from './StreamerDetail';
interface Props {
@@ -172,12 +178,18 @@ function StreamerCard({
}) {
const [busy, setBusy] = useState(false);
const [avatarBroken, setAvatarBroken] = useState(false);
// La valeur en échec, pas un simple booléen : `preview` change de contenu à
// chaque sonde (nouveau `snapshotAt`) sans changer d'identité de composant,
// un booléen figé resterait donc bloqué après le premier accroc réseau.
const [brokenPreview, setBrokenPreview] = useState<string | null>(null);
const state = stateLabel(target);
const agent = agents.find((candidate) => candidate.id === target.agentId) ?? null;
const context = recordingContext(target, agent, targets);
const verdict = preemption(target, context);
const isLive = target.state === 'public';
const preview = isLive ? previewSrc(target) : null;
const showPreview = preview !== null && preview !== brokenPreview;
const liveFor = isLive && target.statusChangedAt ? Date.now() - target.statusChangedAt : null;
const lastDuration =
@@ -218,6 +230,22 @@ function StreamerCard({
tabIndex={0}
title="Ouvrir la fiche"
>
{showPreview && (
// Un `<img>`, pas un flux : le serveur n'a que l'API REST de la
// plateforme, ce n'est pas l'agent qui pilote un navigateur ici. Le
// clic n'est pas intercepté : même agrandie, l'image reste celle de
// cette fiche, et l'ouvrir en cliquant dessus reste cohérent.
<div className="streamer-preview">
<img
src={preview}
alt=""
loading="lazy"
referrerPolicy="no-referrer"
onError={() => setBrokenPreview(preview)}
/>
</div>
)}
<header className="streamer-head">
{target.avatarUrl && !avatarBroken ? (
<img