feat: make media inspection progressive
This commit is contained in:
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user