@@ -14,16 +14,17 @@ import clsx from 'clsx'; | |||
import {SpectrogramCanvas, WaveformCanvas} from '@modal-soft/react-wavesurfer'; | |||
import {Slider} from '@/categories/number/react'; | |||
import {KeyValueTable} from '@/categories/information/react'; | |||
import {useEnhanced} from '@modal-soft/react-utils'; | |||
type AudioFilePreviewDerivedComponent = HTMLAudioElement; | |||
type AudioFilePreviewDerivedElement = HTMLAudioElement; | |||
export interface AudioFilePreviewProps<F extends Partial<File> = Partial<File>> extends Omit<React.HTMLProps<AudioFilePreviewDerivedComponent>, 'controls'> { | |||
export interface AudioFilePreviewProps<F extends Partial<File> = Partial<File>> extends Omit<React.HTMLProps<AudioFilePreviewDerivedElement>, 'controls'> { | |||
file?: F; | |||
disabled?: boolean; | |||
enhanced?: boolean; | |||
} | |||
export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponent, AudioFilePreviewProps>(({ | |||
export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedElement, AudioFilePreviewProps>(({ | |||
file, | |||
style, | |||
className, | |||
@@ -58,16 +59,12 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen | |||
endSeek, | |||
setSeek, | |||
visualizationId, | |||
formId, | |||
} = useMediaControls<HTMLAudioElement>({ | |||
controllerRef: forwardedRef, | |||
visualizationMode: 'waveform', | |||
}); | |||
const formId = React.useId(); | |||
const [enhanced, setEnhanced] = React.useState(false); | |||
React.useEffect(() => { | |||
setEnhanced(enhancedProp); | |||
}, [enhancedProp]); | |||
const { enhanced } = useEnhanced({ enhanced: enhancedProp }); | |||
if (!fileWithMetadata) { | |||
return null; | |||
@@ -90,11 +87,10 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen | |||
&& ( | |||
<div | |||
className="w-full h-full bg-black flex flex-col items-stretch" | |||
data-testid="preview" | |||
key={`${fileWithMetadata.url}:${fileWithMetadata.type}`} | |||
> | |||
<div | |||
className="w-full flex-auto relative aspect-video sm:aspect-auto" | |||
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`} | |||
> | |||
<audio | |||
{...etcProps} | |||
@@ -104,9 +100,9 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen | |||
onDurationChange={refreshControls} | |||
onEnded={reset} | |||
onTimeUpdate={updateSeekFromPlayback} | |||
data-testid="preview" | |||
> | |||
<source | |||
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`} | |||
src={fileWithMetadata.url} | |||
type={fileWithMetadata.type} | |||
/> | |||
@@ -149,7 +145,7 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen | |||
'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', | |||
)} | |||
ref={mediaControllerRef} | |||
audioRef={mediaControllerRef} | |||
data-testid="preview" | |||
barWidth={1} | |||
barGap={1} | |||
@@ -199,7 +195,7 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen | |||
'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', | |||
)} | |||
ref={mediaControllerRef} | |||
audioRef={mediaControllerRef} | |||
data-testid="preview" | |||
barWidth={1} | |||
barGap={1} | |||
@@ -216,7 +212,7 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen | |||
)} | |||
</div> | |||
{enhanced && ( | |||
<div className="w-full flex-shrink-0 h-10 flex gap-4 items-center"> | |||
<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" | |||
> | |||
@@ -297,8 +293,10 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen | |||
} | |||
> | |||
<Slider | |||
className="bg-negative text-base flex-auto" | |||
className="text-base flex-auto" | |||
ref={seekRef} | |||
min={0} | |||
max={durationDisplay} | |||
onMouseDown={startSeek} | |||
onMouseUp={endSeek} | |||
onChange={setSeek} | |||
@@ -342,6 +340,7 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen | |||
className: 'font-bold', | |||
valueProps: { | |||
ref: filenameRef, | |||
title: fileWithMetadata.name, | |||
children: fileWithMetadata.name, | |||
}, | |||
}, | |||
@@ -1,26 +1,214 @@ | |||
import * as React from 'react'; | |||
import {AudioFile, getMimeTypeDescription} from '@/utils/blob'; | |||
import {formatFileSize, formatNumeral, formatSecondsDurationPrecise} from '@/utils/numeral'; | |||
import {useAudioControls} from '@tesseract-design/web-blob-react'; | |||
import {augmentAudioFile} from '@/utils/blob'; | |||
import theme from '@/styles/theme'; | |||
import {useMediaControls} from '../../hooks/interactive'; | |||
import {useFileMetadata, useFileUrl} from '@/categories/blob/react'; | |||
import clsx from 'clsx'; | |||
import {SpectrogramCanvas, WaveformCanvas} from '@modal-soft/react-wavesurfer'; | |||
import {useEnhanced} from '@modal-soft/react-utils'; | |||
export interface AudioMiniFilePreviewProps { | |||
file: AudioFile; | |||
type AudioMiniFilePreviewDerivedElement = HTMLAudioElement; | |||
export interface AudioMiniFilePreviewProps<F extends Partial<File> = Partial<File>> extends Omit<React.HTMLProps<AudioMiniFilePreviewDerivedElement>, 'controls'> { | |||
file?: F; | |||
disabled?: boolean; | |||
enhanced?: boolean; | |||
} | |||
export const AudioMiniFilePreview: React.FC<AudioMiniFilePreviewProps> = ({ | |||
file: f, | |||
}) => { | |||
export const AudioMiniFilePreview = React.forwardRef<AudioMiniFilePreviewDerivedElement, AudioMiniFilePreviewProps>(({ | |||
file, | |||
style, | |||
className, | |||
enhanced: enhancedProp = false, | |||
disabled = false, | |||
...etcProps | |||
}, forwardedRef) => { | |||
const { fileWithUrl } = useFileUrl({ | |||
file, | |||
}); | |||
const { fileWithMetadata, error } = useFileMetadata({ | |||
file: fileWithUrl, | |||
augmentFunction: augmentAudioFile, | |||
}); | |||
const { | |||
mountRef, | |||
playMedia, | |||
mediaControllerRef, | |||
refreshControls, | |||
reset, | |||
updateSeekFromPlayback, | |||
isPlaying, | |||
} = useAudioControls({ file: f }); | |||
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 { enhanced } = useEnhanced({ enhanced: enhancedProp }); | |||
if (!fileWithMetadata) { | |||
return null; | |||
} | |||
const finalSeekTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - seekTimeDisplay) : seekTimeDisplay; | |||
const finalCurrentTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - currentTimeDisplay) : currentTimeDisplay; | |||
return ( | |||
<div | |||
className="absolute top-0 left-0 w-full h-full cursor-pointer" | |||
ref={mountRef} | |||
onClick={playMedia} | |||
/> | |||
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" | |||
data-testid="preview" | |||
> | |||
<div | |||
className="w-full flex-auto relative aspect-video sm:aspect-auto" | |||
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`} | |||
> | |||
<audio | |||
{...etcProps} | |||
controls={!enhanced} | |||
ref={mediaControllerRef} | |||
onLoadedMetadata={refreshControls} | |||
onDurationChange={refreshControls} | |||
onEnded={reset} | |||
onTimeUpdate={updateSeekFromPlayback} | |||
> | |||
<source | |||
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`} | |||
src={fileWithMetadata.url} | |||
type={fileWithMetadata.type} | |||
/> | |||
Audio playback not supported. | |||
</audio> | |||
{enhanced && ( | |||
<> | |||
<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> | |||
</div> | |||
) | |||
} | |||
</div> | |||
</div> | |||
); | |||
}; | |||
}); | |||
AudioMiniFilePreview.displayName = 'AudioMiniFilePreview'; |
@@ -5,15 +5,16 @@ import {useFileMetadata, useFileUrl} from '@/categories/blob/react'; | |||
import clsx from 'clsx'; | |||
import {KeyValueTable} from '@/categories/information/react'; | |||
import {BinaryDataCanvas} from '@modal-soft/react-binary-data-canvas'; | |||
import {useEnhanced} from '@modal-soft/react-utils'; | |||
type BinaryFilePreviewDerivedComponent = HTMLDivElement; | |||
type BinaryFilePreviewDerivedElement = HTMLDivElement; | |||
export interface BinaryFilePreviewProps<F extends Partial<File> = Partial<File>> extends React.HTMLProps<BinaryFilePreviewDerivedComponent> { | |||
export interface BinaryFilePreviewProps<F extends Partial<File> = Partial<File>> extends React.HTMLProps<BinaryFilePreviewDerivedElement> { | |||
file?: F; | |||
enhanced?: boolean; | |||
} | |||
export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedComponent, BinaryFilePreviewProps>(({ | |||
export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedElement, BinaryFilePreviewProps>(({ | |||
file, | |||
className, | |||
style, | |||
@@ -25,11 +26,7 @@ export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedCompon | |||
file: fileWithUrl, | |||
augmentFunction: augmentBinaryFile, | |||
}); | |||
const [enhanced, setEnhanced] = React.useState(false); | |||
React.useEffect(() => { | |||
setEnhanced(enhancedProp); | |||
}, [enhancedProp]); | |||
const { enhanced } = useEnhanced({ enhanced: enhancedProp }); | |||
if (!fileWithMetadata) { | |||
return null; | |||
@@ -42,6 +39,7 @@ export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedCompon | |||
className, | |||
)} | |||
style={style} | |||
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`} | |||
> | |||
<div className="h-full relative"> | |||
<div className="absolute top-0 left-0 w-full h-full"> | |||
@@ -54,6 +52,7 @@ export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedCompon | |||
role="presentation" | |||
className="w-full h-full select-none overflow-hidden text-xs" | |||
ref={forwardedRef} | |||
key={`${fileWithMetadata.url}:${fileWithMetadata.type}`} | |||
> | |||
<BinaryDataCanvas | |||
arrayBuffer={fileWithMetadata.metadata?.contents} | |||
@@ -4,7 +4,7 @@ import { ImageFilePreview } from '../ImageFilePreview'; | |||
import { AudioFilePreview } from '../AudioFilePreview'; | |||
import { VideoFilePreview } from '../VideoFilePreview'; | |||
import { BinaryFilePreview } from '../BinaryFilePreview'; | |||
import {AugmentedFile, augmentFile, ContentType, getContentType} from '@/utils/blob'; | |||
import { ContentType, getContentType } from '@/utils/blob'; | |||
const FILE_PREVIEW_COMPONENTS: Record<ContentType, React.ElementType> = { | |||
[ContentType.TEXT]: TextFilePreview, | |||
@@ -14,43 +14,16 @@ const FILE_PREVIEW_COMPONENTS: Record<ContentType, React.ElementType> = { | |||
[ContentType.BINARY]: BinaryFilePreview, | |||
}; | |||
const useFilePreviews = (fileList?: FileList) => { | |||
const [selectedFiles, setSelectedFiles] = React.useState([] as AugmentedFile[]); | |||
React.useEffect(() => { | |||
const loadFilePreviews = async (fileList: FileList) => { | |||
const files = Array.from(fileList); | |||
return Promise.all( | |||
files.map(async (f) => { | |||
const augmentedFile = await augmentFile(f); | |||
if (augmentedFile.resolvedType === ContentType.TEXT && augmentedFile.metadata?.scheme) { | |||
await import(`prismjs/components/prism-${augmentedFile.metadata.scheme}`); | |||
} | |||
return augmentedFile; | |||
}) | |||
); | |||
} | |||
if (fileList) { | |||
loadFilePreviews(fileList).then((fileResult) => { | |||
setSelectedFiles(fileResult); | |||
}); | |||
} | |||
}, [fileList]); | |||
return React.useMemo(() => ({ | |||
files: selectedFiles, | |||
}), [selectedFiles]); | |||
}; | |||
export interface FilePreviewProps { | |||
fileList?: FileList; | |||
className?: string; | |||
} | |||
export const FilePreview: React.FC<FilePreviewProps> = ({ | |||
fileList, | |||
className, | |||
}) => { | |||
const { files } = useFilePreviews(fileList); | |||
if (files.length < 1) { | |||
if ((fileList?.length ?? 0) < 1) { | |||
return null; | |||
} | |||
@@ -59,6 +32,11 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ | |||
const FilePreviewComponent = FILE_PREVIEW_COMPONENTS[contentType] ?? BinaryFilePreview; | |||
return ( | |||
<FilePreviewComponent file={f} /> | |||
<FilePreviewComponent | |||
key={contentType} | |||
file={f} | |||
className={className} | |||
enhanced | |||
/> | |||
); | |||
}; |
@@ -1,10 +1,11 @@ | |||
import * as React from 'react'; | |||
import {AugmentedFile, augmentFile, ContentType, getMimeTypeDescription} from '@/utils/blob'; | |||
import { FilePreview as FilePreviewComponent} from '../FilePreview'; | |||
import {ContentType, FileWithResolvedContentType, getMimeTypeDescription} from '@/utils/blob'; | |||
import { FilePreview } from '../FilePreview'; | |||
import {formatFileSize} from '@/utils/numeral'; | |||
import {AudioMiniFilePreview} from '@tesseract-design/web-blob-react'; | |||
import {delegateTriggerChangeEvent} from '@/utils/event'; | |||
import clsx from 'clsx'; | |||
import {useEnhanced} from '@modal-soft/react-utils'; | |||
export interface FileButtonProps extends Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style' | 'label' | 'list'> { | |||
/** | |||
@@ -30,69 +31,46 @@ export interface FileButtonProps extends Omit<React.HTMLProps<HTMLInputElement>, | |||
hiddenLabel?: boolean, | |||
} | |||
const useFilePreviews = (fileList?: FileList) => { | |||
const [selectedFiles, setSelectedFiles] = React.useState([] as AugmentedFile[]); | |||
React.useEffect(() => { | |||
const loadFilePreviews = async (fileList: FileList) => { | |||
const files = Array.from(fileList); | |||
return Promise.all( | |||
files.map(async (f) => { | |||
const augmentedFile = await augmentFile(f); | |||
if (augmentedFile.resolvedType === ContentType.TEXT && augmentedFile.metadata?.scheme) { | |||
await import(`prismjs/components/prism-${augmentedFile.metadata.scheme}`); | |||
} | |||
return augmentedFile; | |||
}) | |||
); | |||
} | |||
if (fileList) { | |||
loadFilePreviews(fileList).then((fileResult) => { | |||
setSelectedFiles(fileResult); | |||
}); | |||
} | |||
}, [fileList]); | |||
return React.useMemo(() => ({ | |||
files: selectedFiles, | |||
}), [selectedFiles]); | |||
} | |||
const FilePreviewGrid = ({ | |||
fileList, | |||
fileList: files, | |||
}: { fileList?: FileList }) => { | |||
const { files } = useFilePreviews(fileList); | |||
if (!files) { | |||
return null; | |||
} | |||
return ( | |||
<div className="w-full h-full overflow-auto -mx-4 px-4"> | |||
<div className="w-full grid gap-4 grid-cols-3"> | |||
{files.map((f, i) => ( | |||
<div | |||
data-testid="selectedFileItem" | |||
key={i} | |||
className={`w-full aspect-square rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10`} | |||
title={[f.name, getMimeTypeDescription(f.type), formatFileSize(f.size)].join(', ')} | |||
> | |||
{ | |||
f.resolvedType === ContentType.IMAGE | |||
&& typeof f.metadata?.previewUrl === 'string' | |||
&& ( | |||
<img | |||
className="block w-full h-full object-center object-cover" | |||
src={f.metadata.previewUrl} | |||
alt={f.name} | |||
data-testid="preview" | |||
/> | |||
) | |||
} | |||
{ | |||
f.resolvedType === ContentType.AUDIO | |||
&& ( | |||
<AudioMiniFilePreview file={f} /> | |||
) | |||
} | |||
</div> | |||
))} | |||
{Array.from(files).map((file: File, i) => { | |||
const f = file as unknown as FileWithResolvedContentType; | |||
return ( | |||
<div | |||
data-testid="selectedFileItem" | |||
key={i} | |||
className={`w-full aspect-square rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10`} | |||
title={[f.name, getMimeTypeDescription(f.type), formatFileSize(f.size)].join(', ')} | |||
> | |||
{ | |||
f.resolvedType === ContentType.IMAGE | |||
&& typeof f?.url === 'string' | |||
&& ( | |||
<img | |||
className="block w-full h-full object-center object-cover" | |||
src={f.url} | |||
alt={f.name} | |||
data-testid="preview" | |||
/> | |||
) | |||
} | |||
{ | |||
f.resolvedType === ContentType.AUDIO | |||
&& ( | |||
<AudioMiniFilePreview file={f} /> | |||
) | |||
} | |||
</div> | |||
); | |||
})} | |||
</div> | |||
</div> | |||
) | |||
@@ -105,7 +83,7 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
hint = '', | |||
border = false, | |||
block = false, | |||
enhanced = false, | |||
enhanced: enhancedProp = false, | |||
hiddenLabel = false, | |||
multiple = false, | |||
onChange, | |||
@@ -116,34 +94,39 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
}: FileButtonProps, | |||
forwardedRef, | |||
) => { | |||
const [renderEnhanced, setRenderEnhanced] = React.useState(false); | |||
const { enhanced } = useEnhanced({ enhanced: enhancedProp }); | |||
const [fileList, setFileList] = React.useState<FileList>(); | |||
const [lastSelectedFileAt, setLastSelectedFileAt] = React.useState<number>(); | |||
const [lastUpdated, setLastUpdated] = React.useState<number>(); | |||
const defaultRef = React.useRef<HTMLInputElement>(null); | |||
const ref = forwardedRef ?? defaultRef; | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
const addFile: React.ChangeEventHandler<HTMLInputElement> = (e) => { | |||
if (!enhanced) { | |||
onChange?.(e); | |||
return; | |||
const doSetFileList: React.ChangeEventHandler<HTMLInputElement> = (e) => { | |||
if (enhancedProp) { | |||
setFileList(e.currentTarget.files as FileList); | |||
setLastUpdated(Date.now()); | |||
} | |||
setFileList(e.currentTarget.files as FileList); | |||
setLastSelectedFileAt(Date.now()); | |||
onChange?.(e); | |||
}; | |||
const deleteFiles: React.MouseEventHandler<HTMLButtonElement> = () => { | |||
if (typeof ref === 'object' && ref.current) { | |||
ref.current.value = ''; | |||
setFileList(undefined); | |||
setTimeout(() => { | |||
delegateTriggerChangeEvent(ref.current as HTMLInputElement); | |||
}) | |||
const doClearFileList: React.MouseEventHandler<HTMLButtonElement> = (e) => { | |||
e.preventDefault(); | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current } = ref; | |||
if (!current) { | |||
return; | |||
} | |||
current.value = ''; | |||
setFileList(undefined); | |||
setLastUpdated(Date.now()); | |||
setTimeout(() => { | |||
delegateTriggerChangeEvent(current); | |||
}); | |||
}; | |||
const cancelEvent = (e: React.DragEvent) => { | |||
@@ -153,20 +136,26 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
const handleDropZone: React.DragEventHandler<HTMLDivElement> = async (e) => { | |||
cancelEvent(e); | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current } = ref; | |||
if (!current) { | |||
return; | |||
} | |||
const { dataTransfer } = e; | |||
if (typeof ref === 'object' && ref.current) { | |||
const { files } = dataTransfer; | |||
if (!(files && files.length > 0)) { | |||
return; | |||
} | |||
setFileList(ref.current.files = files); | |||
delegateTriggerChangeEvent(ref.current); | |||
const { files } = dataTransfer; | |||
if (!(files && files.length > 0)) { | |||
return; | |||
} | |||
setFileList(current.files = files); | |||
setLastUpdated(Date.now()); | |||
setTimeout(() => { | |||
delegateTriggerChangeEvent(current); | |||
}); | |||
} | |||
React.useEffect(() => { | |||
setRenderEnhanced(enhanced); | |||
}, [enhanced]); | |||
const filesCount = React.useMemo(() => fileList?.length ?? 0, [fileList]); | |||
return ( | |||
<div | |||
@@ -195,10 +184,10 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
className={clsx( | |||
'peer', | |||
{ | |||
'sr-only': renderEnhanced, | |||
'sr-only': enhanced, | |||
} | |||
)} | |||
onChange={addFile} | |||
onChange={doSetFileList} | |||
multiple={multiple} | |||
data-testid="input" | |||
aria-labelledby={label ? `${labelId}` : undefined} | |||
@@ -222,8 +211,8 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
) | |||
} | |||
{ | |||
(fileList?.length ?? 0) < 1 | |||
&& renderEnhanced | |||
filesCount < 1 | |||
&& enhanced | |||
&& hint | |||
&& ( | |||
<div | |||
@@ -237,14 +226,13 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
) | |||
} | |||
{ | |||
(fileList?.length ?? 0) > 0 | |||
&& renderEnhanced | |||
filesCount > 0 | |||
&& enhanced | |||
&& ( | |||
<> | |||
<div className={`absolute top-0 left-0 w-full h-full pointer-events-none pb-12 box-border overflow-hidden pt-8`}> | |||
<React.Fragment key={lastUpdated}> | |||
<div className={`sm:absolute top-0 left-0 w-full h-full pointer-events-none pb-12 box-border overflow-hidden pt-8`}> | |||
<div | |||
className={`pointer-events-auto w-full h-full px-4 pb-4 box-border`} | |||
key={lastSelectedFileAt} | |||
> | |||
{ | |||
multiple | |||
@@ -259,11 +247,10 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
className={`w-full h-full`} | |||
> | |||
<div data-testid="selectedFileItem" className={`h-full w-full p-4 box-border rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10`}> | |||
<div className="relative"> | |||
<FilePreviewComponent | |||
fileList={fileList} | |||
/> | |||
</div> | |||
<FilePreview | |||
className="w-full h-full relative" | |||
fileList={fileList} | |||
/> | |||
</div> | |||
</div> | |||
) | |||
@@ -288,18 +275,18 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
<button | |||
data-testid="clear" | |||
type="button" | |||
onClick={deleteFiles} | |||
onClick={doClearFileList} | |||
className="flex w-full h-full 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" | |||
> | |||
<span | |||
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" | |||
> | |||
{multiple ? 'Clear' : 'Delete'} | |||
Clear | |||
</span> | |||
</button> | |||
</div> | |||
</div> | |||
</> | |||
</React.Fragment> | |||
) | |||
} | |||
{ | |||
@@ -4,16 +4,114 @@ import {formatFileSize, formatNumeral} from '@/utils/numeral'; | |||
import clsx from 'clsx'; | |||
import {useFileMetadata, useFileUrl, useImageControls} from '@/categories/blob/react'; | |||
import {KeyValueTable} from '@/categories/information/react'; | |||
import {useEnhanced} from '@modal-soft/react-utils'; | |||
type ImageFilePreviewDerivedComponent = HTMLImageElement; | |||
type RgbTuple = [number, number, number]; | |||
export interface ImageFilePreviewProps<F extends Partial<File> = Partial<File>> extends Omit<React.HTMLProps<ImageFilePreviewDerivedComponent>, 'src' | 'alt'> { | |||
type SwatchDerivedElement = HTMLInputElement; | |||
export interface SwatchProps extends Omit<React.HTMLProps<SwatchDerivedElement>, 'color'> { | |||
color: RgbTuple; | |||
mode?: 'rgb' | 'hsl'; | |||
} | |||
export const useSwatchControls = () => { | |||
const id = React.useId(); | |||
const copyColor: React.ReactEventHandler<SwatchDerivedElement> = React.useCallback(async (e) => { | |||
const { value } = e.currentTarget; | |||
await window.navigator.clipboard.writeText(value); | |||
}, []); | |||
return React.useMemo(() => ({ | |||
id, | |||
copyColor, | |||
}), [id, copyColor]); | |||
}; | |||
export const Swatch = React.forwardRef<SwatchDerivedElement, SwatchProps>(({ | |||
color, | |||
mode = 'rgb', | |||
className, | |||
style, | |||
...etcProps | |||
}, forwardedRef) => { | |||
const { id, copyColor } = useSwatchControls(); | |||
const colorValue = `${mode}(${color.join(', ')})`; | |||
return ( | |||
<span | |||
className={clsx( | |||
'inline-block align-middle', | |||
className, | |||
)} | |||
style={style} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={forwardedRef} | |||
type="text" | |||
value={colorValue} | |||
className="sr-only select-all peer" | |||
readOnly | |||
id={id} | |||
onSelect={copyColor} | |||
/> | |||
<label | |||
className={clsx( | |||
'relative rounded ring-secondary/50 whitespace-nowrap inline-block align-top leading-none cursor-pointer', // todo eyedropper cursor | |||
'peer-focus:outline-0 peer-focus:ring-4', | |||
'peer-active:ring-tertiary/50', | |||
'peer-disabled:opacity-50 peer-disabled:cursor-not-allowed', | |||
)} | |||
title={colorValue} | |||
htmlFor={id} | |||
> | |||
<span | |||
className="inline-block w-5 h-5 align-middle border border-[#ffffff]" | |||
> | |||
<span | |||
className="block w-full h-full border border-[#000000]" | |||
style={{ | |||
backgroundColor: `${mode}(${color.join(' ')})`, | |||
}} | |||
/> | |||
</span> | |||
<span className="tabular-nums text-xs sr-only"> | |||
{ | |||
color | |||
.map((c) => c | |||
.toString() | |||
.padStart(4, ' ') | |||
.split('') | |||
.map((cc, j) => ( | |||
<span | |||
key={`${cc}:${j}`} | |||
className={clsx({ | |||
'opacity-0': cc === ' ', | |||
})} | |||
> | |||
{j === 0 && ' '} | |||
{cc === ' ' && j > 0 ? '0' : cc} | |||
</span> | |||
)) | |||
) | |||
} | |||
</span> | |||
</label> | |||
</span> | |||
); | |||
}); | |||
Swatch.displayName = 'Swatch'; | |||
type ImageFilePreviewDerivedElement = HTMLImageElement; | |||
export interface ImageFilePreviewProps<F extends Partial<File> = Partial<File>> extends Omit<React.HTMLProps<ImageFilePreviewDerivedElement>, 'src' | 'alt'> { | |||
file?: F; | |||
disabled?: boolean; | |||
enhanced?: boolean; | |||
} | |||
export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponent, ImageFilePreviewProps>(({ | |||
export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedElement, ImageFilePreviewProps>(({ | |||
file, | |||
className, | |||
style, | |||
@@ -26,6 +124,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen | |||
file: fileWithUrl as File, | |||
augmentFunction: augmentImageFile, | |||
}); | |||
const { | |||
fullScreen, | |||
handleAction, | |||
@@ -35,10 +134,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen | |||
forwardedRef, | |||
}); | |||
const [enhanced, setEnhanced] = React.useState(false); | |||
React.useEffect(() => { | |||
setEnhanced(enhancedProp); | |||
}, [enhancedProp]); | |||
const { enhanced } = useEnhanced({ enhanced: enhancedProp }); | |||
if (!(fileWithMetadata)) { | |||
return null; | |||
@@ -66,7 +162,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen | |||
className={clsx( | |||
'block h-full max-w-full object-center bg-[#000000]', | |||
{ | |||
'object-contain fixed w-full top-0 left-0': fullScreen, | |||
'object-contain fixed w-full top-0 left-0 z-[3]': fullScreen, | |||
'object-cover w-full': !fullScreen, | |||
}, | |||
)} | |||
@@ -94,6 +190,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen | |||
className: 'font-bold', | |||
valueProps: { | |||
ref: filenameRef, | |||
title: fileWithMetadata.name, | |||
children: fileWithMetadata.name, | |||
}, | |||
}, | |||
@@ -128,47 +225,22 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen | |||
&& { | |||
key: 'Palette', | |||
valueProps: { | |||
className: 'mt-1', | |||
children: fileWithMetadata.metadata?.palette.map((rgb, i) => ( | |||
<React.Fragment | |||
key={rgb.join(' ')} | |||
> | |||
{i > 0 && ' '} | |||
<span | |||
className="whitespace-nowrap inline-block align-top leading-none" | |||
title={`rgb(${rgb.join(', ')})`} | |||
> | |||
<span | |||
className="inline-block w-5 h-5 align-middle border border-[#ffffff] ring-1 ring-[#000000]" | |||
style={{ | |||
backgroundColor: `rgb(${rgb.join(' ')})`, | |||
}} | |||
className: '-ml-4 pl-4 pt-4 ', | |||
children: ( | |||
<div className="flex flex-wrap gap-3"> | |||
{fileWithMetadata.metadata?.palette.map((rgb, i) => ( | |||
<React.Fragment | |||
key={rgb.join(' ')} | |||
> | |||
{i > 0 && ' '} | |||
<Swatch | |||
color={rgb} | |||
mode="rgb" | |||
/> | |||
<span className="tabular-nums text-xs sr-only"> | |||
{ | |||
rgb | |||
.map((c) => c | |||
.toString() | |||
.padStart(4, ' ') | |||
.split('') | |||
.map((cc, j) => ( | |||
<span | |||
key={`${rgb.join(',')}:${c}:${j}`} | |||
className={clsx({ | |||
'opacity-0': cc === ' ', | |||
})} | |||
> | |||
{j === 0 && ' '} | |||
{cc === ' ' && j > 0 ? '0' : cc} | |||
</span> | |||
)) | |||
) | |||
.flat() | |||
} | |||
</span> | |||
</span> | |||
</React.Fragment> | |||
)), | |||
</React.Fragment> | |||
))} | |||
</div> | |||
), | |||
}, | |||
}, | |||
]} | |||
@@ -190,7 +262,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen | |||
name="action" | |||
value="toggleFullScreen" | |||
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', | |||
'h-12 flex 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', | |||
{ | |||
@@ -210,7 +282,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen | |||
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', | |||
'h-12 flex 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', | |||
)} | |||
@@ -4,7 +4,8 @@ import {useFileMetadata, useFileUrl} from '@/categories/blob/react'; | |||
import {augmentTextFile, getMimeTypeDescription} from '@/utils/blob'; | |||
import clsx from 'clsx'; | |||
import {KeyValueTable} from '@/categories/information/react'; | |||
import {Refractor} from '../../../../../packages/react-refractor'; | |||
import {Refractor} from '@modal-soft/react-refractor'; | |||
import {useEnhanced} from '@modal-soft/react-utils'; | |||
type TextFilePreviewDerivedComponent = HTMLDivElement; | |||
@@ -25,11 +26,7 @@ export const TextFilePreview = React.forwardRef<TextFilePreviewDerivedComponent, | |||
file: fileWithUrl, | |||
augmentFunction: augmentTextFile, | |||
}); | |||
const [enhanced, setEnhanced] = React.useState(false); | |||
React.useEffect(() => { | |||
setEnhanced(enhancedProp); | |||
}, [enhancedProp]); | |||
const { enhanced } = useEnhanced({ enhanced: enhancedProp }); | |||
if (!fileWithMetadata) { | |||
return null; | |||
@@ -54,12 +51,12 @@ export const TextFilePreview = React.forwardRef<TextFilePreviewDerivedComponent, | |||
role="presentation" | |||
className="w-full h-full select-none overflow-hidden text-xs" | |||
ref={forwardedRef} | |||
key={`${fileWithMetadata.url}:${fileWithMetadata.type}`} | |||
> | |||
<Refractor | |||
code={fileWithMetadata.metadata.contents} | |||
language={fileWithMetadata.metadata.scheme} | |||
lineNumbers={Boolean(fileWithMetadata.metadata.scheme)} | |||
maxLineNumber={20} | |||
/> | |||
</div> | |||
) | |||
@@ -5,6 +5,7 @@ import {useFileMetadata, useFileUrl, useMediaControls} from '@tesseract-design/w | |||
import clsx from 'clsx'; | |||
import {Slider} from '@tesseract-design/web-number-react'; | |||
import {KeyValueTable} from '@/categories/information/react'; | |||
import {useEnhanced} from '@modal-soft/react-utils'; | |||
type VideoFilePreviewDerivedComponent = HTMLVideoElement; | |||
@@ -46,15 +47,12 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen | |||
mediaControllerRef, | |||
handleAction, | |||
filenameRef, | |||
formId, | |||
} = useMediaControls<HTMLVideoElement>({ | |||
controllerRef: forwardedRef, | |||
}); | |||
const formId = React.useId(); | |||
const [enhanced, setEnhanced] = React.useState(false); | |||
React.useEffect(() => { | |||
setEnhanced(enhancedProp); | |||
}, [enhancedProp]); | |||
const { enhanced } = useEnhanced({ enhanced: enhancedProp }); | |||
if (!fileWithMetadata) { | |||
return null; | |||
@@ -78,10 +76,10 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen | |||
<div | |||
className="w-full h-full bg-black flex flex-col items-stretch" | |||
data-testid="preview" | |||
key={`${fileWithMetadata.url}:${fileWithMetadata.type}`} | |||
> | |||
<div | |||
className="w-full flex-auto relative" | |||
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`} | |||
> | |||
<video | |||
{...etcProps} | |||
@@ -95,10 +93,10 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen | |||
controls={!enhanced} | |||
> | |||
<source | |||
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`} | |||
src={fileWithMetadata.url} | |||
type={fileWithMetadata.type} | |||
/> | |||
Video playback not supported. | |||
</video> | |||
<button | |||
className="absolute w-full h-full top-0 left-0" | |||
@@ -114,7 +112,7 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen | |||
</button> | |||
</div> | |||
{enhanced && ( | |||
<div className="w-full flex-shrink-0 h-10 flex gap-4 items-center"> | |||
<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" | |||
> | |||
@@ -195,8 +193,10 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen | |||
} | |||
> | |||
<Slider | |||
className="flex-auto bg-negative text-base" | |||
className="flex-auto text-base" | |||
ref={seekRef} | |||
min={0} | |||
max={durationDisplay} | |||
onMouseDown={startSeek} | |||
onMouseUp={endSeek} | |||
onChange={setSeek} | |||
@@ -242,6 +242,7 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen | |||
className: 'font-bold', | |||
valueProps: { | |||
ref: filenameRef, | |||
title: fileWithMetadata.name, | |||
children: fileWithMetadata.name, | |||
}, | |||
}, | |||
@@ -294,7 +295,7 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen | |||
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', | |||
'h-12 flex 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', | |||
)} | |||
@@ -12,11 +12,14 @@ export const useFileUrl = (options: UseFileUrlOptions) => { | |||
React.useEffect(() => { | |||
if (!file) { | |||
setFileWithUrl(undefined); | |||
if (fileWithUrl) { | |||
setFileWithUrl(undefined); | |||
} | |||
setLoading(false); | |||
return; | |||
} | |||
setFileWithUrl(undefined); | |||
setLoading(true); | |||
addDataUrl(file) | |||
.then((fileWithUrl) => { | |||
@@ -24,7 +27,6 @@ export const useFileUrl = (options: UseFileUrlOptions) => { | |||
setLoading(false); | |||
}) | |||
.catch(() => { | |||
setFileWithUrl(file); | |||
setLoading(false); | |||
}); | |||
}, [file]); | |||
@@ -40,7 +42,7 @@ export interface UseFileMetadataOptions<T extends Partial<File> = Partial<File>, | |||
augmentFunction: (file: U) => Promise<T>; | |||
} | |||
export const useFileMetadata = <T extends Partial<File>>(options: UseFileMetadataOptions<T>) => { | |||
export const useFileMetadata = <T extends Partial<FileWithDataUrl>>(options: UseFileMetadataOptions<T>) => { | |||
const { file, augmentFunction } = options; | |||
const [fileWithMetadata, setFileWithMetadata] = React.useState<T | undefined>(file as T | undefined); | |||
const [loading, setLoading] = React.useState(false); | |||
@@ -48,11 +50,14 @@ export const useFileMetadata = <T extends Partial<File>>(options: UseFileMetadat | |||
React.useEffect(() => { | |||
if (!file) { | |||
setFileWithMetadata(undefined); | |||
if (fileWithMetadata) { | |||
setFileWithMetadata(undefined); | |||
} | |||
setLoading(false); | |||
return; | |||
} | |||
setFileWithMetadata(undefined); | |||
setLoading(true); | |||
setError(undefined); | |||
augmentFunction(file) | |||
@@ -61,6 +66,7 @@ export const useFileMetadata = <T extends Partial<File>>(options: UseFileMetadat | |||
setLoading(false); | |||
}) | |||
.catch((error) => { | |||
setFileWithMetadata(file as T); | |||
setError(error); | |||
setLoading(false); | |||
}); | |||
@@ -13,6 +13,16 @@ export const useImageControls = (options = {} as UseImageControlsOptions) => { | |||
const imageRef = forwardedRef ?? defaultRef; | |||
const filenameRef = React.useRef<HTMLElement>(null); | |||
React.useEffect(() => { | |||
if (fullScreen) { | |||
window.document.body.style.overflow = 'hidden'; | |||
} | |||
if (!fullScreen) { | |||
window.document.body.style.overflow = ''; | |||
} | |||
}, [fullScreen]); | |||
const toggleFullScreen = React.useCallback(() => { | |||
setFullScreen((b) => !b); | |||
}, []); | |||
@@ -10,7 +10,6 @@ export interface UseMediaControlsOptions<T extends HTMLMediaElement> { | |||
export const useMediaControls = <T extends HTMLMediaElement>({ | |||
controllerRef: forwardedRef, | |||
actionFormKey = 'action' as const, | |||
visualizationMode: initialVisualizationMode, | |||
}: UseMediaControlsOptions<T>) => { | |||
const defaultRef = React.useRef<T>(null); | |||
const ref = forwardedRef ?? defaultRef; | |||
@@ -18,6 +17,7 @@ export const useMediaControls = <T extends HTMLMediaElement>({ | |||
const volumeRef = React.useRef<HTMLInputElement>(null); | |||
const filenameRef = React.useRef<HTMLElement>(null); | |||
const visualizationId = React.useId(); | |||
const formId = React.useId(); | |||
const [isPlaying, setIsPlaying] = React.useState(false); | |||
const [isSeeking, setIsSeeking] = React.useState(false); | |||
const [currentTimeDisplay, setCurrentTimeDisplay] = React.useState<number>(); | |||
@@ -45,10 +45,9 @@ export const useMediaControls = <T extends HTMLMediaElement>({ | |||
}, []); | |||
const setSeek: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => { | |||
if (!(typeof ref === 'object' && ref !== null)) { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current: mediaController } = ref; | |||
if (!mediaController) { | |||
return; | |||
@@ -65,12 +64,10 @@ export const useMediaControls = <T extends HTMLMediaElement>({ | |||
}, [ref, isSeeking, doSetSeek]); | |||
const endSeek: React.MouseEventHandler<HTMLInputElement> = React.useCallback((e) => { | |||
if (!(typeof ref === 'object' && ref !== null)) { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current: mediaController } = ref; | |||
if (!mediaController) { | |||
return; | |||
} | |||
@@ -96,7 +93,7 @@ export const useMediaControls = <T extends HTMLMediaElement>({ | |||
const currentTime = videoElement.currentTime; | |||
setCurrentTimeDisplay(currentTime); | |||
if (!seekRef.current) { | |||
if (!(typeof seekRef === 'object' && seekRef)) { | |||
return; | |||
} | |||
const { current: seek } = seekRef; | |||
@@ -104,24 +101,32 @@ export const useMediaControls = <T extends HTMLMediaElement>({ | |||
return; | |||
} | |||
seek.value = String(currentTime); | |||
}, [isSeeking]); | |||
}, [isSeeking, seekRef]); | |||
const toggleSeekTimeCountMode = React.useCallback(() => { | |||
setIsSeekTimeCountingDown((b) => !b); | |||
}, []); | |||
const download = React.useCallback(() => { | |||
if (!(typeof ref === 'object' && ref?.current !== null)) { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current: mediaController } = ref; | |||
if (!mediaController) { | |||
return; | |||
} | |||
if (!(typeof filenameRef === 'object' && filenameRef?.current !== null)) { | |||
if (!(typeof filenameRef === 'object' && filenameRef)) { | |||
return; | |||
} | |||
const { current: filename } = filenameRef; | |||
if (!filename) { | |||
return; | |||
} | |||
const downloadLink = window.document.createElement('a'); | |||
downloadLink.download = filenameRef.current.textContent ?? 'image'; | |||
downloadLink.href = ref.current.currentSrc; | |||
downloadLink.download = filename.textContent ?? 'media'; | |||
downloadLink.href = mediaController.currentSrc; | |||
downloadLink.addEventListener('click', () => { | |||
downloadLink.remove(); | |||
}); | |||
@@ -136,7 +141,6 @@ export const useMediaControls = <T extends HTMLMediaElement>({ | |||
}), [togglePlayback, toggleSeekTimeCountMode, download]); | |||
const handleAction: React.FormEventHandler<HTMLFormElement> = React.useCallback((e) => { | |||
e.preventDefault(); | |||
e.preventDefault(); | |||
const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement }; | |||
const formData = getFormValues( | |||
@@ -151,77 +155,66 @@ export const useMediaControls = <T extends HTMLMediaElement>({ | |||
}, [actions, actionFormKey]); | |||
const adjustVolume: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => { | |||
if (!(typeof ref === 'object' && ref !== null)) { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
if (!ref.current) { | |||
const { current: mediaController } = ref; | |||
if (!mediaController) { | |||
return; | |||
} | |||
const { value } = e.currentTarget; | |||
ref.current.volume = Number(value); | |||
mediaController.volume = Number(value); | |||
}, [ref]); | |||
React.useEffect(() => { | |||
if (!(typeof ref === 'object' && ref !== null)) { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
if (!ref.current) { | |||
const { current: mediaController } = ref; | |||
if (!mediaController) { | |||
return; | |||
} | |||
if (isPlaying) { | |||
void ref.current.play(); | |||
void mediaController.play(); | |||
return | |||
} | |||
ref.current.pause(); | |||
mediaController.pause(); | |||
}, [isPlaying, ref]); | |||
React.useEffect(() => { | |||
if (!seekRef.current) { | |||
if (!(typeof seekRef === 'object' && seekRef)) { | |||
return; | |||
} | |||
const { current: seek } = seekRef; | |||
if (!seek) { | |||
return; | |||
} | |||
seek.value = String(currentTimeDisplay); | |||
}, [currentTimeDisplay]); | |||
}, [currentTimeDisplay, seekRef]); | |||
React.useEffect(() => { | |||
if (!seekRef.current) { | |||
return; | |||
} | |||
const { current: seek } = seekRef; | |||
if (!seek) { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
seek.max = String(durationDisplay); | |||
}, [durationDisplay]); | |||
React.useEffect(() => { | |||
if (!volumeRef.current) { | |||
const { current: mediaController } = ref; | |||
if (!mediaController) { | |||
return; | |||
} | |||
if (!(typeof ref === 'object' && ref !== null)) { | |||
if (!(typeof volumeRef === 'object' && volumeRef)) { | |||
return; | |||
} | |||
if (!ref.current) { | |||
const { current: volume } = volumeRef; | |||
if (!volume) { | |||
return; | |||
} | |||
const { current: mediaController } = ref; | |||
const { current: volume } = volumeRef; | |||
volume.value = String(mediaController.volume); | |||
}, [ref]); | |||
}, [ref, volumeRef]); | |||
return React.useMemo(() => ({ | |||
seekRef, | |||
@@ -243,6 +236,7 @@ export const useMediaControls = <T extends HTMLMediaElement>({ | |||
handleAction, | |||
filenameRef, | |||
visualizationId, | |||
formId, | |||
}), [ | |||
refreshControls, | |||
isPlaying, | |||
@@ -261,5 +255,6 @@ export const useMediaControls = <T extends HTMLMediaElement>({ | |||
handleAction, | |||
filenameRef, | |||
visualizationId, | |||
formId, | |||
]); | |||
}; |
@@ -1,7 +1,7 @@ | |||
export * from './components/AudioFilePreview'; | |||
//export * from './components/AudioMiniFilePreview'; | |||
export * from './components/AudioMiniFilePreview'; | |||
export * from './components/BinaryFilePreview'; | |||
//export * from './components/FileSelectBox'; | |||
export * from './components/FileSelectBox'; | |||
export * from './components/ImageFilePreview'; | |||
export * from './components/TextFilePreview'; | |||
export * from './components/VideoFilePreview'; | |||
@@ -41,8 +41,6 @@ export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInp | |||
/** | |||
* Component for inputting textual values. | |||
* | |||
* This component supports multiline input and adjusts its layout accordingly. | |||
*/ | |||
export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, MaskedTextInputProps>( | |||
( | |||
@@ -1,10 +1,11 @@ | |||
import * as React from 'react'; | |||
import * as TextControlBase from '@tesseract-design/web-base-textcontrol'; | |||
import clsx from 'clsx'; | |||
import styles from './style.module.css'; | |||
type MaskedTextInputDerivedElement = HTMLInputElement; | |||
type SpinnerDerivedElement = HTMLInputElement; | |||
export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInputDerivedElement>, 'size' | 'type' | 'label'> { | |||
export interface SpinnerProps extends Omit<React.HTMLProps<SpinnerDerivedElement>, 'size' | 'type' | 'label'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
*/ | |||
@@ -40,11 +41,9 @@ export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInp | |||
} | |||
/** | |||
* Component for inputting textual values. | |||
* | |||
* This component supports multiline input and adjusts its layout accordingly. | |||
* Component for inputting numeric values. | |||
*/ | |||
export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, MaskedTextInputProps>( | |||
export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>( | |||
( | |||
{ | |||
label, | |||
@@ -57,7 +56,7 @@ export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, M | |||
hiddenLabel = false, | |||
className, | |||
...etcProps | |||
}: MaskedTextInputProps, | |||
}: SpinnerProps, | |||
ref, | |||
) => { | |||
const labelId = React.useId(); | |||
@@ -78,12 +77,13 @@ export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, M | |||
{...etcProps} | |||
ref={ref} | |||
aria-labelledby={labelId} | |||
type="password" | |||
type="number" | |||
data-testid="input" | |||
className={clsx( | |||
'bg-negative rounded-inherit w-full peer block', | |||
'bg-negative rounded-inherit w-full peer block tabular-nums', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
styles['spinner'], | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
@@ -195,4 +195,4 @@ export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, M | |||
}, | |||
); | |||
MaskedTextInput.displayName = 'MaskedTextInput'; | |||
Spinner.displayName = 'Spinner'; |
@@ -0,0 +1,12 @@ | |||
.spinner { | |||
position: relative; | |||
} | |||
.spinner::-webkit-inner-spin-button { | |||
position: absolute; | |||
top: 0; | |||
right: 0; | |||
height: 100%; | |||
width: 1.5rem; | |||
z-index: 2; | |||
} |
@@ -1 +1,2 @@ | |||
export * from './components/Slider'; | |||
export * from './components/Spinner'; |
@@ -44,7 +44,6 @@ export const Refractor = React.forwardRef<PrismDerivedElement, PrismProps>(({ | |||
{language} | |||
</div> | |||
{actions} | |||
</div> | |||
)} | |||
<div | |||
@@ -0,0 +1,17 @@ | |||
import * as React from 'react'; | |||
export interface UseEnhancedOptions { | |||
enhanced: boolean; | |||
} | |||
export const useEnhanced = (options: UseEnhancedOptions) => { | |||
const { enhanced: enhancedProp } = options; | |||
const [enhanced, setEnhanced] = React.useState(false); | |||
React.useEffect(() => { | |||
setEnhanced(enhancedProp); | |||
}, [enhancedProp]); | |||
return React.useMemo(() => ({ | |||
enhanced, | |||
}), [enhanced]); | |||
}; |
@@ -3,15 +3,16 @@ import {WaveSurferOptions} from 'wavesurfer.js'; | |||
import clsx from 'clsx'; | |||
import {getFormValues} from '@theoryofnekomata/formxtra'; | |||
type SpectrogramCanvasDerivedComponent = HTMLAudioElement; | |||
type SpectrogramCanvasDerivedElement = HTMLDivElement; | |||
export interface SpectrogramCanvasProps | |||
extends React.HTMLProps<SpectrogramCanvasDerivedComponent>, | |||
extends React.HTMLProps<SpectrogramCanvasDerivedElement>, | |||
Omit<WaveSurferOptions, 'waveColor' | 'plugins' | 'height' | 'media' | 'container' | 'fillParent' | 'url' | 'autoplay' | 'renderFunction'> { | |||
waveColor?: string; | |||
audioRef?: React.Ref<HTMLAudioElement>; | |||
} | |||
export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent, SpectrogramCanvasProps>(({ | |||
export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedElement, SpectrogramCanvasProps>(({ | |||
className, | |||
children, | |||
controls, | |||
@@ -36,17 +37,16 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon | |||
sampleRate, | |||
splitChannels, | |||
normalize, | |||
audioRef, | |||
...etcProps | |||
}, forwardedRef) => { | |||
const [isPlaying, setIsPlaying] = React.useState(false); | |||
const defaultRef = React.useRef<HTMLAudioElement>(null); | |||
const ref = forwardedRef ?? defaultRef; | |||
const containerRef = React.useRef<HTMLDivElement>(null); | |||
const defaultRef = React.useRef<SpectrogramCanvasDerivedElement>(null); | |||
const containerRef = forwardedRef ?? defaultRef; | |||
const waveSurferRef = React.useRef<any>(null); | |||
const cursorRef = React.useRef<HTMLDivElement>(null); | |||
const handleAction: React.FormEventHandler<HTMLFormElement> = (e) => { | |||
e.preventDefault(); | |||
e.preventDefault(); | |||
const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement }; | |||
const formData = getFormValues( | |||
@@ -66,24 +66,36 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon | |||
}; | |||
React.useEffect(() => { | |||
if (!(typeof audioRef === 'object' && audioRef)) { | |||
return; | |||
} | |||
const { current: media } = audioRef; | |||
if (!media) { | |||
return; | |||
} | |||
if (!(typeof containerRef === 'object' && containerRef)) { | |||
return; | |||
} | |||
const { current: container } = containerRef; | |||
const media = typeof ref === 'object' ? ref?.current : null; | |||
const { current: waveSurferCurrent } = waveSurferRef; | |||
if (!container) { | |||
return; | |||
} | |||
if (!(typeof cursorRef === 'object' && cursorRef)) { | |||
return; | |||
} | |||
const { current: cursor } = cursorRef; | |||
if (!cursor) { | |||
return; | |||
} | |||
const handleTimeUpdate = (e: Event) => { | |||
const thisMedia = e.currentTarget as HTMLAudioElement; | |||
if (cursorRef.current) { | |||
cursorRef.current.style.width = `${(thisMedia?.currentTime ?? 0) / (thisMedia?.duration ?? 1) * 100}%`; | |||
} | |||
cursor.style.width = `${(thisMedia?.currentTime ?? 0) / (thisMedia?.duration ?? 1) * 100}%`; | |||
}; | |||
const load = async (ref: React.Ref<HTMLAudioElement>) => { | |||
if (!(typeof ref === 'object' && ref?.current)) { | |||
return; | |||
} | |||
if (!(typeof containerRef === 'object' && containerRef?.current)) { | |||
return; | |||
} | |||
const load = async (media: HTMLAudioElement, container: HTMLElement) => { | |||
const { default: WaveSurfer } = await import('wavesurfer.js'); | |||
const { default: Spectrogram, } = await import('wavesurfer.js/dist/plugins/spectrogram'); | |||
const dummyContainer = window.document.createElement('div'); | |||
@@ -115,7 +127,7 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon | |||
normalize, | |||
plugins: [], | |||
cursorWidth, | |||
media: media ?? undefined, | |||
media, | |||
}); | |||
let colorMap: Array<[number, number, number, number]> = []; | |||
@@ -130,10 +142,10 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon | |||
} | |||
waveSurferInstance.registerPlugin( | |||
Spectrogram.create({ | |||
container: containerRef.current, | |||
container, | |||
labels: true, | |||
labelsColor: 'rgb(0 0 0/0)', | |||
height: containerRef.current.clientHeight, | |||
height: container.clientHeight, | |||
colorMap, | |||
}), | |||
) | |||
@@ -147,12 +159,17 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon | |||
} | |||
dummyContainer.remove(); | |||
}); | |||
await waveSurferInstance.load(ref.current.currentSrc); | |||
waveSurferInstance.setTime(ref.current.currentTime); | |||
waveSurferRef.current = waveSurferInstance; | |||
media!.addEventListener('timeupdate', handleTimeUpdate); | |||
await waveSurferInstance.load(media.currentSrc); | |||
waveSurferInstance.setTime(media.currentTime); | |||
media.addEventListener('timeupdate', handleTimeUpdate); | |||
return waveSurferInstance; | |||
}; | |||
void load(ref); | |||
const { current: waveSurferCurrent } = waveSurferRef; | |||
void load(media, container).then((i) => { | |||
waveSurferRef.current = i; | |||
}); | |||
return () => { | |||
if (waveSurferCurrent) { | |||
(waveSurferCurrent as unknown as Record<string, Function>).destroy(); | |||
@@ -160,13 +177,12 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon | |||
if (container) { | |||
container.innerHTML = ''; | |||
} | |||
if (!media) { | |||
return; | |||
if (media) { | |||
media.removeEventListener('timeupdate', handleTimeUpdate); | |||
} | |||
media.removeEventListener('timeupdate', handleTimeUpdate); | |||
}; | |||
}, [ | |||
ref, | |||
audioRef, | |||
autoPlay, | |||
waveColor, | |||
progressColor, | |||
@@ -188,6 +204,7 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon | |||
splitChannels, | |||
normalize, | |||
cursorWidth, | |||
containerRef, | |||
]); | |||
return ( | |||
@@ -3,13 +3,15 @@ import {WaveSurferOptions} from 'wavesurfer.js'; | |||
import clsx from 'clsx'; | |||
import {getFormValues} from '@theoryofnekomata/formxtra'; | |||
type SpectrogramCanvasDerivedComponent = HTMLAudioElement; | |||
type WaveformCanvasDerivedElement = HTMLDivElement; | |||
export interface WaveformCanvasProps | |||
extends React.HTMLProps<SpectrogramCanvasDerivedComponent>, | |||
Omit<WaveSurferOptions, 'plugins' | 'height' | 'media' | 'container' | 'fillParent' | 'url' | 'autoplay' | 'renderFunction'> {} | |||
extends React.HTMLProps<WaveformCanvasDerivedElement>, | |||
Omit<WaveSurferOptions, 'plugins' | 'height' | 'media' | 'container' | 'fillParent' | 'url' | 'autoplay' | 'renderFunction'> { | |||
audioRef?: React.Ref<HTMLAudioElement>; | |||
} | |||
export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent, WaveformCanvasProps>(({ | |||
export const WaveformCanvas = React.forwardRef<WaveformCanvasDerivedElement, WaveformCanvasProps>(({ | |||
className, | |||
children, | |||
controls, | |||
@@ -34,16 +36,15 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent | |||
sampleRate, | |||
splitChannels, | |||
normalize, | |||
audioRef, | |||
...etcProps | |||
}, forwardedRef) => { | |||
const [isPlaying, setIsPlaying] = React.useState(false); | |||
const defaultRef = React.useRef<HTMLAudioElement>(null); | |||
const ref = forwardedRef ?? defaultRef; | |||
const containerRef = React.useRef<HTMLDivElement>(null); | |||
const defaultRef = React.useRef<WaveformCanvasDerivedElement>(null); | |||
const containerRef = forwardedRef ?? defaultRef; | |||
const waveSurferRef = React.useRef<any>(null); | |||
const handleAction: React.FormEventHandler<HTMLFormElement> = (e) => { | |||
e.preventDefault(); | |||
e.preventDefault(); | |||
const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement }; | |||
const formData = getFormValues( | |||
@@ -63,21 +64,26 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent | |||
}; | |||
React.useEffect(() => { | |||
const { current: container } = containerRef; | |||
const media = typeof ref === 'object' ? ref?.current : null; | |||
const { current: waveSurferCurrent } = waveSurferRef; | |||
if (!(typeof audioRef === 'object' && audioRef)) { | |||
return; | |||
} | |||
const { current: media } = audioRef; | |||
if (!media) { | |||
return; | |||
} | |||
const load = async (ref: React.Ref<HTMLAudioElement>) => { | |||
if (!(typeof ref === 'object' && ref?.current)) { | |||
return; | |||
} | |||
if (!(typeof containerRef === 'object' && containerRef)) { | |||
return; | |||
} | |||
const { current: container } = containerRef; | |||
if (!container) { | |||
return; | |||
} | |||
if (!(typeof containerRef === 'object' && containerRef?.current)) { | |||
return; | |||
} | |||
const { default: WaveSurfer } = await import('wavesurfer.js'); | |||
const load = async (media: HTMLAudioElement, container: HTMLElement) => { | |||
const {default: WaveSurfer} = await import('wavesurfer.js'); | |||
const waveSurferInstance = WaveSurfer.create({ | |||
container: containerRef.current, | |||
container, | |||
height: 'auto', | |||
autoplay: autoPlay, | |||
fillParent: true, | |||
@@ -101,7 +107,8 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent | |||
splitChannels, | |||
normalize, | |||
cursorWidth, | |||
media: media ?? undefined, | |||
plugins: [], | |||
media, | |||
}); | |||
waveSurferInstance.on('ready', () => { | |||
if (!container) { | |||
@@ -111,11 +118,20 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent | |||
container.removeChild(container.children[0]); | |||
} | |||
}); | |||
await waveSurferInstance.load(ref.current.currentSrc); | |||
waveSurferInstance.setTime(ref.current.currentTime); | |||
waveSurferRef.current = waveSurferInstance; | |||
await waveSurferInstance.load(media.currentSrc); | |||
waveSurferInstance.setTime(media.currentTime); | |||
return waveSurferInstance; | |||
}; | |||
void load(ref); | |||
const { current: waveSurferCurrent } = waveSurferRef; | |||
load(media, container) | |||
.then((i) => { | |||
waveSurferRef.current = i; | |||
}) | |||
.catch((error) => { | |||
console.log(error); | |||
}); | |||
return () => { | |||
if (waveSurferCurrent) { | |||
(waveSurferCurrent as unknown as Record<string, Function>).destroy(); | |||
@@ -125,7 +141,7 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent | |||
} | |||
}; | |||
}, [ | |||
ref, | |||
audioRef, | |||
autoPlay, | |||
waveColor, | |||
progressColor, | |||
@@ -147,6 +163,7 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent | |||
splitChannels, | |||
normalize, | |||
cursorWidth, | |||
containerRef, | |||
]); | |||
return ( | |||
@@ -169,15 +169,15 @@ const BlobPage: NextPage = () => { | |||
</Section> | |||
<Section title="FileSelectBox"> | |||
<Subsection title="Single File"> | |||
{/*<BlobReact.FileSelectBox*/} | |||
{/* border*/} | |||
{/* enhanced*/} | |||
{/* label="Primary Image"*/} | |||
{/* hint="Select any files here"*/} | |||
{/* block*/} | |||
{/* className="h-64"*/} | |||
{/* onChange={(e) => console.log(e.currentTarget.files)}*/} | |||
{/*/>*/} | |||
<BlobReact.FileSelectBox | |||
border | |||
enhanced | |||
label="Primary Image" | |||
hint="Select any files here" | |||
block | |||
className="sm:h-96" | |||
onChange={(e) => console.log(e.currentTarget.files)} | |||
/> | |||
</Subsection> | |||
</Section> | |||
</DefaultLayout> | |||
@@ -1,6 +1,6 @@ | |||
import * as React from 'react'; | |||
import { NextPage } from 'next'; | |||
import {Slider} from '@/categories/number/react'; | |||
import * as TesseractNumber from '@tesseract-design/web-number-react'; | |||
import {Section, Subsection} from '@/components/Section'; | |||
const NumberPage: NextPage = () => { | |||
@@ -8,21 +8,24 @@ const NumberPage: NextPage = () => { | |||
<main className="my-16 md:my-32"> | |||
<Section title="Spinner"> | |||
<Subsection title="Default"> | |||
TODO | |||
<input type="number" /> | |||
<TesseractNumber.Spinner | |||
min={-100} | |||
max={100} | |||
step="any" | |||
label="Step" | |||
/> | |||
</Subsection> | |||
</Section> | |||
<Section title="Slider"> | |||
<Subsection title="Default"> | |||
<Slider | |||
<TesseractNumber.Slider | |||
min={-100} | |||
max={100} | |||
tickMarks={[{ label: 'low', value: 25, }, 50]} | |||
/> | |||
</Subsection> | |||
<Subsection title="Vertical"> | |||
<Slider | |||
<TesseractNumber.Slider | |||
min={-100} | |||
max={100} | |||
tickMarks={[{ label: 'low', value: 25, }, 50]} | |||
@@ -1,7 +1,7 @@ | |||
import * as mimeTypes from 'mime-types'; | |||
import {getTextMetadata, TextMetadata} from '@modal-soft/text-utils'; | |||
import {getImageMetadata, ImageMetadata} from '@modal-soft/image-utils'; | |||
import {AudioMetadata, getAudioMetadata} from '@modal-soft/audio-utils'; | |||
import {getAudioMetadata, AudioMetadata} from '@modal-soft/audio-utils'; | |||
import {getVideoMetadata, VideoFileMetadata} from '@modal-soft/video-utils'; | |||
const MIME_TYPE_DESCRIPTIONS = { | |||
@@ -137,7 +137,7 @@ export const addDataUrl = async (f: Partial<File>): Promise<Partial<FileWithData | |||
return f; | |||
} | |||
interface FileWithResolvedContentType extends Partial<FileWithDataUrl> { | |||
export interface FileWithResolvedContentType extends Partial<FileWithDataUrl> { | |||
resolvedType: ContentType; | |||
} | |||
@@ -204,22 +204,3 @@ export const augmentVideoFile = async <T extends Partial<FileWithDataUrl>>(file: | |||
fileMutable.metadata = await getVideoMetadata(file.url); | |||
return fileMutable as unknown as VideoFile; | |||
}; | |||
export type AugmentedFile = TextFile | ImageFile | AudioFile | VideoFile | BinaryFile; | |||
export const augmentFile = async (f: File): Promise<AugmentedFile> => { | |||
const contentType = getContentType(f.type, f.name); | |||
switch (contentType) { | |||
case ContentType.TEXT: | |||
return augmentTextFile(f); | |||
case ContentType.IMAGE: | |||
return augmentImageFile(f); | |||
case ContentType.AUDIO: | |||
return augmentAudioFile(f); | |||
case ContentType.VIDEO: | |||
return augmentVideoFile(f); | |||
default: | |||
break; | |||
} | |||
return augmentBinaryFile(f); | |||
}; |
@@ -34,6 +34,7 @@ module.exports = { | |||
'code-global': 'rgb(var(--color-code-global))', | |||
'current': 'currentcolor', | |||
'inherit': 'inherit', | |||
'transparent': 'transparent', | |||
}, | |||
extend: { | |||
fontSize: { | |||