Design system.
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 

317 rader
12 KiB

  1. import * as React from 'react';
  2. import {augmentVideoFile, getMimeTypeDescription} from 'packages/web-kitchensink-reactnext/src/utils/blob';
  3. import {formatFileSize, formatNumeral, formatSecondsDurationConcise} from 'packages/web-kitchensink-reactnext/src/utils/numeral';
  4. import {useFileMetadata, useFileUrl, useMediaControls} from 'src/index';
  5. import clsx from 'clsx';
  6. import {Slider} from 'categories/number/react';
  7. import {KeyValueTable} from 'categories/information/react';
  8. import {useClientSide} from 'packages/react-utils';
  9. import type {CommonPreviewProps} from '../../../../../categories/blob/react/src/components/FileSelectBox';
  10. export type VideoFilePreviewDerivedComponent = HTMLVideoElement;
  11. export interface VideoFilePreviewProps<F extends Partial<File> = Partial<File>>
  12. extends Omit<React.HTMLProps<VideoFilePreviewDerivedComponent>, 'controls'>, CommonPreviewProps<F> {}
  13. export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponent, VideoFilePreviewProps>(({
  14. file,
  15. className,
  16. style,
  17. disabled = false,
  18. enhanced: enhancedProp = false,
  19. ...etcProps
  20. }, forwardedRef) => {
  21. const { fileWithUrl } = useFileUrl({ file });
  22. const { fileWithMetadata, error } = useFileMetadata({
  23. file: fileWithUrl,
  24. augmentFunction: augmentVideoFile,
  25. });
  26. const {
  27. seekRef,
  28. volumeRef,
  29. isPlaying,
  30. adjustVolume,
  31. reset,
  32. startSeek,
  33. endSeek,
  34. setSeek,
  35. updateSeekFromPlayback,
  36. refreshControls,
  37. durationDisplay = 0,
  38. currentTimeDisplay = 0,
  39. seekTimeDisplay = 0,
  40. isSeeking,
  41. isSeekTimeCountingDown,
  42. mediaControllerRef,
  43. handleAction,
  44. filenameRef,
  45. formId,
  46. } = useMediaControls<HTMLVideoElement>({
  47. controllerRef: forwardedRef,
  48. });
  49. const { clientSide } = useClientSide({ clientSide: enhancedProp });
  50. if (!fileWithMetadata) {
  51. return null;
  52. }
  53. const finalSeekTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - seekTimeDisplay) : seekTimeDisplay;
  54. const finalCurrentTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - currentTimeDisplay) : currentTimeDisplay;
  55. return (
  56. <div
  57. className={clsx(
  58. 'flex flex-col sm:grid sm:grid-cols-3 gap-8 w-full',
  59. className,
  60. )}
  61. style={style}
  62. >
  63. <div className="h-full relative col-span-2">
  64. {
  65. typeof fileWithMetadata.url === 'string'
  66. && (
  67. <div
  68. className="w-full h-full bg-black flex flex-col items-stretch"
  69. data-testid="preview"
  70. key={`${fileWithMetadata.url}:${fileWithMetadata.type}`}
  71. >
  72. <div
  73. className="w-full flex-auto relative"
  74. >
  75. <video
  76. {...etcProps}
  77. 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"
  78. ref={mediaControllerRef}
  79. onLoadedMetadata={refreshControls}
  80. onDurationChange={refreshControls}
  81. onEnded={reset}
  82. onTimeUpdate={updateSeekFromPlayback}
  83. data-testid="preview"
  84. controls={!clientSide}
  85. >
  86. <source
  87. src={fileWithMetadata.url}
  88. type={fileWithMetadata.type}
  89. />
  90. Video playback not supported.
  91. </video>
  92. <button
  93. className="absolute w-full h-full top-0 left-0"
  94. type="submit"
  95. name="action"
  96. value="togglePlayback"
  97. form={formId}
  98. tabIndex={-1}
  99. >
  100. <span className="sr-only">
  101. {isPlaying ? 'Pause' : 'Play'}
  102. </span>
  103. </button>
  104. </div>
  105. {clientSide && (
  106. <div className="w-full flex-shrink-0 h-10 flex gap-4 items-center bg-[#000000] px-3">
  107. <div
  108. className="py-1 w-14 h-full flex-shrink-0 text-primary flex items-center justify-center"
  109. >
  110. <button
  111. className={
  112. clsx(
  113. 'w-full h-full flex-shrink-0 text-primary flex items-center justify-center bg-primary/30 rounded',
  114. 'focus:text-secondary focus:outline-0 focus:bg-secondary/30',
  115. 'active:text-tertiary active:bg-tertiary/30',
  116. 'disabled:text-primary disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-primary/30',
  117. )
  118. }
  119. type="submit"
  120. name="action"
  121. value="togglePlayback"
  122. form={formId}
  123. >
  124. {
  125. isPlaying
  126. ? (
  127. <svg
  128. aria-label="Pause"
  129. viewBox="0 0 24 24"
  130. className="w-6 h-6 fill-none stroke-current stroke-2 linecap-round linejoin-round"
  131. >
  132. <rect
  133. x="6"
  134. y="4"
  135. width="4"
  136. height="16"
  137. />
  138. <rect
  139. x="14"
  140. y="4"
  141. width="4"
  142. height="16"
  143. />
  144. </svg>
  145. )
  146. : (
  147. <svg
  148. aria-label="Play"
  149. viewBox="0 0 24 24"
  150. className="w-6 h-6 fill-none stroke-current stroke-2 linecap-round linejoin-round"
  151. >
  152. <polygon points="5 3 19 12 5 21 5 3" />
  153. </svg>
  154. )
  155. }
  156. </button>
  157. </div>
  158. <div className="flex-auto w-full flex items-center gap-2 text-sm relative">
  159. <button
  160. className="absolute overflow-hidden w-12 opacity-0 h-10 peer/seek"
  161. title="Toggle Seek Time Count Mode"
  162. type="submit"
  163. name="action"
  164. value="toggleSeekTimeCountMode"
  165. form={formId}
  166. >
  167. Toggle Seek Time Count Mode
  168. </button>
  169. <div
  170. className={clsx(
  171. 'before:block before:content-[attr(title)] contents tabular-nums flex-auto text-primary font-bold',
  172. 'peer-focus/seek:text-secondary peer-focus/seek:outline-0',
  173. 'peer-active/seek:text-tertiary',
  174. 'peer-disabled/seek:text-primary \'peer-disabled/seek:cursor-not-allowed \'peer-disabled/seek:opacity-50'
  175. )}
  176. title={
  177. `${
  178. isSeekTimeCountingDown ? '−' : '+'
  179. }${
  180. isSeeking
  181. ? formatSecondsDurationConcise(finalSeekTimeDisplay)
  182. : formatSecondsDurationConcise(finalCurrentTimeDisplay)
  183. }`
  184. }
  185. >
  186. <Slider
  187. className="flex-auto text-base"
  188. ref={seekRef}
  189. min={0}
  190. max={durationDisplay}
  191. onMouseDown={startSeek}
  192. onMouseUp={endSeek}
  193. onChange={setSeek}
  194. defaultValue="0"
  195. step="any"
  196. length="100%"
  197. />
  198. </div>
  199. <span className="tabular-nums font-bold">
  200. {formatSecondsDurationConcise(durationDisplay)}
  201. </span>
  202. </div>
  203. <div
  204. className="flex-shrink-0 w-12 flex items-center"
  205. >
  206. <Slider
  207. ref={volumeRef}
  208. max={1}
  209. min={0}
  210. onChange={adjustVolume}
  211. className="flex-auto"
  212. step="any"
  213. defaultValue="1"
  214. title="Volume"
  215. length="100%"
  216. />
  217. </div>
  218. </div>
  219. )}
  220. </div>
  221. )
  222. }
  223. </div>
  224. <div
  225. className="flex-shrink-0 m-0 flex flex-col gap-4 justify-between"
  226. >
  227. <KeyValueTable
  228. hiddenKeys
  229. data-testid="infoBox"
  230. properties={[
  231. Boolean(fileWithMetadata.name) && {
  232. key: 'Name',
  233. className: 'font-bold',
  234. valueProps: {
  235. ref: filenameRef,
  236. title: fileWithMetadata.name,
  237. children: fileWithMetadata.name,
  238. },
  239. },
  240. (clientSide && Boolean(getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)') || Boolean(fileWithMetadata.type)) && {
  241. key: 'Type',
  242. valueProps: {
  243. className: clsx(
  244. !getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) && 'opacity-50'
  245. ),
  246. children: getMimeTypeDescription(fileWithMetadata.type, fileWithMetadata.name) || '(Loading)',
  247. },
  248. },
  249. (clientSide && Boolean(formatFileSize(fileWithMetadata.size) || '(Loading)') || Boolean(fileWithMetadata.size)) && {
  250. key: 'Size',
  251. valueProps: {
  252. className: clsx(
  253. !formatFileSize(fileWithMetadata.size) && 'opacity-50'
  254. ),
  255. title: `${formatNumeral(fileWithMetadata.size ?? 0)} byte(s)`,
  256. children: formatFileSize(fileWithMetadata.size) || '(Loading)',
  257. },
  258. },
  259. typeof fileWithMetadata.metadata?.width === 'number'
  260. && typeof fileWithMetadata.metadata?.height === 'number'
  261. && {
  262. key: 'Pixel Dimensions',
  263. valueProps: {
  264. children: `${formatNumeral(fileWithMetadata.metadata.width)} × ${formatNumeral(fileWithMetadata.metadata.height)} pixel(s)`,
  265. },
  266. },
  267. ]}
  268. />
  269. {
  270. clientSide
  271. && (
  272. <form
  273. id={formId}
  274. onSubmit={handleAction}
  275. className="flex gap-4 justify-end"
  276. >
  277. <fieldset
  278. disabled={disabled || typeof error !== 'undefined'}
  279. className="contents"
  280. >
  281. <legend className="sr-only">
  282. Controls
  283. </legend>
  284. <button
  285. type="submit"
  286. name="action"
  287. value="download"
  288. className={clsx(
  289. 'h-12 flex text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
  290. 'focus:outline-0',
  291. 'disabled:opacity-50 disabled:cursor-not-allowed',
  292. )}
  293. >
  294. <span
  295. className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
  296. >
  297. Download
  298. </span>
  299. </button>
  300. </fieldset>
  301. </form>
  302. )
  303. }
  304. </div>
  305. </div>
  306. );
  307. });
  308. VideoFilePreview.displayName = 'VideoFilePreview';