Feat : Interface scalable size
This commit is contained in:
12
README.md
12
README.md
@@ -708,6 +708,18 @@ inutile de forcer l'invalidation du cache navigateur en plus.
|
|||||||
Se rafraîchit donc au rythme de la veille (`WATCHLIST_INTERVAL_MS`, 30 s par défaut) — un
|
Se rafraîchit donc au rythme de la veille (`WATCHLIST_INTERVAL_MS`, 30 s par défaut) — un
|
||||||
aperçu, pas un flux temps réel.
|
aperçu, pas un flux temps réel.
|
||||||
|
|
||||||
|
**Sa taille se règle** depuis la barre de filtres de l'onglet Streamers : *Aucun*, *Petit*,
|
||||||
|
*Moyen* (par défaut) ou *Grand*. Selon la largeur de l'écran et le nombre de profils suivis,
|
||||||
|
un aperçu pleine largeur rend la liste agréable ou la rend illisible à force de faire défiler
|
||||||
|
— d'où un réglage plutôt qu'un choix imposé. C'est la **largeur** qui est bridée, pas la
|
||||||
|
hauteur : la boîte 16/9 entière rétrécit, là où une hauteur maximale l'aurait recadrée en
|
||||||
|
bandeau et rogné l'image.
|
||||||
|
|
||||||
|
*Aucun* ne se contente pas de masquer : la vignette n'est alors pas rendue du tout, donc pas
|
||||||
|
téléchargée — ce sont des images distantes, autant ne pas les demander quand personne ne les
|
||||||
|
regarde. Le choix est mémorisé dans le navigateur : c'est un confort propre à l'écran qu'on a
|
||||||
|
sous les yeux, pas un réglage à imposer aux autres postes.
|
||||||
|
|
||||||
## Frise des diffusions
|
## Frise des diffusions
|
||||||
|
|
||||||
Deux vues sur le même historique : la fiche d'un profil (clic sur sa vignette, onglet
|
Deux vues sur le même historique : la fiche d'un profil (clic sur sa vignette, onglet
|
||||||
|
|||||||
@@ -63,8 +63,11 @@ export function AgentCard({
|
|||||||
</label>
|
</label>
|
||||||
<div className="identity">
|
<div className="identity">
|
||||||
<h2>{agent.name}</h2>
|
<h2>{agent.name}</h2>
|
||||||
|
{/* Le nom de machine ne figure plus ici : il redoublait le nom affiché
|
||||||
|
de la VM sans rien en dire de plus. Il reste porté par l'agent, et
|
||||||
|
donc consultable via l'API. */}
|
||||||
<span className="muted small">
|
<span className="muted small">
|
||||||
{agent.hostname ?? '—'} · {agent.platform}
|
{agent.platform}
|
||||||
{agent.agentVersion ? ` · v${agent.agentVersion}` : ''}
|
{agent.agentVersion ? ` · v${agent.agentVersion}` : ''}
|
||||||
{status.buildId ? ` · build ${status.buildId}` : ''}
|
{status.buildId ? ` · build ${status.buildId}` : ''}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ import { DEFAULT_PRIORITY, priorityLabel } from '@stream-control/shared';
|
|||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import { formatDateTime, formatDuration, formatRelative } from '../format';
|
import { formatDateTime, formatDuration, formatRelative } from '../format';
|
||||||
import {
|
import {
|
||||||
|
PREVIEW_SIZES,
|
||||||
|
loadPreviewSize,
|
||||||
preemption,
|
preemption,
|
||||||
previewSrc,
|
previewSrc,
|
||||||
recordingContext,
|
recordingContext,
|
||||||
|
savePreviewSize,
|
||||||
stateLabel,
|
stateLabel,
|
||||||
tagStyle,
|
tagStyle,
|
||||||
usedTags,
|
usedTags,
|
||||||
|
type PreviewSize,
|
||||||
type RecordingContext,
|
type RecordingContext,
|
||||||
} from '../streamers';
|
} from '../streamers';
|
||||||
import { StreamerDetail } from './StreamerDetail';
|
import { StreamerDetail } from './StreamerDetail';
|
||||||
@@ -44,6 +48,14 @@ export function StreamersPage({
|
|||||||
const [openId, setOpenId] = useState<string | null>(null);
|
const [openId, setOpenId] = useState<string | null>(null);
|
||||||
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
|
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
|
||||||
const [managingTags, setManagingTags] = useState(false);
|
const [managingTags, setManagingTags] = useState(false);
|
||||||
|
const [previewSize, setPreviewSize] = useState<PreviewSize>(loadPreviewSize);
|
||||||
|
|
||||||
|
function changePreviewSize(size: PreviewSize) {
|
||||||
|
setPreviewSize(size);
|
||||||
|
// Mémorisé côté navigateur : c'est un confort d'affichage propre à l'écran
|
||||||
|
// qu'on a sous les yeux, pas un réglage à imposer aux autres postes.
|
||||||
|
savePreviewSize(size);
|
||||||
|
}
|
||||||
|
|
||||||
// Tirées des profils plutôt que du serveur : la liste arrive déjà entière par
|
// Tirées des profils plutôt que du serveur : la liste arrive déjà entière par
|
||||||
// le flux temps réel, donc toujours à jour, et une étiquette que personne ne
|
// le flux temps réel, donc toujours à jour, et une étiquette que personne ne
|
||||||
@@ -142,6 +154,21 @@ export function StreamersPage({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<div className="spacer" />
|
<div className="spacer" />
|
||||||
|
<span className="muted small">Aperçus</span>
|
||||||
|
{PREVIEW_SIZES.map((size) => (
|
||||||
|
<button
|
||||||
|
key={size.id}
|
||||||
|
className={previewSize === size.id ? 'chip active' : 'chip'}
|
||||||
|
onClick={() => changePreviewSize(size.id)}
|
||||||
|
title={
|
||||||
|
size.id === 'off'
|
||||||
|
? "N'afficher aucune image du flux — et ne pas la charger"
|
||||||
|
: `Aperçu ${size.label.toLowerCase()}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{size.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
<span className="muted small">{visible.length} profil(s)</span>
|
<span className="muted small">{visible.length} profil(s)</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -188,7 +215,7 @@ export function StreamersPage({
|
|||||||
) : visible.length === 0 ? (
|
) : visible.length === 0 ? (
|
||||||
<p className="muted small">Aucun profil pour ce filtre.</p>
|
<p className="muted small">Aucun profil pour ce filtre.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="streamer-grid">
|
<div className={`streamer-grid preview-${previewSize}`}>
|
||||||
{visible.map((target) => (
|
{visible.map((target) => (
|
||||||
<StreamerCard
|
<StreamerCard
|
||||||
key={target.id}
|
key={target.id}
|
||||||
@@ -196,6 +223,7 @@ export function StreamersPage({
|
|||||||
targets={targets}
|
targets={targets}
|
||||||
agents={agents}
|
agents={agents}
|
||||||
notify={notify}
|
notify={notify}
|
||||||
|
previewSize={previewSize}
|
||||||
onOpen={() => setOpenId(target.id)}
|
onOpen={() => setOpenId(target.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -235,12 +263,14 @@ function StreamerCard({
|
|||||||
targets,
|
targets,
|
||||||
agents,
|
agents,
|
||||||
notify,
|
notify,
|
||||||
|
previewSize,
|
||||||
onOpen,
|
onOpen,
|
||||||
}: {
|
}: {
|
||||||
target: WatchTarget;
|
target: WatchTarget;
|
||||||
targets: WatchTarget[];
|
targets: WatchTarget[];
|
||||||
agents: AgentView[];
|
agents: AgentView[];
|
||||||
notify: (message: string, tone?: 'info' | 'error') => void;
|
notify: (message: string, tone?: 'info' | 'error') => void;
|
||||||
|
previewSize: PreviewSize;
|
||||||
onOpen: () => void;
|
onOpen: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
@@ -255,7 +285,9 @@ function StreamerCard({
|
|||||||
const context = recordingContext(target, agent, targets);
|
const context = recordingContext(target, agent, targets);
|
||||||
const verdict = preemption(target, context);
|
const verdict = preemption(target, context);
|
||||||
const isLive = target.state === 'public';
|
const isLive = target.state === 'public';
|
||||||
const preview = isLive ? previewSrc(target) : null;
|
// Rien n'est calculé — ni chargé — quand les aperçus sont coupés : ce sont des
|
||||||
|
// images distantes, autant ne pas les demander si personne ne les regarde.
|
||||||
|
const preview = isLive && previewSize !== 'off' ? previewSrc(target) : null;
|
||||||
const showPreview = preview !== null && preview !== brokenPreview;
|
const showPreview = preview !== null && preview !== brokenPreview;
|
||||||
|
|
||||||
const liveFor = isLive && target.statusChangedAt ? Date.now() - target.statusChangedAt : null;
|
const liveFor = isLive && target.statusChangedAt ? Date.now() - target.statusChangedAt : null;
|
||||||
|
|||||||
@@ -166,6 +166,36 @@ export function usedTags(targets: WatchTarget[]): Array<{ id: string; name: stri
|
|||||||
return [...byId.values()].sort((a, b) => a.name.localeCompare(b.name));
|
return [...byId.values()].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tailles proposées pour l'aperçu du flux sur les vignettes.
|
||||||
|
*
|
||||||
|
* Réglable plutôt que fixé : selon la largeur de l'écran et le nombre de
|
||||||
|
* profils suivis, un aperçu pleine largeur rend la liste agréable ou la rend
|
||||||
|
* illisible à force de faire défiler. `off` va jusqu'à ne rien charger du tout —
|
||||||
|
* ce sont des images distantes, autant ne pas les demander quand on ne les
|
||||||
|
* regarde pas.
|
||||||
|
*/
|
||||||
|
export const PREVIEW_SIZES = [
|
||||||
|
{ id: 'off', label: 'Aucun' },
|
||||||
|
{ id: 'sm', label: 'Petit' },
|
||||||
|
{ id: 'md', label: 'Moyen' },
|
||||||
|
{ id: 'lg', label: 'Grand' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type PreviewSize = (typeof PREVIEW_SIZES)[number]['id'];
|
||||||
|
|
||||||
|
const PREVIEW_SIZE_KEY = 'stream-control.previewSize';
|
||||||
|
|
||||||
|
/** Préférence enregistrée, ou « Moyen » à défaut. */
|
||||||
|
export function loadPreviewSize(): PreviewSize {
|
||||||
|
const saved = localStorage.getItem(PREVIEW_SIZE_KEY);
|
||||||
|
return PREVIEW_SIZES.some((entry) => entry.id === saved) ? (saved as PreviewSize) : 'md';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function savePreviewSize(size: PreviewSize): void {
|
||||||
|
localStorage.setItem(PREVIEW_SIZE_KEY, size);
|
||||||
|
}
|
||||||
|
|
||||||
/** Fenêtres proposées sous les frises. */
|
/** Fenêtres proposées sous les frises. */
|
||||||
export const TIMELINE_RANGES = [
|
export const TIMELINE_RANGES = [
|
||||||
{ days: 1, label: '24 h' },
|
{ days: 1, label: '24 h' },
|
||||||
|
|||||||
@@ -725,6 +725,18 @@ fieldset.group > legend {
|
|||||||
background: var(--panel-2);
|
background: var(--panel-2);
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
cursor: zoom-in;
|
cursor: zoom-in;
|
||||||
|
/* Bride la largeur, pas la hauteur : c'est la boîte 16/9 entière qui rétrécit,
|
||||||
|
là où une hauteur maximale l'aurait recadrée en bandeau et rogné l'image. */
|
||||||
|
max-width: var(--preview-max, none);
|
||||||
|
}
|
||||||
|
/* Réglé sur la grille et hérité par les vignettes — voir le sélecteur de taille
|
||||||
|
sur la page Streamers. La taille « Aucun » ne passe pas par le CSS : la
|
||||||
|
vignette n'est alors pas rendue du tout, donc pas chargée. */
|
||||||
|
.streamer-grid.preview-sm {
|
||||||
|
--preview-max: 150px;
|
||||||
|
}
|
||||||
|
.streamer-grid.preview-md {
|
||||||
|
--preview-max: 240px;
|
||||||
}
|
}
|
||||||
.streamer-preview img {
|
.streamer-preview img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user