From b611a5dac86a80acd74a3d0ff59e7d5b96324f6b Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Tue, 6 Jun 2023 19:39:43 +0800 Subject: [PATCH] Implement other previews Implement previews for audio and text. --- packages/web/base/blob/src/index.ts | 123 ++- .../web/kitchen-sink/react-next/package.json | 7 +- .../src/pages/categories/blob/index.tsx | 958 +++++++++++++++++- .../web/kitchen-sink/react-next/yarn.lock | 25 + 4 files changed, 1100 insertions(+), 13 deletions(-) diff --git a/packages/web/base/blob/src/index.ts b/packages/web/base/blob/src/index.ts index 0c58ee4..01f9f13 100644 --- a/packages/web/base/blob/src/index.ts +++ b/packages/web/base/blob/src/index.ts @@ -1,8 +1,127 @@ import { css } from '@tesseract-design/goofy-goober'; export interface BlobBaseArgs { - + border: boolean, + block: boolean, } -export const Root = ({}: BlobBaseArgs) => css.cx( +export const Root = ({ + block, +}: BlobBaseArgs) => css.cx( + css` + vertical-align: middle; + position: relative; + border-radius: 0.25rem; + font-family: var(--font-family-base, sans-serif); + max-width: 100%; + overflow: hidden; + min-height: 20rem; + min-width: 20rem; + &:focus-within { + --color-accent: var(--color-hover, red); + } + & > span { + border-color: var(--color-accent, blue); + box-sizing: border-box; + display: inline-block; + border-width: 0.125rem; + border-style: solid; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: inherit; + z-index: 2; + pointer-events: none; + transition-property: border-color; + } + & > span::before { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + content: ''; + border-radius: 0.125rem; + opacity: 0.5; + pointer-events: none; + box-shadow: 0 0 0 0 var(--color-accent, blue); + transition-property: box-shadow; + transition-duration: 150ms; + transition-timing-function: linear; + } + &:focus-within > span::before { + box-shadow: 0 0 0 0.375rem var(--color-accent, blue); + } + `, + css.dynamic({ + display: block ? 'block' : 'inline-block', + }), +); + +export const Border = ({ + border +}: BlobBaseArgs): string => css.cx( + css.if (border) ( + css` + border-color: var(--color-accent, blue); + box-sizing: border-box; + display: inline-block; + border-width: 0.125rem; + border-style: solid; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: inherit; + pointer-events: none; + z-index: 10; + &::before { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + content: ''; + border-radius: 0.125rem; + opacity: 0.5; + pointer-events: none; + } + ` + ), ); + + +export const LabelWrapper = ({ + border, +}: BlobBaseArgs): string => css.cx( + css` + color: var(--color-accent, blue); + box-sizing: border-box; + position: absolute; + top: 0; + left: 0; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: bolder; + z-index: 1; + pointer-events: none; + transition-property: color; + line-height: 0.65; + user-select: none; + font-size: 0.725em; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + padding-right: 0.5rem; + `, + css.if (border) ( + css` + background-color: var(--color-background, white); + ` + ), +) diff --git a/packages/web/kitchen-sink/react-next/package.json b/packages/web/kitchen-sink/react-next/package.json index 40e3ce8..31f5950 100644 --- a/packages/web/kitchen-sink/react-next/package.json +++ b/packages/web/kitchen-sink/react-next/package.json @@ -16,12 +16,17 @@ "@tesseract-design/web-information-react": "link:../../categories/information/react", "@tesseract-design/web-navigation-react": "link:../../categories/navigation/react", "@tesseract-design/web-option-react": "link:../../categories/option/react", + "language-map": "^1.5.0", + "languagedetect": "^2.0.0", "next": "12.2.4", + "prismjs": "^1.29.0", "react": "18.2.0", - "react-dom": "18.2.0" + "react-dom": "18.2.0", + "wavesurfer.js": "^7.0.0-beta.6" }, "devDependencies": { "@types/node": "18.6.4", + "@types/prismjs": "^1.26.0", "@types/react": "18.0.15", "@types/react-dom": "18.0.6", "autoprefixer": "^10.4.14", diff --git a/packages/web/kitchen-sink/react-next/src/pages/categories/blob/index.tsx b/packages/web/kitchen-sink/react-next/src/pages/categories/blob/index.tsx index 23d826e..58686b4 100644 --- a/packages/web/kitchen-sink/react-next/src/pages/categories/blob/index.tsx +++ b/packages/web/kitchen-sink/react-next/src/pages/categories/blob/index.tsx @@ -1,19 +1,957 @@ import {NextPage} from 'next'; import * as React from 'react'; -import * as BlobReact from '@tesseract-design/web-blob-react'; +//import * as BlobReact from '@tesseract-design/web-blob-react'; + +import * as BlobBase from '@tesseract-design/web-base-blob'; +import * as ButtonBase from '@tesseract-design/web-base-button'; +import WaveSurfer from 'wavesurfer.js'; +import LanguageDetect from 'languagedetect'; +import Prism from 'prismjs'; +import { languages } from 'prismjs/components'; + +console.log(languages); + +interface FileWithPreview extends File { + metadata?: Record; + internal?: Record; +} + +export interface FileButtonProps extends Omit, 'size' | 'type' | 'style' | 'label' | 'list'> { + /** + * Should the component display a border? + */ + border?: boolean, + /** + * Should the component occupy the whole width of its parent? + */ + block?: boolean, + /** + * Short textual description indicating the nature of the component's value. + */ + label?: React.ReactNode, + /** + * Short textual description as guidelines for valid input values. + */ + hint?: React.ReactNode, + enhanced?: boolean, + /** + * Is the label hidden? + */ + hiddenLabel?: boolean, +} + +const formatNumeral = (n?: number) => { + if (typeof n !== 'number') { + return ''; + } + + if (!Number.isFinite(n)) { + return ''; + } + + return new Intl.NumberFormat().format(n); +} + +const formatFileSize = (size?: number) => { + if (typeof size !== 'number') { + return ''; + } + + if (!Number.isFinite(size)) { + return ''; + } + + if (size < (2 ** 10)) { + return `${formatNumeral(size)} bytes`; + } + + if (size < (2 ** 20)) { + return `${(size / (2 ** 10)).toFixed(3)} kiB`; + } + + if (size < (2 ** 30)) { + return `${(size / (2 ** 20)).toFixed(3)} MiB`; + } + + if (size < (2 ** 40)) { + return `${(size / (2 ** 30)).toFixed(3)} GiB`; + } +}; + +const MIME_TYPE_DESCRIPTIONS = { + 'image/jpeg': 'JPEG Image', + 'image/png': 'PNG Image', + 'image/tiff': 'TIFF Image', + 'image/svg+xml': 'SVG Image', + 'audio/wav': 'WAVE Audio', + 'audio/ogg': 'OGG Audio', + 'audio/mpeg': 'MPEG Audio', + 'application/json': 'JSON Data', + 'application/xml': 'XML Data', +} as const; + +const getMimeTypeDescription = (type?: string) => { + if (typeof type !== 'string') { + return ''; + } + + if (type === 'application/octet-stream') { + return type; + } + + const { + [type as keyof typeof MIME_TYPE_DESCRIPTIONS]: description = type, + } = MIME_TYPE_DESCRIPTIONS; + + return description; +} + +const formatSecondsDuration = (seconds: number) => { + const secondsInt = Math.floor(seconds); + const secondsFrac = seconds - secondsInt; + const hh = Math.floor(secondsInt / 3600).toString().padStart(2, '0'); + const mm = Math.floor(secondsInt / 60 % 60).toString().padStart(2, '0'); + const ss = (secondsInt % 60).toString().padStart(2, '0'); + const sss = Math.floor(secondsFrac * 1000).toString().padStart(3, '0'); + return `${hh}:${mm}:${ss}.${sss}`; +}; + +enum ContentType { + TEXT = 'text', + AUDIO = 'audio', + VIDEO = 'video', + IMAGE = 'image', + BINARY = 'binary', +} + +const getContentType = (mimeType?: string) => { + if (typeof mimeType !== 'string') { + return ContentType.BINARY; + } + + if ( + mimeType === 'application/json' + || mimeType === 'application/xml' + || mimeType.startsWith('text/') + ) { + return ContentType.TEXT; + } + + if (mimeType.startsWith('video/')) { + return ContentType.VIDEO; + } + + if (mimeType.startsWith('audio/')) { + return ContentType.AUDIO; + } + + if (mimeType.startsWith('image/')) { + return ContentType.IMAGE; + } + + return ContentType.BINARY; +} + +const useFilePreviews = (fileList?: FileList) => { + const [selectedFiles, setSelectedFiles] = React.useState([] as Partial[]); + React.useEffect(() => { + const loadFilePreviews = async (fileList?: FileList) => { + if (!fileList) { + return; + } + const files = Array.from(fileList); + const fileResult = await Promise.all( + files.map((f) => new Promise>((resolve, reject) => { + const contentType = getContentType(f.type); + + switch (contentType) { + case ContentType.TEXT: { + const reader = new FileReader(); + reader.addEventListener('error', () => { + reject(); + }); + + reader.addEventListener('load', async (loadEvent) => { + const target = loadEvent.target as FileReader; + const contents = target.result as string; + const resolvedAliases = Object.fromEntries( + Object + .entries(languages) + .reduce( + (resolved, [languageId, languageDefinition]) => { + if (Array.isArray(languageDefinition.alias)) { + return [ + ...resolved, + ...(languageDefinition.alias.map((a: string) => [a, { title: languageDefinition.title, extension: `.${a}`} ])), + [languageId, { title: languageDefinition.title, extension: `.${languageId}`}], + ]; + } + + if (typeof languageDefinition.alias === 'string') { + return [ + ...resolved, + [languageDefinition.alias, { title: languageDefinition.title, extension: `.${languageDefinition.alias}`}], + [languageId, { title: languageDefinition.title, extension: `.${languageId}`}], + ]; + } + + return [ + ...resolved, + [languageId, { title: languageDefinition.title, extension: `.${languageId}`}], + ]; + }, + [] as [string, { title: string, extension: string }][] + ) + ); + const metadata = Object + .entries(resolvedAliases) + .reduce( + (theMetadata, [key, value]) => { + if (typeof theMetadata.scheme === 'undefined' && f.name.endsWith(value.extension)) { + return { + ...theMetadata, + }; + } + + return theMetadata; + }, + { + contents, + } as Record + ); + + if (typeof metadata.scheme !== 'string') { + const naturalLanguageDetector = new LanguageDetect(); + const probableLanguages = naturalLanguageDetector.detect(contents); + const [languageName, probability] = probableLanguages[0]; + metadata.language = languageName; + } else { + await import(`prismjs/components/prism-${metadata.scheme}`); + } + + resolve({ + ...f, + name: f.name, + type: f.type, + size: f.size, + lastModified: f.lastModified, + metadata, + }); + }); + + reader.readAsText(f); + return; + } + case ContentType.IMAGE: { + const reader = new FileReader(); + reader.addEventListener('error', () => { + reject(); + }); + + reader.addEventListener('load', (loadEvent) => { + const target = loadEvent.target as FileReader; + const previewUrl = target.result as string; + const image = new Image(); + image.addEventListener('load', (imageLoadEvent) => { + const thisImage = imageLoadEvent.currentTarget as HTMLImageElement; + resolve({ + ...f, + name: f.name, + type: f.type, + size: f.size, + lastModified: f.lastModified, + metadata: { + previewUrl, + width: thisImage.naturalWidth, + height: thisImage.naturalHeight, + }, + }); + }); + + image.addEventListener('error', () => { + reject(); + }); + + image.src = previewUrl; + }); + + reader.readAsDataURL(f); + return; + } + case ContentType.AUDIO: { + const reader = new FileReader(); + reader.addEventListener('error', () => { + reject(); + }); + + reader.addEventListener('load', (loadEvent) => { + const target = loadEvent.target as FileReader; + const previewUrl = target.result as string; + let mediaInstance = null as (WaveSurfer | null); + + const waveSurferInstance = WaveSurfer.create({ + container: window.document.createElement('div'), + }); + + waveSurferInstance.on('ready', async () => { + const metadata = { + duration: waveSurferInstance.getDuration(), + }; + + waveSurferInstance.destroy(); + resolve({ + ...f, + name: f.name, + type: f.type, + size: f.size, + lastModified: f.lastModified, + metadata, + internal: { + createMediaInstance(container: HTMLElement) { + if (mediaInstance instanceof WaveSurfer) { + mediaInstance.destroy(); + } + mediaInstance = WaveSurfer.create({ + container, + url: previewUrl, + height: container.clientHeight, + interact: false, + cursorWidth: 0, + barWidth: 2, + barGap: 2, + barRadius: 1, + normalize: true, + }); + }, + destroyMediaInstance() { + if (!(mediaInstance instanceof WaveSurfer)) { + return; + } + mediaInstance.destroy(); + }, + play() { + if (!(mediaInstance instanceof WaveSurfer)) { + return; + } + mediaInstance.play(); + }, + pause() { + if (!(mediaInstance instanceof WaveSurfer)) { + return; + } + mediaInstance.pause(); + }, + stop() { + if (!(mediaInstance instanceof WaveSurfer)) { + return; + } + mediaInstance.stop(); + }, + playPause() { + if (!(mediaInstance instanceof WaveSurfer)) { + return; + } + mediaInstance.playPause(); + }, + }, + }) + }); + + waveSurferInstance.load(previewUrl); + }); + + reader.readAsDataURL(f); + return; + } + default: + break; + } + resolve(f); + })) + ); + + setSelectedFiles(fileResult); + } + + loadFilePreviews(fileList); + }, [fileList]); + + return React.useMemo(() => ({ + files: selectedFiles, + }), [selectedFiles]); +} + +const FilePreview = ({ + fileList: fileList, +}: { fileList?: FileList }) => { + const { files } = useFilePreviews(fileList); + const mediaContainerRef = React.useRef(null); + + const playMedia = () => { + if (files.length < 1) { + return; + } + const [theFile] = files; + if (typeof theFile.internal?.playPause === 'function') { + theFile.internal.playPause(); + } + }; + + React.useEffect(() => { + if (files.length < 1) { + return; + } + + const [theFile] = files; + if (typeof theFile.internal?.createMediaInstance === 'function' && mediaContainerRef.current) { + theFile.internal.createMediaInstance(mediaContainerRef.current); + } + + return () => { + if (typeof theFile.internal?.destroyMediaInstance === 'function') { + theFile.internal.destroyMediaInstance(); + } + } + }, [files, mediaContainerRef]); + + if (files.length < 1) { + return null; + } + + const f = files[0]; + const contentType = getContentType(f.type); + + console.log(f); -const BlobPage: NextPage = () => { return ( -
- +
+
+ { + contentType === ContentType.TEXT + && ( +
+
+ { + typeof f.metadata?.contents === 'string' + && typeof f.metadata?.scheme === 'string' + && ( +
+
+                        {
+                          f.metadata.scheme
+                          && (
+                            
+                          )
+                        }
+                        {
+                          !f.metadata.scheme
+                          && (
+                            
+                              {f.metadata.contents}
+                            
+                          )
+                        }
+                      
+
+ ) + } +
+
+
+
+ Name +
+
+ {f.name} +
+
+
+
+ Type +
+
+ {typeof f.metadata?.schemeTitle === 'string' ? `${f.metadata.schemeTitle} Source` : 'Text File'} +
+
+
+
+ Size +
+
+ {formatFileSize(f.size)} +
+
+ { + typeof f.metadata?.language === 'string' + && ( +
+
+ Language +
+
+ {f.metadata.language.slice(0, 1).toUpperCase()} + {f.metadata.language.slice(1)} +
+
+ ) + } +
+
+ ) + } + { + contentType === ContentType.IMAGE + && ( +
+
+ { + typeof f.metadata?.previewUrl === 'string' + && ( + {f.name} + ) + } +
+
+
+
+ Name +
+
+ {f.name} +
+
+
+
+ Type +
+
+ {getMimeTypeDescription(f.type)} +
+
+
+
+ Size +
+
+ {formatFileSize(f.size)} +
+
+ { + typeof f.metadata?.width === 'number' + && typeof f.metadata?.height === 'number' + && ( +
+
+ Pixel Dimensions +
+
+ {formatNumeral(f.metadata.width)}×{formatNumeral(f.metadata.height)} pixels +
+
+ ) + } +
+
+ ) + } + { + contentType === ContentType.AUDIO + && ( +
+
+
+
+
+ Name +
+
+ {f.name} +
+
+
+
+ Type +
+
+ {getMimeTypeDescription(f.type)} +
+
+
+
+ Size +
+
+ {formatFileSize(f.size)} +
+
+ { + typeof f.metadata?.duration === 'number' + && ( +
+
+ Duration +
+
+ {formatSecondsDuration(f.metadata.duration)} +
+
+ ) + } +
+
+ ) + } +
) } +const FilePreviewGrid = ({ + fileList, +}: { fileList?: FileList }) => { + const { files } = useFilePreviews(fileList); + const mediaContainerRef = React.useRef(null); + + const playMedia = (index: number) => () => { + if (files.length < 1) { + return; + } + const theFile = files[index]; + if (typeof theFile.internal?.playPause === 'function') { + theFile.internal.playPause(); + } + }; + + React.useEffect(() => { + if (files.length < 1) { + return; + } + + files.forEach((theFile, i) => { + if (typeof theFile.internal?.createMediaInstance === 'function' && mediaContainerRef.current) { + theFile.internal.createMediaInstance(mediaContainerRef.current.children[i].children[0]); + } + }); + + return () => { + files.forEach((theFile) => { + if (typeof theFile.internal?.destroyMediaInstance === 'function') { + theFile.internal.destroyMediaInstance(); + } + }); + } + }, [files, mediaContainerRef]); + + return ( +
+
+ {files.map((f, i) => ( +
+ { + f.type?.startsWith('image/') + && typeof f.metadata?.previewUrl === 'string' + && ( + {f.name} + ) + } + { + f.type?.startsWith('audio/') + && ( +
+ ) + } +
+ ))} +
+
+ ) +} + +export const FileSelectBox = React.forwardRef( + ( + { + label = '', + hint = '', + border = false, + block = false, + enhanced = false, + hiddenLabel = false, + multiple = false, + onChange, + disabled = false, + className: _className, + placeholder: _placeholder, + as: _as, + ...etcProps + }: FileButtonProps, + forwardedRef, + ) => { + const [isEnhanced, setIsEnhanced] = React.useState(false); + const [fileList, setFileList] = React.useState(); + const defaultRef = React.useRef(null); + const ref = forwardedRef ?? defaultRef; + + const addFile: React.ChangeEventHandler = (e) => { + if (!enhanced) { + onChange?.(e); + return; + } + + setFileList(e.currentTarget.files as FileList); + onChange?.(e); + }; + + const deleteFiles: React.MouseEventHandler = () => { + if (typeof ref === 'object' && ref.current) { + ref.current.value = ''; + setFileList(undefined); + } + }; + + const cancelEvent = (e: React.DragEvent) => { + e.stopPropagation(); + e.preventDefault(); + } + + const handleDropZone: React.DragEventHandler = async (e) => { + cancelEvent(e); + const { dataTransfer } = e; + if (typeof ref === 'object' && ref.current) { + const { files } = dataTransfer; + setFileList(ref.current.files = files); + ref.current.dispatchEvent(new Event('change')); + } + } + + React.useEffect(() => { + setIsEnhanced(enhanced); + }, [enhanced]); + + return ( +
+ + { + border && ( + + ) + } + { + label + && !hiddenLabel + && ( +
+ {label} +
+ ) + } + { + (fileList?.length ?? 0) < 1 + && isEnhanced + && hint + && ( +
+
+ {hint} +
+
+ ) + } + { + (fileList?.length ?? 0) > 0 + && isEnhanced + && ( + <> +
+
+ { + multiple + && ( + + ) + } + { + !multiple + && ( + + ) + } +
+
+
+
+ + Reselect + +
+
+ +
+
+ + ) + } +
+ ); + } +); + +FileSelectBox.displayName = 'FileSelectBox'; + +const BlobReact = { + FileSelectBox +} + +const BlobPage: NextPage = () => { + return ( + + ) +} + export default BlobPage; diff --git a/packages/web/kitchen-sink/react-next/yarn.lock b/packages/web/kitchen-sink/react-next/yarn.lock index 1e7723e..1c75902 100644 --- a/packages/web/kitchen-sink/react-next/yarn.lock +++ b/packages/web/kitchen-sink/react-next/yarn.lock @@ -261,6 +261,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.4.tgz#fd26723a8a3f8f46729812a7f9b4fc2d1608ed39" integrity sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg== +"@types/prismjs@^1.26.0": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.0.tgz#a1c3809b0ad61c62cac6d4e0c56d610c910b7654" + integrity sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ== + "@types/prop-types@*": version "15.7.5" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" @@ -1534,6 +1539,11 @@ json5@^1.0.2: array-includes "^3.1.5" object.assign "^4.1.3" +language-map@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/language-map/-/language-map-1.5.0.tgz#65c6d2c7493efa6708585386f0c2799d544fa367" + integrity sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg== + language-subtag-registry@~0.3.2: version "0.3.22" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" @@ -1546,6 +1556,11 @@ language-tags@=1.0.5: dependencies: language-subtag-registry "~0.3.2" +languagedetect@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/languagedetect/-/languagedetect-2.0.0.tgz#4b8fa2b7593b2a3a02fb1100891041c53238936c" + integrity sha512-AZb/liiQ+6ZoTj4f1J0aE6OkzhCo8fyH+tuSaPfSo8YHCWLFJrdSixhtO2TYdIkjcDQNaR4RmGaV2A5FJklDMQ== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -1910,6 +1925,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prismjs@^1.29.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" + integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== + prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" @@ -2331,6 +2351,11 @@ v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +wavesurfer.js@^7.0.0-beta.6: + version "7.0.0-beta.6" + resolved "https://registry.yarnpkg.com/wavesurfer.js/-/wavesurfer.js-7.0.0-beta.6.tgz#5ba26d10015e46aae3bf279438ee0903ea4e63fd" + integrity sha512-vB8J1ppZ58vozmBDmqADDdKBYY6bebSYKUgIaDXB56Qo/CpPdExSlg91tN1FAubN5swZ1IUyB8Z9xOY/TsYRoA== + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"