132 lines
4.6 KiB
TypeScript
132 lines
4.6 KiB
TypeScript
import type { ImportedMediaAsset } 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;
|
||
}
|
||
|
||
export function MediaBin({
|
||
assets,
|
||
selectedId,
|
||
onSelect,
|
||
onInspect,
|
||
inspectDisabled = false,
|
||
removeDisabled = false,
|
||
onRemove,
|
||
}: MediaBinProps) {
|
||
if (assets.length === 0) {
|
||
return (
|
||
<div className="media-bin__empty">
|
||
<Icon name="video" />
|
||
<p>Your source files will appear here.</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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;
|
||
return (
|
||
<li key={asset.id}>
|
||
<button
|
||
type="button"
|
||
className={[
|
||
'media-card',
|
||
selectedId === asset.id ? 'media-card--selected' : '',
|
||
]
|
||
.filter(Boolean)
|
||
.join(' ')}
|
||
aria-pressed={selectedId === asset.id}
|
||
onClick={() => onSelect(asset.id)}
|
||
>
|
||
<span className="media-card__type">
|
||
<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)}
|
||
{durationSeconds !== undefined
|
||
? ` · ${formatDuration(durationSeconds)}`
|
||
: ''}
|
||
</small>
|
||
<small className={`phase phase--${asset.phase}`}>
|
||
{asset.phase === 'probing'
|
||
? `${asset.browserMetadata ? 'Ready to play' : 'File ready'} · Inspecting details…`
|
||
: asset.phase === 'ready'
|
||
? `${hasVideo ? `${width ?? '?'}×${height ?? '?'}` : 'Audio'} · ${audioCount} audio`
|
||
: asset.phase === 'error'
|
||
? `${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" />
|
||
</button>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
);
|
||
}
|