feat: make media inspection progressive

This commit is contained in:
2026-07-27 18:25:47 +02:00
parent 6c68f9bfbd
commit f2c0fc12d2
32 changed files with 2169 additions and 462 deletions

View File

@@ -1,11 +1,19 @@
import type { ImportedMediaAsset } from '../app/application-state';
import { formatBytes, formatDuration } from '../app/application-state';
import {
assetDurationSeconds,
assetHasVideo,
formatBytes,
formatDuration,
} from '../app/application-state';
import { Icon } from './Icon';
export interface MediaBinProps {
assets: readonly ImportedMediaAsset[];
selectedId?: string;
onSelect: (id: string) => void;
onInspect: (id: string) => void;
inspectDisabled?: boolean;
removeDisabled?: boolean;
onRemove: (id: string) => void;
}
@@ -13,6 +21,9 @@ export function MediaBin({
assets,
selectedId,
onSelect,
onInspect,
inspectDisabled = false,
removeDisabled = false,
onRemove,
}: MediaBinProps) {
if (assets.length === 0) {
@@ -27,9 +38,13 @@ export function MediaBin({
return (
<ul className="media-list" aria-label="Imported media">
{assets.map((asset) => {
const hasVideo = assetHasVideo(asset) === true;
const video = asset.probe?.streams.find(
(stream) => stream.type === 'video'
);
const durationSeconds = assetDurationSeconds(asset);
const width = video?.width ?? asset.browserMetadata?.width;
const height = video?.height ?? asset.browserMetadata?.height;
const audioCount =
asset.probe?.streams.filter((stream) => stream.type === 'audio')
.length ?? 0;
@@ -43,34 +58,67 @@ export function MediaBin({
]
.filter(Boolean)
.join(' ')}
aria-pressed={selectedId === asset.id}
onClick={() => onSelect(asset.id)}
>
<span className="media-card__type">
<Icon name={video ? 'video' : 'audio'} />
<Icon name={hasVideo ? 'video' : 'audio'} />
</span>
<span className="media-card__content">
<strong title={asset.file.name}>{asset.file.name}</strong>
<small>
{formatBytes(asset.file.size)}
{asset.probe?.durationSeconds !== undefined
? ` · ${formatDuration(asset.probe.durationSeconds)}`
{durationSeconds !== undefined
? ` · ${formatDuration(durationSeconds)}`
: ''}
</small>
<small className={`phase phase--${asset.phase}`}>
{asset.phase === 'probing'
? 'Inspecting locally…'
? `${asset.browserMetadata ? 'Ready to play' : 'File ready'} · Inspecting details…`
: asset.phase === 'ready'
? `${video ? `${video.width ?? '?'}×${video.height ?? '?'}` : 'Audio'} · ${audioCount} audio`
? `${hasVideo ? `${width ?? '?'}×${height ?? '?'}` : 'Audio'} · ${audioCount} audio`
: asset.phase === 'error'
? asset.error
: 'Queued'}
? `${asset.browserMetadata ? 'Ready to play' : 'File ready'} · Details unavailable`
: asset.browserMetadataError
? 'File ready · Basic metadata unavailable'
: asset.browserMetadata
? 'Ready to play · Details on demand'
: 'File ready · Reading basic metadata…'}
</small>
</span>
</button>
{asset.phase !== 'ready' ? (
<button
className="icon-action"
type="button"
aria-label={`${asset.phase === 'error' ? 'Retry detailed inspection for' : 'Inspect details for'} ${asset.file.name}`}
title={
asset.phase === 'error'
? asset.error
: 'Read codecs, streams, chapters and tags'
}
disabled={inspectDisabled || asset.phase === 'probing'}
{...(asset.error
? { 'aria-describedby': `inspection-error-${asset.id}` }
: {})}
onClick={() => onInspect(asset.id)}
>
<Icon name={asset.phase === 'probing' ? 'queue' : 'info'} />
</button>
) : null}
{asset.error ? (
<span
className="visually-hidden"
id={`inspection-error-${asset.id}`}
>
{asset.error}
</span>
) : null}
<button
className="icon-action"
type="button"
aria-label={`Remove ${asset.file.name}`}
disabled={removeDisabled}
onClick={() => onRemove(asset.id)}
>
<Icon name="trash" />