Design system.
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 

415 satır
17 KiB

  1. import * as React from 'react';
  2. import {augmentAudioFile, getMimeTypeDescription} from 'packages/web-kitchensink-reactnext/src/utils/blob';
  3. import {
  4. formatFileSize,
  5. formatNumeral,
  6. formatSecondsDurationConcise,
  7. formatSecondsDurationPrecise,
  8. } from 'packages/web-kitchensink-reactnext/src/utils/numeral';
  9. import theme from 'packages/web-kitchensink-reactnext/src/styles/theme';
  10. import {useMediaControls} from '../../hooks/interactive';
  11. import {useFileMetadata, useFileUrl} from 'src/index';
  12. import clsx from 'clsx';
  13. import {SpectrogramCanvas, WaveformCanvas} from 'packages/web-kitchensink-reactnext/src/packages/react-wavesurfer';
  14. import {Slider} from 'categories/number/react';
  15. import {KeyValueTable} from 'categories/information/react';
  16. import {useClientSide} from 'packages/react-utils';
  17. import type {CommonPreviewProps} from '../../../../../categories/blob/react/src/components/FileSelectBox';
  18. export type AudioFilePreviewDerivedElement = HTMLAudioElement;
  19. export interface AudioFilePreviewProps<F extends Partial<File> = Partial<File>>
  20. extends Omit<React.HTMLProps<AudioFilePreviewDerivedElement>, 'controls'>, CommonPreviewProps<F> {}
  21. export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedElement, AudioFilePreviewProps>(({
  22. file,
  23. style,
  24. className,
  25. enhanced: enhancedProp = false,
  26. disabled = false,
  27. ...etcProps
  28. }, forwardedRef) => {
  29. const { fileWithUrl } = useFileUrl({
  30. file,
  31. });
  32. const { fileWithMetadata, error } = useFileMetadata({
  33. file: fileWithUrl,
  34. augmentFunction: augmentAudioFile,
  35. });
  36. const {
  37. mediaControllerRef,
  38. refreshControls,
  39. reset,
  40. updateSeekFromPlayback,
  41. isPlaying,
  42. isSeeking,
  43. currentTimeDisplay = 0,
  44. seekTimeDisplay = 0,
  45. durationDisplay = 0,
  46. isSeekTimeCountingDown,
  47. adjustVolume,
  48. volumeRef,
  49. handleAction,
  50. filenameRef,
  51. seekRef,
  52. startSeek,
  53. endSeek,
  54. setSeek,
  55. visualizationId,
  56. formId,
  57. } = useMediaControls<HTMLAudioElement>({
  58. controllerRef: forwardedRef,
  59. visualizationMode: 'waveform',
  60. });
  61. const { clientSide } = useClientSide({ clientSide: enhancedProp });
  62. if (!fileWithMetadata) {
  63. return null;
  64. }
  65. const finalSeekTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - seekTimeDisplay) : seekTimeDisplay;
  66. const finalCurrentTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - currentTimeDisplay) : currentTimeDisplay;
  67. return (
  68. <div
  69. className={clsx(
  70. 'flex flex-col sm:grid sm:grid-cols-3 gap-8 w-full',
  71. className,
  72. )}
  73. style={style}
  74. >
  75. <div className="sm:h-full relative col-span-2">
  76. {
  77. typeof fileWithMetadata.url === 'string'
  78. && (
  79. <div
  80. className="w-full h-full bg-black flex flex-col items-stretch"
  81. key={`${fileWithMetadata.url}:${fileWithMetadata.type}`}
  82. >
  83. <div
  84. className="w-full flex-auto relative aspect-video sm:aspect-auto"
  85. >
  86. <audio
  87. {...etcProps}
  88. controls={!clientSide}
  89. ref={mediaControllerRef}
  90. onLoadedMetadata={refreshControls}
  91. onDurationChange={refreshControls}
  92. onEnded={reset}
  93. onTimeUpdate={updateSeekFromPlayback}
  94. data-testid="preview"
  95. >
  96. <source
  97. src={fileWithMetadata.url}
  98. type={fileWithMetadata.type}
  99. />
  100. Audio playback not supported.
  101. </audio>
  102. {clientSide && (
  103. <>
  104. <div className="flex justify-end w-full h-full gap-4 absolute top-0 right-0 z-[5] px-4">
  105. <div className="contents">
  106. <input
  107. type="radio"
  108. name="visualizationMode"
  109. value="waveform"
  110. className="sr-only peer/waveform"
  111. defaultChecked
  112. id={`${visualizationId}-waveform`}
  113. />
  114. <label
  115. htmlFor={`${visualizationId}-waveform`}
  116. className={clsx(
  117. 'relative z-[5]',
  118. 'h-12 flex items-center justify-center leading-none gap-4 select-none',
  119. 'text-primary cursor-pointer',
  120. 'peer-focus/waveform:text-secondary',
  121. 'peer-active/waveform:text-tertiary',
  122. 'peer-checked/waveform:text-tertiary',
  123. 'peer-disabled/waveform:text-primary peer-disabled/waveform:cursor-not-allowed peer-disabled/waveform:opacity-50',
  124. )}
  125. >
  126. <span
  127. className={clsx(
  128. 'flex items-center uppercase font-bold h-full w-full whitespace-nowrap overflow-hidden text-ellipsis',
  129. )}
  130. >
  131. Waveform
  132. </span>
  133. </label>
  134. <WaveformCanvas
  135. className={clsx(
  136. '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',
  137. 'peer-checked/waveform:opacity-100',
  138. )}
  139. audioRef={mediaControllerRef}
  140. data-testid="preview"
  141. barWidth={1}
  142. barGap={1}
  143. progressColor={`rgb(${theme['color-primary']})`}
  144. waveColor={`rgb(${theme['color-primary'].split(' ').map((c) => Math.floor(Number(c) / 2)).join(' ')})`}
  145. interact
  146. // waveColor={`rgb(${theme.primary})`}
  147. // barHeight={4}
  148. // minPxPerSec={20000}
  149. // hideScrollbar
  150. // autoCenter
  151. // autoScroll
  152. />
  153. </div>
  154. <div
  155. className="contents"
  156. >
  157. <input
  158. type="radio"
  159. name="visualizationMode"
  160. value="spectrum"
  161. className="sr-only peer/waveform"
  162. id={`${visualizationId}-spectrum`}
  163. />
  164. <label
  165. htmlFor={`${visualizationId}-spectrum`}
  166. className={clsx(
  167. 'relative z-[5]',
  168. 'h-12 flex items-center justify-center leading-none gap-4 select-none',
  169. 'text-primary cursor-pointer',
  170. 'peer-focus/waveform:text-secondary',
  171. 'peer-active/waveform:text-tertiary',
  172. 'peer-checked/waveform:text-tertiary',
  173. 'peer-disabled/waveform:text-primary peer-disabled/waveform:cursor-not-allowed peer-disabled/waveform:opacity-50',
  174. )}
  175. >
  176. <span
  177. className={clsx(
  178. 'flex items-center uppercase font-bold h-full w-full whitespace-nowrap overflow-hidden text-ellipsis',
  179. )}
  180. >
  181. Spectrum
  182. </span>
  183. </label>
  184. <SpectrogramCanvas
  185. className={clsx(
  186. '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',
  187. 'peer-checked/waveform:opacity-100',
  188. )}
  189. audioRef={mediaControllerRef}
  190. data-testid="preview"
  191. barWidth={1}
  192. barGap={1}
  193. waveColor={`rgb(${theme['color-primary']})`}
  194. cursorWidth={2}
  195. minPxPerSec={20000}
  196. hideScrollbar
  197. autoCenter
  198. autoScroll
  199. />
  200. </div>
  201. </div>
  202. </>
  203. )}
  204. </div>
  205. {clientSide && (
  206. <div className="w-full flex-shrink-0 h-10 flex gap-4 items-center bg-[#000000] px-3">
  207. <div
  208. className="py-1 w-14 h-full flex-shrink-0 text-primary flex items-center justify-center"
  209. >
  210. <button
  211. className={
  212. clsx(
  213. 'w-full h-full flex-shrink-0 text-primary flex items-center justify-center bg-primary/30 rounded',
  214. 'focus:text-secondary focus:outline-0 focus:bg-secondary/30',
  215. 'active:text-tertiary active:bg-tertiary/30',
  216. 'disabled:text-primary disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-primary/30',
  217. )
  218. }
  219. type="submit"
  220. name="action"
  221. value="togglePlayback"
  222. form={formId}
  223. >
  224. {
  225. isPlaying
  226. ? (
  227. <svg
  228. aria-label="Pause"
  229. viewBox="0 0 24 24"
  230. className="w-6 h-6 fill-none stroke-current stroke-2 linecap-round linejoin-round"
  231. >
  232. <rect
  233. x="6"
  234. y="4"
  235. width="4"
  236. height="16"
  237. />
  238. <rect
  239. x="14"
  240. y="4"
  241. width="4"
  242. height="16"
  243. />
  244. </svg>
  245. )
  246. : (
  247. <svg
  248. aria-label="Play"
  249. viewBox="0 0 24 24"
  250. className="w-6 h-6 fill-none stroke-current stroke-2 linecap-round linejoin-round"
  251. >
  252. <polygon points="5 3 19 12 5 21 5 3" />
  253. </svg>
  254. )
  255. }
  256. </button>
  257. </div>
  258. <div className="flex-auto w-full flex items-center gap-2 text-sm relative">
  259. <button
  260. className="absolute overflow-hidden w-12 opacity-0 h-10 peer/seek"
  261. title="Toggle Seek Time Count Mode"
  262. type="submit"
  263. name="action"
  264. value="toggleSeekTimeCountMode"
  265. form={formId}
  266. >
  267. Toggle Seek Time Count Mode
  268. </button>
  269. <div
  270. className={clsx(
  271. 'before:block before:content-[attr(title)] contents tabular-nums flex-auto text-primary font-bold',
  272. 'peer-focus/seek:text-secondary peer-focus/seek:outline-0',
  273. 'peer-active/seek:text-tertiary',
  274. 'peer-disabled/seek:text-primary \'peer-disabled/seek:cursor-not-allowed \'peer-disabled/seek:opacity-50'
  275. )}
  276. title={
  277. `${
  278. isSeekTimeCountingDown ? '−' : '+'
  279. }${
  280. isSeeking
  281. ? formatSecondsDurationConcise(finalSeekTimeDisplay)
  282. : formatSecondsDurationConcise(finalCurrentTimeDisplay)
  283. }`
  284. }
  285. >
  286. <Slider
  287. className="text-base flex-auto"
  288. ref={seekRef}
  289. min={0}
  290. max={durationDisplay}
  291. onMouseDown={startSeek}
  292. onMouseUp={endSeek}
  293. onChange={setSeek}
  294. defaultValue="0"
  295. step="any"
  296. length="100%"
  297. />
  298. </div>
  299. <span className="tabular-nums font-bold">
  300. {formatSecondsDurationConcise(durationDisplay)}
  301. </span>
  302. </div>
  303. <div
  304. className="flex-shrink-0 w-12 flex items-center"
  305. >
  306. <Slider
  307. className="flex-auto"
  308. ref={volumeRef}
  309. max={1}
  310. min={0}
  311. onChange={adjustVolume}
  312. step="any"
  313. defaultValue="1"
  314. title="Volume"
  315. length="100%"
  316. />
  317. </div>
  318. </div>
  319. )}
  320. </div>
  321. )
  322. }
  323. </div>
  324. <div className="flex-shrink-0 m-0 flex flex-col gap-4 justify-between">
  325. <KeyValueTable
  326. hiddenKeys
  327. data-testid="infoBox"
  328. properties={[
  329. Boolean(fileWithMetadata.name) && {
  330. key: 'Name',
  331. className: 'font-bold',
  332. valueProps: {
  333. ref: filenameRef,
  334. title: fileWithMetadata.name,
  335. children: fileWithMetadata.name,
  336. },
  337. },
  338. (clientSide && Boolean(getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)') || Boolean(fileWithMetadata.type)) && {
  339. key: 'Type',
  340. valueProps: {
  341. className: clsx(
  342. !getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) && 'opacity-50'
  343. ),
  344. children: getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)',
  345. },
  346. },
  347. (clientSide && Boolean(formatFileSize(fileWithMetadata.size) || '(Loading)') || Boolean(fileWithMetadata.size)) && {
  348. key: 'Size',
  349. valueProps: {
  350. className: clsx(
  351. !formatFileSize(fileWithMetadata.size) && 'opacity-50'
  352. ),
  353. title: `${formatNumeral(fileWithMetadata.size ?? 0)} byte(s)`,
  354. children: formatFileSize(fileWithMetadata.size) || '(Loading)',
  355. },
  356. },
  357. typeof fileWithMetadata.metadata?.duration === 'number'
  358. && {
  359. key: 'Duration',
  360. valueProps: {
  361. className: clsx(
  362. !formatSecondsDurationPrecise(fileWithMetadata.metadata.duration) && 'opacity-50'
  363. ),
  364. title: `${formatNumeral(fileWithMetadata.metadata.duration ?? 0)} second(s)`,
  365. children: formatSecondsDurationPrecise(fileWithMetadata.metadata.duration),
  366. },
  367. },
  368. ]}
  369. />
  370. {clientSide && (
  371. <form
  372. id={formId}
  373. onSubmit={handleAction}
  374. className="flex gap-4 justify-end"
  375. >
  376. <fieldset
  377. disabled={disabled || typeof error !== 'undefined'}
  378. className="contents"
  379. >
  380. <legend className="sr-only">
  381. Controls
  382. </legend>
  383. <button
  384. type="submit"
  385. name="action"
  386. value="download"
  387. className={clsx(
  388. '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',
  389. 'focus:outline-0',
  390. 'disabled:opacity-50 disabled:cursor-not-allowed',
  391. )}
  392. >
  393. <span
  394. className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
  395. >
  396. Download
  397. </span>
  398. </button>
  399. </fieldset>
  400. </form>
  401. )}
  402. </div>
  403. </div>
  404. );
  405. });
  406. AudioFilePreview.displayName = 'AudioFilePreview';