Feat : pause now showing in another color
This commit is contained in:
@@ -22,7 +22,7 @@ interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const EMPTY: TimelineData = { sessions: [], spans: [] };
|
||||
const EMPTY: TimelineData = { sessions: [], spans: [], pauses: [] };
|
||||
|
||||
/**
|
||||
* Fiche détaillée d'un profil : identité, réglages, et son historique de
|
||||
@@ -343,6 +343,7 @@ export function StreamerDetail({ target, targets, agents, notify, onClose }: Pro
|
||||
<p className="muted small timeline-legend">
|
||||
<span className="legend-swatch live" /> diffusion publique
|
||||
<span className="legend-swatch rec" /> enregistré
|
||||
<span className="legend-swatch paused" /> en pause
|
||||
</p>
|
||||
|
||||
{loading ? (
|
||||
@@ -358,6 +359,7 @@ export function StreamerDetail({ target, targets, agents, notify, onClose }: Pro
|
||||
label: target.label ?? target.username,
|
||||
sessions: timeline.sessions,
|
||||
spans: timeline.spans,
|
||||
pauses: timeline.pauses,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -367,7 +369,12 @@ export function StreamerDetail({ target, targets, agents, notify, onClose }: Pro
|
||||
<ul className="session-list">
|
||||
{sessions.map((session) => {
|
||||
const end = session.endedAt ?? now;
|
||||
const recorded = overlapMs(timeline.spans, session.startedAt, end);
|
||||
// Pauses déduites : elles n'ont rien écrit dans le fichier.
|
||||
const recorded = Math.max(
|
||||
0,
|
||||
overlapMs(timeline.spans, session.startedAt, end) -
|
||||
overlapMs(timeline.pauses, session.startedAt, end),
|
||||
);
|
||||
return (
|
||||
<li key={session.id}>
|
||||
<span className="session-when">{formatDateTime(session.startedAt)}</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { RecordingSpan, StreamSession } from '@stream-control/shared';
|
||||
import type { RecordingPause, RecordingSpan, StreamSession } from '@stream-control/shared';
|
||||
import { sessionPreviewSrc } from '../api';
|
||||
import { formatDuration } from '../format';
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface TimelineLane {
|
||||
avatarUrl?: string | null;
|
||||
sessions: StreamSession[];
|
||||
spans: RecordingSpan[];
|
||||
pauses: RecordingPause[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -81,8 +82,11 @@ export function Timeline({ from, to, lanes, hideLabels, onSelectLane }: Props) {
|
||||
{lanes.map((lane) => {
|
||||
const sessions = toBars(lane.sessions, from, to, now, 'Diffusion');
|
||||
const spans = toBars(lane.spans, from, to, now, 'Enregistrement');
|
||||
const pauses = toBars(lane.pauses, from, to, now, 'En pause');
|
||||
const liveMs = totalMs(sessions);
|
||||
const recordedMs = totalMs(spans);
|
||||
// Le temps en pause est retranché : la capture tournait, mais n'écrivait
|
||||
// rien. L'inclure gonflerait le total d'un temps absent du fichier.
|
||||
const recordedMs = Math.max(0, totalMs(spans) - totalMs(pauses));
|
||||
|
||||
return (
|
||||
<div key={lane.id} className="timeline-lane">
|
||||
@@ -111,7 +115,7 @@ export function Timeline({ from, to, lanes, hideLabels, onSelectLane }: Props) {
|
||||
</button>
|
||||
<span
|
||||
className="muted small timeline-totals"
|
||||
title={`${formatDuration(liveMs)} en direct, dont ${formatDuration(recordedMs)} enregistré`}
|
||||
title={`${formatDuration(liveMs)} en direct, dont ${formatDuration(recordedMs)} réellement enregistré (pauses déduites)`}
|
||||
>
|
||||
{formatDuration(liveMs)}
|
||||
{recordedMs > 0 ? <span className="timeline-rec-total"> · {formatDuration(recordedMs)}</span> : null}
|
||||
@@ -164,6 +168,18 @@ export function Timeline({ from, to, lanes, hideLabels, onSelectLane }: Props) {
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Après les intervalles, donc par-dessus : une pause se lit comme
|
||||
une portion de la capture qui l'entoure, pas comme une barre
|
||||
concurrente. */}
|
||||
{pauses.map((bar) => (
|
||||
<span
|
||||
key={bar.key}
|
||||
className={bar.open ? 'timeline-pause live' : 'timeline-pause'}
|
||||
style={{ left: `${bar.left}%`, width: `${bar.width}%` }}
|
||||
title={bar.title}
|
||||
/>
|
||||
))}
|
||||
|
||||
{nowLeft !== null && (
|
||||
<span className="timeline-now" style={{ left: `${nowLeft}%` }} title="maintenant" />
|
||||
)}
|
||||
@@ -224,7 +240,7 @@ const percent = (at: number, from: number, to: number) => ((at - from) / (to - f
|
||||
* remplissant tout l'écran.
|
||||
*/
|
||||
function toBars(
|
||||
intervals: Array<StreamSession | RecordingSpan>,
|
||||
intervals: Array<StreamSession | RecordingSpan | RecordingPause>,
|
||||
from: number,
|
||||
to: number,
|
||||
now: number,
|
||||
|
||||
@@ -12,7 +12,7 @@ interface Props {
|
||||
notify: (message: string, tone?: 'info' | 'error') => void;
|
||||
}
|
||||
|
||||
const EMPTY: TimelineData = { sessions: [], spans: [] };
|
||||
const EMPTY: TimelineData = { sessions: [], spans: [], pauses: [] };
|
||||
|
||||
/**
|
||||
* Frise de toutes les diffusions suivies, une voie par profil.
|
||||
@@ -60,12 +60,16 @@ export function TimelinePage({ targets, agents, notify }: Props) {
|
||||
avatarUrl: target.avatarUrl,
|
||||
sessions: [],
|
||||
spans: [],
|
||||
pauses: [],
|
||||
});
|
||||
}
|
||||
for (const session of data.sessions) byTarget.get(session.targetId)?.sessions.push(session);
|
||||
for (const span of data.spans) {
|
||||
if (span.targetId) byTarget.get(span.targetId)?.spans.push(span);
|
||||
}
|
||||
for (const pause of data.pauses) {
|
||||
if (pause.targetId) byTarget.get(pause.targetId)?.pauses.push(pause);
|
||||
}
|
||||
|
||||
// Un profil sans la moindre diffusion sur la période n'apporte qu'une ligne
|
||||
// vide : la frise sert à comparer des activités, pas à recenser les suivis.
|
||||
@@ -75,9 +79,14 @@ export function TimelinePage({ targets, agents, notify }: Props) {
|
||||
.sort((a, b) => lastActivity(b) - lastActivity(a));
|
||||
}, [data, targets, onlyRecorded]);
|
||||
|
||||
const totalRecorded = data.spans.reduce(
|
||||
(sum, span) => sum + ((span.endedAt ?? now) - span.startedAt),
|
||||
// Pauses déduites : la capture tournait, mais n'écrivait rien pendant ce
|
||||
// temps-là. L'annoncer comme enregistré surestimerait ce qu'on a réellement.
|
||||
const elapsed = (interval: { startedAt: number; endedAt: number | null }) =>
|
||||
(interval.endedAt ?? now) - interval.startedAt;
|
||||
const totalRecorded = Math.max(
|
||||
0,
|
||||
data.spans.reduce((sum, span) => sum + elapsed(span), 0) -
|
||||
data.pauses.reduce((sum, pause) => sum + elapsed(pause), 0),
|
||||
);
|
||||
const openTarget = openId ? (targets.find((target) => target.id === openId) ?? null) : null;
|
||||
|
||||
@@ -107,6 +116,7 @@ export function TimelinePage({ targets, agents, notify }: Props) {
|
||||
<p className="muted small timeline-legend">
|
||||
<span className="legend-swatch live" /> diffusion publique
|
||||
<span className="legend-swatch rec" /> enregistré
|
||||
<span className="legend-swatch paused" /> en pause
|
||||
<span className="spacer" />
|
||||
{data.sessions.length} diffusion(s) · {formatDuration(totalRecorded)} enregistré au total
|
||||
</p>
|
||||
|
||||
@@ -1021,6 +1021,24 @@ fieldset.group > legend {
|
||||
.timeline-rec.live {
|
||||
background: repeating-linear-gradient(90deg, var(--rec) 0 6px, #b91c1c 6px 12px);
|
||||
}
|
||||
|
||||
/*
|
||||
* Portion en pause, posée par-dessus la capture qui la contient et calée sur sa
|
||||
* géométrie : elle se lit comme une part de l'enregistrement, pas comme une
|
||||
* barre concurrente. Jaune plutôt que rouge — la capture tournait, mais
|
||||
* n'écrivait rien.
|
||||
*/
|
||||
.timeline-pause {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
bottom: 7px;
|
||||
min-width: 2px;
|
||||
border-radius: 2px;
|
||||
background: var(--warn);
|
||||
}
|
||||
.timeline-pause.live {
|
||||
background: repeating-linear-gradient(90deg, var(--warn) 0 6px, #b45309 6px 12px);
|
||||
}
|
||||
.timeline-now {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -1056,6 +1074,9 @@ fieldset.group > legend {
|
||||
.legend-swatch.rec {
|
||||
background: var(--rec);
|
||||
}
|
||||
.legend-swatch.paused {
|
||||
background: var(--warn);
|
||||
}
|
||||
|
||||
/*
|
||||
* Vignette conservée d'une diffusion, montrée au survol de sa barre. En `fixed`
|
||||
|
||||
Reference in New Issue
Block a user