|
- import * as React from 'react';
- import {augmentAudioFile, getMimeTypeDescription} from 'packages/web-kitchensink-reactnext/src/utils/blob';
- import {
- formatFileSize,
- formatNumeral,
- formatSecondsDurationConcise,
- formatSecondsDurationPrecise,
- } from 'packages/web-kitchensink-reactnext/src/utils/numeral';
- import theme from 'packages/web-kitchensink-reactnext/src/styles/theme';
- import {useMediaControls} from '../../hooks/interactive';
- import {useFileMetadata, useFileUrl} from 'src/index';
-
- import clsx from 'clsx';
- import {SpectrogramCanvas, WaveformCanvas} from 'packages/web-kitchensink-reactnext/src/packages/react-wavesurfer';
- 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 AudioFilePreviewDerivedElement = HTMLAudioElement;
-
- export interface AudioFilePreviewProps<F extends Partial<File> = Partial<File>>
- extends Omit<React.HTMLProps<AudioFilePreviewDerivedElement>, 'controls'>, CommonPreviewProps<F> {}
-
- export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedElement, AudioFilePreviewProps>(({
- file,
- style,
- className,
- enhanced: enhancedProp = false,
- disabled = false,
- ...etcProps
- }, forwardedRef) => {
- const { fileWithUrl } = useFileUrl({
- file,
- });
- const { fileWithMetadata, error } = useFileMetadata({
- file: fileWithUrl,
- augmentFunction: augmentAudioFile,
- });
- const {
- mediaControllerRef,
- refreshControls,
- reset,
- updateSeekFromPlayback,
- isPlaying,
- isSeeking,
- currentTimeDisplay = 0,
- seekTimeDisplay = 0,
- durationDisplay = 0,
- isSeekTimeCountingDown,
- adjustVolume,
- volumeRef,
- handleAction,
- filenameRef,
- seekRef,
- startSeek,
- endSeek,
- setSeek,
- visualizationId,
- formId,
- } = useMediaControls<HTMLAudioElement>({
- controllerRef: forwardedRef,
- visualizationMode: 'waveform',
- });
- 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="sm:h-full relative col-span-2">
- {
- typeof fileWithMetadata.url === 'string'
- && (
- <div
- className="w-full h-full bg-black flex flex-col items-stretch"
- key={`${fileWithMetadata.url}:${fileWithMetadata.type}`}
- >
- <div
- className="w-full flex-auto relative aspect-video sm:aspect-auto"
- >
- <audio
- {...etcProps}
- controls={!clientSide}
- ref={mediaControllerRef}
- onLoadedMetadata={refreshControls}
- onDurationChange={refreshControls}
- onEnded={reset}
- onTimeUpdate={updateSeekFromPlayback}
- data-testid="preview"
- >
- <source
- src={fileWithMetadata.url}
- type={fileWithMetadata.type}
- />
- Audio playback not supported.
- </audio>
- {clientSide && (
- <>
- <div className="flex justify-end w-full h-full gap-4 absolute top-0 right-0 z-[5] px-4">
- <div className="contents">
- <input
- type="radio"
- name="visualizationMode"
- value="waveform"
- className="sr-only peer/waveform"
- defaultChecked
- id={`${visualizationId}-waveform`}
- />
- <label
- htmlFor={`${visualizationId}-waveform`}
- className={clsx(
- 'relative z-[5]',
- 'h-12 flex items-center justify-center leading-none gap-4 select-none',
- 'text-primary cursor-pointer',
- 'peer-focus/waveform:text-secondary',
- 'peer-active/waveform:text-tertiary',
- 'peer-checked/waveform:text-tertiary',
- 'peer-disabled/waveform:text-primary peer-disabled/waveform:cursor-not-allowed peer-disabled/waveform:opacity-50',
- )}
- >
- <span
- className={clsx(
- 'flex items-center uppercase font-bold h-full w-full whitespace-nowrap overflow-hidden text-ellipsis',
- )}
- >
- Waveform
- </span>
- </label>
- <WaveformCanvas
- className={clsx(
- '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 cursor-text opacity-0',
- 'peer-checked/waveform:opacity-100',
- )}
- audioRef={mediaControllerRef}
- data-testid="preview"
- barWidth={1}
- barGap={1}
- progressColor={`rgb(${theme['color-primary']})`}
- waveColor={`rgb(${theme['color-primary'].split(' ').map((c) => Math.floor(Number(c) / 2)).join(' ')})`}
- interact
- // waveColor={`rgb(${theme.primary})`}
- // barHeight={4}
- // minPxPerSec={20000}
- // hideScrollbar
- // autoCenter
- // autoScroll
- />
- </div>
- <div
- className="contents"
- >
- <input
- type="radio"
- name="visualizationMode"
- value="spectrum"
- className="sr-only peer/waveform"
- id={`${visualizationId}-spectrum`}
- />
- <label
- htmlFor={`${visualizationId}-spectrum`}
- className={clsx(
- 'relative z-[5]',
- 'h-12 flex items-center justify-center leading-none gap-4 select-none',
- 'text-primary cursor-pointer',
- 'peer-focus/waveform:text-secondary',
- 'peer-active/waveform:text-tertiary',
- 'peer-checked/waveform:text-tertiary',
- 'peer-disabled/waveform:text-primary peer-disabled/waveform:cursor-not-allowed peer-disabled/waveform:opacity-50',
- )}
- >
- <span
- className={clsx(
- 'flex items-center uppercase font-bold h-full w-full whitespace-nowrap overflow-hidden text-ellipsis',
- )}
- >
- Spectrum
- </span>
- </label>
- <SpectrogramCanvas
- className={clsx(
- '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 pointer-events-none opacity-0',
- 'peer-checked/waveform:opacity-100',
- )}
- audioRef={mediaControllerRef}
- data-testid="preview"
- barWidth={1}
- barGap={1}
- waveColor={`rgb(${theme['color-primary']})`}
- cursorWidth={2}
- minPxPerSec={20000}
- hideScrollbar
- autoCenter
- autoScroll
- />
- </div>
- </div>
- </>
- )}
- </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="text-base flex-auto"
- 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
- className="flex-auto"
- ref={volumeRef}
- max={1}
- min={0}
- onChange={adjustVolume}
- 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?.duration === 'number'
- && {
- key: 'Duration',
- valueProps: {
- className: clsx(
- !formatSecondsDurationPrecise(fileWithMetadata.metadata.duration) && 'opacity-50'
- ),
- title: `${formatNumeral(fileWithMetadata.metadata.duration ?? 0)} second(s)`,
- children: formatSecondsDurationPrecise(fileWithMetadata.metadata.duration),
- },
- },
- ]}
- />
- {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 bg-negative 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>
- );
- });
-
- AudioFilePreview.displayName = 'AudioFilePreview';
|