From 49cdad2e9ff10d8dbab4892ef24e176670cef5f3 Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Tue, 27 Jun 2023 19:07:47 +0800 Subject: [PATCH] Update audio file preview component Add spectrum view. --- .../web-kitchensink-reactnext/package.json | 1 + .../web-kitchensink-reactnext/pnpm-lock.yaml | 10 +- .../web-kitchensink-reactnext/public/next.svg | 6 +- .../components/AudioFilePreview/index.tsx | 150 ++++++----- .../components/ImageFilePreview/index.tsx | 225 ++++++++-------- .../components/VideoFilePreview/index.tsx | 94 +++---- .../blob/react/hooks/blob/metadata.ts | 54 +++- .../blob/react/hooks/interactive/image.ts | 11 +- .../blob/react/hooks/interactive/media.ts | 12 +- .../react/components/KeyValueTable/index.tsx | 57 +++++ .../src/categories/information/react/index.ts | 1 + .../SpectrogramCanvas/index.tsx | 241 ++++++++++++++++++ .../index.tsx | 39 +-- .../src/packages/react-wavesurfer/index.ts | 12 +- .../src/pages/categories/blob/index.tsx | 20 +- .../src/utils/blob.ts | 33 +-- .../src/utils/image.ts | 8 +- 17 files changed, 675 insertions(+), 299 deletions(-) create mode 100644 packages/web-kitchensink-reactnext/src/categories/information/react/components/KeyValueTable/index.tsx create mode 100644 packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/SpectrogramCanvas/index.tsx rename packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/{WaveSurferCanvas => WaveformCanvas}/index.tsx (78%) diff --git a/packages/web-kitchensink-reactnext/package.json b/packages/web-kitchensink-reactnext/package.json index b5542a6..ad0a3ef 100644 --- a/packages/web-kitchensink-reactnext/package.json +++ b/packages/web-kitchensink-reactnext/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@reach/slider": "^0.18.0", + "@theoryofnekomata/formxtra": "^1.0.3", "@types/node": "20.3.1", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", diff --git a/packages/web-kitchensink-reactnext/pnpm-lock.yaml b/packages/web-kitchensink-reactnext/pnpm-lock.yaml index 908c8a6..c6d4f5f 100644 --- a/packages/web-kitchensink-reactnext/pnpm-lock.yaml +++ b/packages/web-kitchensink-reactnext/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -8,6 +8,9 @@ dependencies: '@reach/slider': specifier: ^0.18.0 version: 0.18.0(react-dom@18.2.0)(react@18.2.0) + '@theoryofnekomata/formxtra': + specifier: ^1.0.3 + version: 1.0.3 '@types/node': specifier: 20.3.1 version: 20.3.1 @@ -359,6 +362,11 @@ packages: tslib: 2.5.3 dev: false + /@theoryofnekomata/formxtra@1.0.3: + resolution: {integrity: sha512-xOzE07Slttpx7vbOWqXfatJ+k44TN4zUjI57A5/sNqUDtHzp3pz94A+AVPGVoBY0QXiwzMjeN4DPMp6U1qlkyg==} + engines: {node: '>=10'} + dev: false + /@types/debounce@1.2.1: resolution: {integrity: sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==} dev: true diff --git a/packages/web-kitchensink-reactnext/public/next.svg b/packages/web-kitchensink-reactnext/public/next.svg index 5174b28..c38cc2f 100644 --- a/packages/web-kitchensink-reactnext/public/next.svg +++ b/packages/web-kitchensink-reactnext/public/next.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + + diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx index b14254c..faf5ccc 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx @@ -8,11 +8,12 @@ import { } from '@/utils/numeral'; import theme from '@/styles/theme'; import {useMediaControls} from '../../hooks/interactive'; -import {useAugmentedFile} from '@/categories/blob/react'; +import {useFileMetadata} from '@/categories/blob/react'; import clsx from 'clsx'; -import {WaveSurferCanvas} from '@/packages/react-wavesurfer'; +import {SpectrogramCanvas, WaveformCanvas} from '@/packages/react-wavesurfer'; import {Slider} from '@/categories/number/react'; +import {KeyValueTable} from '@/categories/information/react'; type AudioFilePreviewDerivedComponent = HTMLAudioElement; @@ -30,7 +31,7 @@ export const AudioFilePreview = React.forwardRef { - const { augmentedFile, error } = useAugmentedFile({ + const { augmentedFile, error } = useFileMetadata({ file, augmentFunction: augmentAudioFile, }); @@ -99,19 +100,42 @@ export const AudioFilePreview = React.forwardRef - {visualizationMode === 'waveform' && ( - Math.floor(Number(c) / 2)).join(' ')})`} - progressColor={`rgb(${theme.primary})`} - interact - /> - )} -
+ Math.floor(Number(c) / 2)).join(' ')})`} + interact + // waveColor={`rgb(${theme.primary})`} + // barHeight={4} + // minPxPerSec={20000} + // hideScrollbar + // autoCenter + // autoScroll + /> + +
- ) - } - + && { + key: 'Duration', + valueProps: { + className: clsx( + !formatSecondsDurationPrecise(augmentedFile.metadata.duration) && 'opacity-50' + ), + title: `${formatNumeral(augmentedFile.metadata.duration ?? 0)} seconds`, + children: formatSecondsDurationPrecise(augmentedFile.metadata.duration), + }, + }, + ]} + />
, 'src' | 'alt'> { - file?: File | FallbackFile; + file?: Partial; disabled?: boolean; } @@ -18,8 +19,9 @@ export const ImageFilePreview = React.forwardRef { - const { augmentedFile, error } = useAugmentedFile({ - file: file as File, + const { fileWithUrl, loading: urlLoading } = useFileUrl({ file }); + const { augmentedFile, loading: metadataLoading, error } = useFileMetadata({ + file: fileWithUrl as File, augmentFunction: augmentImageFile, }); const { @@ -35,7 +37,10 @@ export const ImageFilePreview = React.forwardRef
- { - typeof augmentedFile.metadata?.previewUrl === 'string' - && ( - - ) - } - { - error - && ( -
- {error.message} -
- ) - } + {typeof augmentedFile.url === 'string' && ( + + )} + {cannotDisplayPicture && ( +
+ {error!.message} +
+ )}
-
-
-
- Name -
-
- {augmentedFile.name} -
-
-
-
- Type -
-
- {getMimeTypeDescription(augmentedFile.type, augmentedFile.name)} -
-
- { - typeof augmentedFile?.size === 'number' - && ( -
-
- Size -
-
- {formatFileSize(augmentedFile.size)} -
-
- ) - } - { + -
- Pixel Dimensions -
-
- {formatNumeral(augmentedFile.metadata.width)} × {formatNumeral(augmentedFile.metadata.height)} pixels -
-
- ) - } - { + && { + key: 'Pixel Dimensions', + valueProps: { + children: `${formatNumeral(augmentedFile.metadata.width)} × ${formatNumeral(augmentedFile.metadata.height)} pixels`, + }, + }, Array.isArray(augmentedFile.metadata?.palette) - && ( -
-
- Palette -
-
- {augmentedFile.metadata?.palette.map((rgb, i) => ( - <> - {i > 0 && ' '} - + && { + key: 'Palette', + valueProps: { + className: 'mt-1', + children: augmentedFile.metadata?.palette.map((rgb, i) => ( + + {i > 0 && ' '} + { rgb - .map((c) => c.toString().padStart(4, ' ').split('').map((c, i) => ( - - {i === 0 && ' '} - {c === ' ' && i > 0 ? '0' : c} - - ))) + .map((c) => c + .toString() + .padStart(4, ' ') + .split('') + .map((cc, j) => ( + + {j === 0 && ' '} + {cc === ' ' && j > 0 ? '0' : cc} + + )) + ) .flat() } - - ))} -
-
- ) - } - + + )), + }, + }, + ]} + />
diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx index 253ec4d..68ef95d 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import {augmentVideoFile, getMimeTypeDescription} from '@/utils/blob'; import {formatFileSize, formatNumeral, formatSecondsDurationConcise} from '@/utils/numeral'; -import {useAugmentedFile, useMediaControls} from '@tesseract-design/web-blob-react'; +import {useFileMetadata, useMediaControls} from '@tesseract-design/web-blob-react'; import clsx from 'clsx'; import {Slider} from '@tesseract-design/web-number-react'; +import {KeyValueTable} from '@/categories/information/react'; type VideoFilePreviewDerivedComponent = HTMLVideoElement; @@ -21,7 +22,7 @@ export const VideoFilePreview = React.forwardRef { - const { augmentedFile, error } = useAugmentedFile({ + const { augmentedFile, error } = useFileMetadata({ file, augmentFunction: augmentVideoFile, }); @@ -220,58 +221,47 @@ export const VideoFilePreview = React.forwardRef -
-
-
- Name -
-
- {augmentedFile.name} -
-
-
-
- Type -
-
- {getMimeTypeDescription(augmentedFile.type, augmentedFile.name)} -
-
-
-
- Size -
-
- {formatFileSize(augmentedFile.size)} -
-
- { + -
- Pixel Dimensions -
-
- {formatNumeral(augmentedFile.metadata.width)}×{formatNumeral(augmentedFile.metadata.height)} pixels -
-
- ) - } - + && { + key: 'Pixel Dimensions', + valueProps: { + children: `${formatNumeral(augmentedFile.metadata.width)} × ${formatNumeral(augmentedFile.metadata.height)} pixels`, + }, + }, + ]} + /> = Partial> { +export interface UseFileUrlOptions { + file?: Partial; +} + +export const useFileUrl = (options: UseFileUrlOptions) => { + const { file } = options; + const [fileWithUrl, setFileWithUrl] = React.useState | undefined>(file); + const [loading, setLoading] = React.useState(false); + + React.useEffect(() => { + if (!file) { + setFileWithUrl(undefined); + setLoading(false); + return; + } + + setLoading(true); + addDataUrl(file) + .then((fileWithUrl) => { + setFileWithUrl(fileWithUrl); + setLoading(false); + }) + .catch(() => { + setFileWithUrl(file); + setLoading(false); + }); + }, [file]); + + return React.useMemo(() => ({ + fileWithUrl, + loading, + }), [fileWithUrl, loading]); +}; + +export interface UseFileMetadataOptions = Partial> { file?: File; augmentFunction: (file: File) => Promise; } -export const useAugmentedFile = >(options = {} as UseAugmentedFileOptions) => { +export const useFileMetadata = >(options: UseFileMetadataOptions) => { const { file, augmentFunction } = options; - const [augmentedFile, setAugmentedFile] = React.useState(); + const [fileWithMetadata, setFileWithMetadata] = React.useState(file as T | undefined); + const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(); React.useEffect(() => { if (!file) { + setFileWithMetadata(undefined); + setLoading(false); return; } + setLoading(true); setError(undefined); augmentFunction(file) .then((theAugmentedFile) => { - setAugmentedFile(theAugmentedFile); + setFileWithMetadata(theAugmentedFile); + setLoading(false); }) .catch((error) => { setError(error); + setLoading(false); }); }, [file, augmentFunction]); return React.useMemo(() => ({ - augmentedFile: (augmentedFile ?? file) as T | undefined, + augmentedFile: fileWithMetadata, error, - }), [augmentedFile, file, error]); + loading, + }), [fileWithMetadata, loading, error]); }; diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/image.ts b/packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/image.ts index eeac848..80391b1 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/image.ts +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/image.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { getFormValues } from '@theoryofnekomata/formxtra'; export interface UseImageControlsOptions { actionFormKey?: string; @@ -41,8 +42,14 @@ export const useImageControls = (options = {} as UseImageControlsOptions) => { const handleAction: React.FormEventHandler = React.useCallback((e) => { e.preventDefault(); - const formData = new FormData(e.currentTarget, (e.nativeEvent as unknown as { submitter: HTMLElement }).submitter); - const actionName = formData.get(actionFormKey) as keyof typeof actions; + const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement }; + const formData = getFormValues( + e.currentTarget, + { + submitter: nativeEvent.submitter, + } + ); + const actionName = formData[actionFormKey] as keyof typeof actions; const { [actionName]: actionFunction } = actions; actionFunction?.(); }, [actions, actionFormKey]); diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/media.ts b/packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/media.ts index 82cbaef..91f52ea 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/media.ts +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/media.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import {getFormValues} from '@theoryofnekomata/formxtra'; export interface UseMediaControlsOptions { controllerRef: React.Ref; @@ -138,8 +139,15 @@ export const useMediaControls = ({ const handleAction: React.FormEventHandler = React.useCallback((e) => { e.preventDefault(); - const formData = new FormData(e.currentTarget, (e.nativeEvent as unknown as { submitter: HTMLElement }).submitter); - const actionName = formData.get(actionFormKey) as keyof typeof actions; + e.preventDefault(); + const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement }; + const formData = getFormValues( + e.currentTarget, + { + submitter: nativeEvent.submitter, + } + ); + const actionName = formData[actionFormKey] as keyof typeof actions; const { [actionName]: actionFunction } = actions; actionFunction?.(); }, [actions, actionFormKey]); diff --git a/packages/web-kitchensink-reactnext/src/categories/information/react/components/KeyValueTable/index.tsx b/packages/web-kitchensink-reactnext/src/categories/information/react/components/KeyValueTable/index.tsx new file mode 100644 index 0000000..908588b --- /dev/null +++ b/packages/web-kitchensink-reactnext/src/categories/information/react/components/KeyValueTable/index.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import clsx from 'clsx'; + +type KeyValueTableDerivedElement = HTMLDListElement; + +interface KeyValueProperty { + key: string; + className?: string; + valueProps?: React.HTMLProps; +} + +export interface KeyValueTableProps extends Omit, 'children'> { + hiddenKeys?: boolean; + properties?: (KeyValueProperty | boolean)[]; +} + +export const KeyValueTable = React.forwardRef(({ + hiddenKeys = false, + properties = [], + ...etcProps +}, forwardedRef) => ( +
+ { + properties.map((property) => typeof property === 'object' && ( +
+
+ {property.key} +
+
+ {property.valueProps?.children} +
+
+ )) + } +
+)); + +KeyValueTable.displayName = 'KeyValueTable'; diff --git a/packages/web-kitchensink-reactnext/src/categories/information/react/index.ts b/packages/web-kitchensink-reactnext/src/categories/information/react/index.ts index 701883e..5ac7847 100644 --- a/packages/web-kitchensink-reactnext/src/categories/information/react/index.ts +++ b/packages/web-kitchensink-reactnext/src/categories/information/react/index.ts @@ -1 +1,2 @@ export * from './components/Badge'; +export * from './components/KeyValueTable'; diff --git a/packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/SpectrogramCanvas/index.tsx b/packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/SpectrogramCanvas/index.tsx new file mode 100644 index 0000000..caf32ed --- /dev/null +++ b/packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/SpectrogramCanvas/index.tsx @@ -0,0 +1,241 @@ +import * as React from 'react'; +import {WaveSurferOptions} from 'wavesurfer.js'; +import clsx from 'clsx'; +import {getFormValues} from '@theoryofnekomata/formxtra'; + +type SpectrogramCanvasDerivedComponent = HTMLAudioElement; + +export interface SpectrogramCanvasProps + extends React.HTMLProps, + Omit { + waveColor?: string; +} + +export const SpectrogramCanvas = React.forwardRef(({ + className, + children, + controls, + waveColor, + progressColor, + cursorColor, + cursorWidth, + barWidth, + barGap, + barRadius, + barHeight, + barAlign, + minPxPerSec, + peaks, + duration, + autoPlay, + interact, + hideScrollbar, + audioRate, + autoScroll, + autoCenter, + sampleRate, + splitChannels, + normalize, + ...etcProps +}, forwardedRef) => { + const [isPlaying, setIsPlaying] = React.useState(false); + const defaultRef = React.useRef(null); + const ref = forwardedRef ?? defaultRef; + const containerRef = React.useRef(null); + const waveSurferRef = React.useRef(null); + const cursorRef = React.useRef(null); + + const handleAction: React.FormEventHandler = (e) => { + e.preventDefault(); + e.preventDefault(); + const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement }; + const formData = getFormValues( + e.currentTarget, + { + submitter: nativeEvent.submitter, + } + ); + const actionName = formData['action'] as string; + switch (actionName) { + case 'togglePlayback': + setIsPlaying((prev) => !prev); + break; + default: + break; + } + }; + + React.useEffect(() => { + const { current: container } = containerRef; + const media = typeof ref === 'object' ? ref?.current : null; + const { current: waveSurferCurrent } = waveSurferRef; + const handleTimeUpdate = (e: Event) => { + const thisMedia = e.currentTarget as HTMLAudioElement; + if (cursorRef.current) { + cursorRef.current.style.width = `${(thisMedia?.currentTime ?? 0) / (thisMedia?.duration ?? 1) * 100}%`; + } + }; + + const load = async (ref: React.Ref) => { + if (!(typeof ref === 'object' && ref?.current)) { + return; + } + + if (!(typeof containerRef === 'object' && containerRef?.current)) { + return; + } + const { default: WaveSurfer } = await import('wavesurfer.js'); + const { default: Spectrogram, } = await import('wavesurfer.js/dist/plugins/spectrogram'); + const dummyContainer = window.document.createElement('div'); + window.document.body.appendChild(dummyContainer); + + const waveSurferInstance = WaveSurfer.create({ + container: dummyContainer, + height: 100, + fillParent: true, + autoplay: autoPlay, + waveColor, + progressColor, + cursorColor, + barWidth, + barGap, + barRadius, + barHeight, + barAlign, + minPxPerSec, + peaks, + duration, + interact, + hideScrollbar, + audioRate, + autoScroll, + autoCenter, + sampleRate, + splitChannels, + normalize, + plugins: [], + cursorWidth, + media: media ?? undefined, + }); + + let colorMap: Array<[number, number, number, number]> = []; + if (waveColor?.toLowerCase().startsWith('rgb(')) { + const waveColorParse = waveColor.match(/rgb\((\d+)[, ]\s*(\d+)[, ]\s*(\d+)\)/); + const waveColorR = parseInt(waveColorParse?.[1] ?? '0', 10); + const waveColorG = parseInt(waveColorParse?.[2] ?? '0', 10); + const waveColorB = parseInt(waveColorParse?.[3] ?? '0', 10); + for (let i = 0; i < 256; i += 1) { + colorMap.push([waveColorR / 256, waveColorG / 256, waveColorB / 256, i / 256]); + } + } + waveSurferInstance.registerPlugin( + Spectrogram.create({ + container: containerRef.current, + labels: true, + labelsColor: 'rgb(0 0 0/0)', + height: containerRef.current.clientHeight, + colorMap, + }), + ) + + waveSurferInstance.on('ready', () => { + if (!container) { + return; + } + while (container.children.length > 1) { + container.removeChild(container.children[0]); + } + dummyContainer.remove(); + }); + await waveSurferInstance.load(ref.current.currentSrc); + waveSurferInstance.setTime(ref.current.currentTime); + waveSurferRef.current = waveSurferInstance; + media!.addEventListener('timeupdate', handleTimeUpdate); + }; + void load(ref); + return () => { + if (waveSurferCurrent) { + (waveSurferCurrent as unknown as Record).destroy(); + } + if (container) { + container.innerHTML = ''; + } + if (!media) { + return; + } + media.removeEventListener('timeupdate', handleTimeUpdate); + }; + }, [ + ref, + autoPlay, + waveColor, + progressColor, + cursorColor, + barWidth, + barGap, + barRadius, + barHeight, + barAlign, + minPxPerSec, + peaks, + duration, + interact, + hideScrollbar, + audioRate, + autoScroll, + autoCenter, + sampleRate, + splitChannels, + normalize, + cursorWidth, + ]); + + return ( +
+
+
+
+
+ {controls && ( + + + + )} +
+ ); +}); + +SpectrogramCanvas.displayName = 'WavesurferCanvas'; diff --git a/packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveSurferCanvas/index.tsx b/packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveformCanvas/index.tsx similarity index 78% rename from packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveSurferCanvas/index.tsx rename to packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveformCanvas/index.tsx index 9696555..8bbf15f 100644 --- a/packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveSurferCanvas/index.tsx +++ b/packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveformCanvas/index.tsx @@ -1,14 +1,15 @@ import * as React from 'react'; import {WaveSurferOptions} from 'wavesurfer.js'; import clsx from 'clsx'; +import {getFormValues} from '@theoryofnekomata/formxtra'; -type WaveSurferCanvasDerivedComponent = HTMLAudioElement; +type SpectrogramCanvasDerivedComponent = HTMLAudioElement; -export interface WaveSurferCanvasProps - extends React.HTMLProps, - Omit {} +export interface WaveformCanvasProps + extends React.HTMLProps, + Omit {} -export const WaveSurferCanvas = React.forwardRef(({ +export const WaveformCanvas = React.forwardRef(({ className, children, controls, @@ -33,7 +34,6 @@ export const WaveSurferCanvas = React.forwardRef { const [isPlaying, setIsPlaying] = React.useState(false); @@ -44,9 +44,16 @@ export const WaveSurferCanvas = React.forwardRef = (e) => { e.preventDefault(); - const formData = new FormData(e.currentTarget, (e.nativeEvent as unknown as { submitter: HTMLElement }).submitter); - const action = formData.get('action'); - switch (action) { + e.preventDefault(); + const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement }; + const formData = getFormValues( + e.currentTarget, + { + submitter: nativeEvent.submitter, + } + ); + const actionName = formData['action'] as string; + switch (actionName) { case 'togglePlayback': setIsPlaying((prev) => !prev); break; @@ -69,11 +76,9 @@ export const WaveSurferCanvas = React.forwardRef -
+
+
+
{controls && (
{ - play: () => Promise; - pause: () => void; - on: (event: string, callback: () => void) => void; - seekTo: (time: number) => void; - load: (url: string) => void; - destroy: () => void; -} +export * from './WaveformCanvas'; +export * from './SpectrogramCanvas'; diff --git a/packages/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx b/packages/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx index 9f5e2d2..538ddc3 100644 --- a/packages/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx +++ b/packages/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx @@ -3,15 +3,18 @@ import * as React from 'react'; import * as BlobReact from '@tesseract-design/web-blob-react'; import {DefaultLayout} from '@/components/DefaultLayout'; import {Section, Subsection} from '@/components/Section'; +import {addDataUrl} from '@/utils/blob'; const BlobPage: NextPage = () => { - const [imageFile, setImageFile] = React.useState(); + const [imageFile, setImageFile] = React.useState>(); React.useEffect(() => { fetch('/image.png').then((response) => { - response.blob().then((blob) => { - setImageFile(new File([blob], 'image.png', { + response.blob().then(async (blob) => { + const imageFile = new File([blob], 'image.png', { type: 'image/png', - })); + }); + const theFile = await addDataUrl(imageFile); + setImageFile(theFile); }); }); }, []); @@ -43,7 +46,14 @@ const BlobPage: NextPage = () => {
diff --git a/packages/web-kitchensink-reactnext/src/utils/blob.ts b/packages/web-kitchensink-reactnext/src/utils/blob.ts index 48b79fe..14c8cbc 100644 --- a/packages/web-kitchensink-reactnext/src/utils/blob.ts +++ b/packages/web-kitchensink-reactnext/src/utils/blob.ts @@ -116,9 +116,9 @@ export const getContentType = (mimeType?: string, filename?: string) => { return ContentType.BINARY; } -export const readAsText = (blob: Blob) => blob.text(); +export const readAsText = (blob: Partial) => blob?.text?.(); -export const readAsDataURL = (blob: Blob) => new Promise((resolve, reject) => { +export const readAsDataURL = (blob: Partial) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.addEventListener('error', () => { reject(new Error('Could not read file as data URL')); @@ -132,12 +132,12 @@ export const readAsDataURL = (blob: Blob) => new Promise((resolve, rejec resolve(e.target.result as string); }); - reader.readAsDataURL(blob); + reader.readAsDataURL(blob as Blob); }); export const readAsArrayBuffer = (blob: Blob) => blob.arrayBuffer(); -interface FileWithResolvedType extends Partial { +interface FileWithResolvedType extends Partial { resolvedType: T; originalFile?: File; } @@ -156,7 +156,7 @@ export interface TextFile extends FileWithResolvedType { const augmentTextFile = async (f: File): Promise => { const contents = await readAsText(f); - const metadata = getTextMetadata(contents, f.name) as TextFileMetadata; + const metadata = getTextMetadata(contents ?? '', f.name) as TextFileMetadata; return { ...f, name: f.name, @@ -186,22 +186,25 @@ export interface ImageFile extends FileWithResolvedType { metadata?: ImageFileMetadata; } -export const augmentImageFile = async (f: File): Promise => { - const previewUrl = await readAsDataURL(f); - const imageMetadata = await getImageMetadata(previewUrl) as ImageFileMetadata; +export interface FileWithDataUrl extends File { + url?: string; +} + +export const addDataUrl = async (f: Partial): Promise> => { + (f as unknown as Record).url = await readAsDataURL(f); + return f; +} + +export const augmentImageFile = async (f: FileWithDataUrl): Promise => { + const imageMetadata = await getImageMetadata(f.url) as ImageFileMetadata; return { name: f.name, type: f.type, size: f.size, lastModified: f.lastModified, resolvedType: ContentType.IMAGE, - originalFile: f, - metadata: { - previewUrl, - width: imageMetadata.width, - height: imageMetadata.height, - palette: imageMetadata.palette, - }, + url: f.url, + metadata: imageMetadata, }; }; diff --git a/packages/web-kitchensink-reactnext/src/utils/image.ts b/packages/web-kitchensink-reactnext/src/utils/image.ts index 27048cd..81f44e4 100644 --- a/packages/web-kitchensink-reactnext/src/utils/image.ts +++ b/packages/web-kitchensink-reactnext/src/utils/image.ts @@ -1,7 +1,8 @@ import ColorThief from 'colorthief'; -export const getImageMetadata = (imageUrl: string) => new Promise>((resolve, reject) => { +export const getImageMetadata = (imageUrl?: string) => new Promise>((resolve, reject) => { const image = new Image(); + image.addEventListener('load', async (imageLoadEvent) => { const thisImage = imageLoadEvent.currentTarget as HTMLImageElement; const colorThief = new ColorThief(); @@ -20,5 +21,10 @@ export const getImageMetadata = (imageUrl: string) => new Promise