- import * as React from 'react';
- import {augmentVideoFile, getMimeTypeDescription} from 'packages/web-kitchensink-reactnext/src/utils/blob';
- import {formatFileSize, formatNumeral, formatSecondsDurationConcise} from 'packages/web-kitchensink-reactnext/src/utils/numeral';
- import {useFileMetadata, useFileUrl, useMediaControls} from 'src/index';
- import clsx from 'clsx';
- import {Slider} from 'categories/number/react';
- import {KeyValueTable} from 'categories/information/react';
- import {useClientSide} from 'packages/react-utils';
- import type {CommonPreviewProps} from '../../../../../categories/blob/react/src/components/FileSelectBox';
-
- export type VideoFilePreviewDerivedComponent = HTMLVideoElement;
-
- export interface VideoFilePreviewProps<F extends Partial<File> = Partial<File>>
- extends Omit<React.HTMLProps<VideoFilePreviewDerivedComponent>, 'controls'>, CommonPreviewProps<F> {}
-
- export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponent, VideoFilePreviewProps>(({
- file,
- className,
- style,
- disabled = false,
- enhanced: enhancedProp = false,
- ...etcProps
- }, forwardedRef) => {
- const { fileWithUrl } = useFileUrl({ file });
- const { fileWithMetadata, error } = useFileMetadata({
- file: fileWithUrl,
- augmentFunction: augmentVideoFile,
- });
- const {
- seekRef,
- volumeRef,
- isPlaying,
- adjustVolume,
- reset,
- startSeek,
- endSeek,
- setSeek,
- updateSeekFromPlayback,
- refreshControls,
- durationDisplay = 0,
- currentTimeDisplay = 0,
- seekTimeDisplay = 0,
- isSeeking,
- isSeekTimeCountingDown,
- mediaControllerRef,
- handleAction,
- filenameRef,
- formId,
- } = useMediaControls<HTMLVideoElement>({
- controllerRef: forwardedRef,
- });
-
- const { clientSide } = useClientSide({ clientSide: enhancedProp });
-
- if (!fileWithMetadata) {
- return null;
- }
-
- const finalSeekTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - seekTimeDisplay) : seekTimeDisplay;
- const finalCurrentTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - currentTimeDisplay) : currentTimeDisplay;
-
- return (
- <div
- className={clsx(
- 'flex flex-col sm:grid sm:grid-cols-3 gap-8 w-full',
- className,
- )}
- style={style}
- >
- <div className="h-full relative col-span-2">
- {
- typeof fileWithMetadata.url === 'string'
- && (
- <div
- className="w-full h-full bg-black flex flex-col items-stretch"
- data-testid="preview"
- key={`${fileWithMetadata.url}:${fileWithMetadata.type}`}
- >
- <div
- className="w-full flex-auto relative"
- >
- <video
- {...etcProps}
- className="sm:absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-primary/10"
- ref={mediaControllerRef}
- onLoadedMetadata={refreshControls}
- onDurationChange={refreshControls}
- onEnded={reset}
- onTimeUpdate={updateSeekFromPlayback}
- data-testid="preview"
- controls={!clientSide}
- >
- <source
- src={fileWithMetadata.url}
- type={fileWithMetadata.type}
- />
- Video playback not supported.
- </video>
- <button
- className="absolute w-full h-full top-0 left-0"
- type="submit"
- name="action"
- value="togglePlayback"
- form={formId}
- tabIndex={-1}
- >
- <span className="sr-only">
- {isPlaying ? 'Pause' : 'Play'}
- </span>
- </button>
- </div>
- {clientSide && (
- <div className="w-full flex-shrink-0 h-10 flex gap-4 items-center bg-[#000000] px-3">
- <div
- className="py-1 w-14 h-full flex-shrink-0 text-primary flex items-center justify-center"
- >
- <button
- className={
- clsx(
- 'w-full h-full flex-shrink-0 text-primary flex items-center justify-center bg-primary/30 rounded',
- 'focus:text-secondary focus:outline-0 focus:bg-secondary/30',
- 'active:text-tertiary active:bg-tertiary/30',
- 'disabled:text-primary disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-primary/30',
- )
- }
- type="submit"
- name="action"
- value="togglePlayback"
- form={formId}
- >
- {
- isPlaying
- ? (
- <svg
- aria-label="Pause"
- viewBox="0 0 24 24"
- className="w-6 h-6 fill-none stroke-current stroke-2 linecap-round linejoin-round"
- >
- <rect
- x="6"
- y="4"
- width="4"
- height="16"
- />
- <rect
- x="14"
- y="4"
- width="4"
- height="16"
- />
- </svg>
- )
- : (
- <svg
- aria-label="Play"
- viewBox="0 0 24 24"
- className="w-6 h-6 fill-none stroke-current stroke-2 linecap-round linejoin-round"
- >
- <polygon points="5 3 19 12 5 21 5 3" />
- </svg>
- )
- }
- </button>
- </div>
- <div className="flex-auto w-full flex items-center gap-2 text-sm relative">
- <button
- className="absolute overflow-hidden w-12 opacity-0 h-10 peer/seek"
- title="Toggle Seek Time Count Mode"
- type="submit"
- name="action"
- value="toggleSeekTimeCountMode"
- form={formId}
- >
- Toggle Seek Time Count Mode
- </button>
- <div
- className={clsx(
- 'before:block before:content-[attr(title)] contents tabular-nums flex-auto text-primary font-bold',
- 'peer-focus/seek:text-secondary peer-focus/seek:outline-0',
- 'peer-active/seek:text-tertiary',
- 'peer-disabled/seek:text-primary \'peer-disabled/seek:cursor-not-allowed \'peer-disabled/seek:opacity-50'
- )}
- title={
- `${
- isSeekTimeCountingDown ? '−' : '+'
- }${
- isSeeking
- ? formatSecondsDurationConcise(finalSeekTimeDisplay)
- : formatSecondsDurationConcise(finalCurrentTimeDisplay)
- }`
- }
- >
- <Slider
- className="flex-auto text-base"
- ref={seekRef}
- min={0}
- max={durationDisplay}
- onMouseDown={startSeek}
- onMouseUp={endSeek}
- onChange={setSeek}
- defaultValue="0"
- step="any"
- length="100%"
- />
- </div>
- <span className="tabular-nums font-bold">
- {formatSecondsDurationConcise(durationDisplay)}
- </span>
- </div>
- <div
- className="flex-shrink-0 w-12 flex items-center"
- >
- <Slider
- ref={volumeRef}
- max={1}
- min={0}
- onChange={adjustVolume}
- className="flex-auto"
- step="any"
- defaultValue="1"
- title="Volume"
- length="100%"
- />
- </div>
- </div>
- )}
- </div>
- )
- }
- </div>
- <div
- className="flex-shrink-0 m-0 flex flex-col gap-4 justify-between"
- >
- <KeyValueTable
- hiddenKeys
- data-testid="infoBox"
- properties={[
- Boolean(fileWithMetadata.name) && {
- key: 'Name',
- className: 'font-bold',
- valueProps: {
- ref: filenameRef,
- title: fileWithMetadata.name,
- children: fileWithMetadata.name,
- },
- },
- (clientSide && Boolean(getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)') || Boolean(fileWithMetadata.type)) && {
- key: 'Type',
- valueProps: {
- className: clsx(
- !getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) && 'opacity-50'
- ),
- children: getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)',
- },
- },
- (clientSide && Boolean(formatFileSize(fileWithMetadata.size) || '(Loading)') || Boolean(fileWithMetadata.size)) && {
- key: 'Size',
- valueProps: {
- className: clsx(
- !formatFileSize(fileWithMetadata.size) && 'opacity-50'
- ),
- title: `${formatNumeral(fileWithMetadata.size ?? 0)} byte(s)`,
- children: formatFileSize(fileWithMetadata.size) || '(Loading)',
- },
- },
- typeof fileWithMetadata.metadata?.width === 'number'
- && typeof fileWithMetadata.metadata?.height === 'number'
- && {
- key: 'Pixel Dimensions',
- valueProps: {
- children: `${formatNumeral(fileWithMetadata.metadata.width)} × ${formatNumeral(fileWithMetadata.metadata.height)} pixel(s)`,
- },
- },
- ]}
- />
- {
- clientSide
- && (
- <form
- id={formId}
- onSubmit={handleAction}
- className="flex gap-4 justify-end"
- >
- <fieldset
- disabled={disabled || typeof error !== 'undefined'}
- className="contents"
- >
- <legend className="sr-only">
- Controls
- </legend>
- <button
- type="submit"
- name="action"
- value="download"
- className={clsx(
- 'h-12 flex text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
- 'focus:outline-0',
- 'disabled:opacity-50 disabled:cursor-not-allowed',
- )}
- >
- <span
- className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
- >
- Download
- </span>
- </button>
- </fieldset>
- </form>
- )
- }
- </div>
- </div>
- );
- });
-
- VideoFilePreview.displayName = 'VideoFilePreview';
|