Browse Source

Refactor media preview behaviors

Unify audio and video interactivity.
pull/1/head
TheoryOfNekomata 1 year ago
parent
commit
6e4d6fa3a7
9 changed files with 84 additions and 177 deletions
  1. +63
    -12
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx
  2. +2
    -2
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx
  3. +1
    -1
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/image.ts
  4. +2
    -0
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/index.ts
  5. +11
    -0
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/media.ts
  6. +0
    -146
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/media/audio.ts
  7. +0
    -3
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/media/index.ts
  8. +1
    -1
      packages/web-kitchensink-reactnext/src/categories/blob/react/index.ts
  9. +4
    -12
      packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveSurferCanvas/index.tsx

+ 63
- 12
packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx View File

@@ -6,7 +6,7 @@ import {
formatSecondsDurationConcise, formatSecondsDurationConcise,
formatSecondsDurationPrecise, formatSecondsDurationPrecise,
} from '@/utils/numeral'; } from '@/utils/numeral';
import {useMediaControls} from '../../hooks/media';
import {useMediaControls} from '../../hooks/interactive';
import {useAugmentedFile} from '@/categories/blob/react'; import {useAugmentedFile} from '@/categories/blob/react';


import clsx from 'clsx'; import clsx from 'clsx';
@@ -26,6 +26,7 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
className, className,
enhanced = false, enhanced = false,
disabled = false, disabled = false,
...etcProps
}, forwardedRef) => { }, forwardedRef) => {
const { augmentedFile, error } = useAugmentedFile({ const { augmentedFile, error } = useAugmentedFile({
file, file,
@@ -50,8 +51,11 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
startSeek, startSeek,
endSeek, endSeek,
setSeek, setSeek,
visualizationMode,
handleVisualizationModeChange,
} = useMediaControls<HTMLAudioElement>({ } = useMediaControls<HTMLAudioElement>({
controllerRef: forwardedRef, controllerRef: forwardedRef,
visualizationMode: 'waveform',
}); });
const formId = React.useId(); const formId = React.useId();


@@ -79,27 +83,74 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
data-testid="preview" data-testid="preview"
> >
<div className="w-full flex-auto relative"> <div className="w-full flex-auto relative">
<WaveSurferCanvas
className="sm:absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-[#000000]"
<audio
{...etcProps}
controls={!enhanced}
ref={mediaControllerRef} ref={mediaControllerRef}
onLoadedMetadata={refreshControls} onLoadedMetadata={refreshControls}
onDurationChange={refreshControls} onDurationChange={refreshControls}
onEnded={reset} onEnded={reset}
onTimeUpdate={updateSeekFromPlayback} onTimeUpdate={updateSeekFromPlayback}
data-testid="preview"
barWidth={2}
barGap={2}
waveColor="rgb(199 138 179)"
progressColor="rgb(238 238 238)"
cursorWidth={2}
cursorColor="rgb(255 153 0)"
interact
> >
<source <source
src={augmentedFile.metadata.previewUrl} src={augmentedFile.metadata.previewUrl}
type={augmentedFile.type} type={augmentedFile.type}
/> />
</WaveSurferCanvas>
</audio>
{visualizationMode === 'waveform' && (
<WaveSurferCanvas
className="sm:absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-[#000000]"
ref={mediaControllerRef}
data-testid="preview"
barWidth={1}
barGap={1}
waveColor="rgb(199 138 179)"
progressColor="rgb(255 153 0)"
interact
/>
)}
<div className="flex gap-4 absolute top-0 right-0 z-[2] px-4">
<label
className={clsx(
'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',
)}
>
<input
type="radio"
name="visualizationMode"
value="waveform"
className="sr-only"
onChange={handleVisualizationModeChange}
/>
<span
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
>
Waveform
</span>
</label>
<label
className={clsx(
'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',
)}
>
<input
type="radio"
name="visualizationMode"
value="spectrum"
className="sr-only"
onChange={handleVisualizationModeChange}
/>
<span
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
>
Spectrum
</span>
</label>
</div>
</div> </div>
{enhanced && ( {enhanced && (
<div className="w-full flex-shrink-0 h-10 flex gap-4 relative"> <div className="w-full flex-shrink-0 h-10 flex gap-4 relative">


+ 2
- 2
packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx View File

@@ -39,7 +39,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
style={style} style={style}
> >
<div className="h-full relative"> <div className="h-full relative">
<div className="sm:absolute top-0 left-0 w-full sm:h-full z-[1]">
<div className="sm:absolute top-0 left-0 w-full sm:h-full z-[3]">
{ {
typeof augmentedFile.metadata?.previewUrl === 'string' typeof augmentedFile.metadata?.previewUrl === 'string'
&& ( && (
@@ -144,7 +144,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
'focus:outline-0', 'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed', 'disabled:opacity-50 disabled:cursor-not-allowed',
{ {
'fixed top-0 left-0 w-full h-full opacity-0 z-[1]': fullScreen,
'fixed top-0 left-0 w-full h-full opacity-0 z-[3]': fullScreen,
} }
)} )}
> >


packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/media/image.ts → packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/image.ts View File

@@ -27,7 +27,7 @@ export const useImageControls = (options = {} as UseImageControlsOptions) => {


const downloadLink = window.document.createElement('a'); const downloadLink = window.document.createElement('a');
downloadLink.download = filenameRef.current.textContent ?? 'image'; downloadLink.download = filenameRef.current.textContent ?? 'image';
downloadLink.href = imageRef.current.src;
downloadLink.href = imageRef.current.currentSrc;
downloadLink.addEventListener('click', () => { downloadLink.addEventListener('click', () => {
downloadLink.remove(); downloadLink.remove();
}); });

+ 2
- 0
packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/index.ts View File

@@ -0,0 +1,2 @@
export * from './media';
export * from './image';

packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/media/video.ts → packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/media.ts View File

@@ -3,17 +3,20 @@ import * as React from 'react';
export interface UseMediaControlsOptions<T extends HTMLMediaElement> { export interface UseMediaControlsOptions<T extends HTMLMediaElement> {
controllerRef: React.Ref<T>; controllerRef: React.Ref<T>;
actionFormKey?: string; actionFormKey?: string;
visualizationMode?: string;
} }


export const useMediaControls = <T extends HTMLMediaElement>({ export const useMediaControls = <T extends HTMLMediaElement>({
controllerRef: forwardedRef, controllerRef: forwardedRef,
actionFormKey = 'action' as const, actionFormKey = 'action' as const,
visualizationMode: initialVisualizationMode,
}: UseMediaControlsOptions<T>) => { }: UseMediaControlsOptions<T>) => {
const defaultRef = React.useRef<T>(null); const defaultRef = React.useRef<T>(null);
const ref = forwardedRef ?? defaultRef; const ref = forwardedRef ?? defaultRef;
const seekRef = React.useRef<HTMLInputElement>(null); const seekRef = React.useRef<HTMLInputElement>(null);
const volumeRef = React.useRef<HTMLInputElement>(null); const volumeRef = React.useRef<HTMLInputElement>(null);
const filenameRef = React.useRef<HTMLElement>(null); const filenameRef = React.useRef<HTMLElement>(null);
const [visualizationMode, setVisualizationMode] = React.useState(initialVisualizationMode);
const [isPlaying, setIsPlaying] = React.useState(false); const [isPlaying, setIsPlaying] = React.useState(false);
const [isSeeking, setIsSeeking] = React.useState(false); const [isSeeking, setIsSeeking] = React.useState(false);
const [currentTimeDisplay, setCurrentTimeDisplay] = React.useState<number>(); const [currentTimeDisplay, setCurrentTimeDisplay] = React.useState<number>();
@@ -123,6 +126,10 @@ export const useMediaControls = <T extends HTMLMediaElement>({
downloadLink.click(); downloadLink.click();
}, [ref, filenameRef]); }, [ref, filenameRef]);


const handleVisualizationModeChange: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => {
setVisualizationMode(e.currentTarget.value);
}, []);

const actions = React.useMemo(() => ({ const actions = React.useMemo(() => ({
togglePlayback, togglePlayback,
toggleSeekTimeCountMode, toggleSeekTimeCountMode,
@@ -229,6 +236,8 @@ export const useMediaControls = <T extends HTMLMediaElement>({
mediaControllerRef: ref, mediaControllerRef: ref,
handleAction, handleAction,
filenameRef, filenameRef,
visualizationMode,
handleVisualizationModeChange,
}), [ }), [
refreshControls, refreshControls,
isPlaying, isPlaying,
@@ -246,5 +255,7 @@ export const useMediaControls = <T extends HTMLMediaElement>({
ref, ref,
handleAction, handleAction,
filenameRef, filenameRef,
visualizationMode,
handleVisualizationModeChange,
]); ]);
}; };

+ 0
- 146
packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/media/audio.ts View File

@@ -1,146 +0,0 @@
import * as React from 'react';

export interface UseAudioControlsOptions {
actionFormKey?: string;
forwardedRef?: React.Ref<any>;
}

export const useAudioControls = ({
forwardedRef,
actionFormKey = 'action' as const,
}: UseAudioControlsOptions) => {
const mediaControllerRef = React.useRef<HTMLAudioElement>(null);
const volumeRef = React.useRef<HTMLInputElement>(null);
const filenameRef = React.useRef<HTMLElement>(null);
const [isPlaying, setIsPlaying] = React.useState(false);
const [isSeeking, setIsSeeking] = React.useState(false);
const [currentTimeDisplay, setCurrentTimeDisplay] = React.useState<number>();
const [seekTimeDisplay, setSeekTimeDisplay] = React.useState<number>();
const [durationDisplay, setDurationDisplay] = React.useState<number>();
const [isSeekTimeCountingDown, setIsSeekTimeCountingDown] = React.useState(false);

const refreshControls: React.ReactEventHandler<HTMLAudioElement> = React.useCallback((e) => {
const { currentTarget: mediaController } = e;

setCurrentTimeDisplay(mediaController.currentTime);
setDurationDisplay(mediaController.duration);
}, []);

const reset: React.ReactEventHandler<HTMLAudioElement> = React.useCallback((e) => {
const videoElement = e.currentTarget;
setIsPlaying(false);
videoElement.currentTime = 0;
}, []);

const updateSeekFromPlayback = React.useCallback((e) => {
if (isSeeking) {
return;
}

const videoElement = e.currentTarget;
const currentTime = videoElement.currentTime;
setCurrentTimeDisplay(currentTime);
}, [isSeeking]);

const adjustVolume: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => {
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef !== null)) {
return;
}

if (!mediaControllerRef.current) {
return;
}
const { value } = e.currentTarget;
mediaControllerRef.current.volume = Number(value);
}, [mediaControllerRef]);

const togglePlayback = React.useCallback(() => {
setIsPlaying((p) => !p);
}, []);

const toggleSeekTimeCountMode = React.useCallback(() => {
setIsSeekTimeCountingDown((b) => !b);
}, []);

const download = React.useCallback(() => {
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef?.current !== null)) {
return;
}

if (!(typeof filenameRef === 'object' && filenameRef?.current !== null)) {
return;
}

const downloadLink = window.document.createElement('a');
downloadLink.download = filenameRef.current.textContent ?? 'image';
downloadLink.href = mediaControllerRef.current.currentSrc;
downloadLink.addEventListener('click', () => {
downloadLink.remove();
});
downloadLink.click();
}, [mediaControllerRef, filenameRef]);

const actions = React.useMemo(() => ({
togglePlayback,
toggleSeekTimeCountMode,
download,
}), [togglePlayback, toggleSeekTimeCountMode, download]);

const handleAction: React.FormEventHandler<HTMLFormElement> = 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 { [actionName]: actionFunction } = actions;
actionFunction?.();
}, [actions, actionFormKey]);

React.useEffect(() => {
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef !== null)) {
return;
}

if (!mediaControllerRef.current) {
return;
}

if (isPlaying) {
//console.log(mediaControllerRef.current);
void mediaControllerRef.current.play();
return
}

mediaControllerRef.current.pause();
}, [isPlaying, mediaControllerRef]);

return React.useMemo(() => ({
mediaControllerRef,
refreshControls,
reset: reset,
updateSeekFromPlayback,
isPlaying,
isSeeking,
currentTimeDisplay,
seekTimeDisplay,
durationDisplay,
isSeekTimeCountingDown,
volumeRef,
adjustVolume,
filenameRef,
handleAction,
}), [
mediaControllerRef,
refreshControls,
reset,
updateSeekFromPlayback,
isPlaying,
isSeeking,
currentTimeDisplay,
seekTimeDisplay,
durationDisplay,
isSeekTimeCountingDown,
volumeRef,
adjustVolume,
filenameRef,
handleAction,
]);
};

+ 0
- 3
packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/media/index.ts View File

@@ -1,3 +0,0 @@
export * from './audio';
export * from './video';
export * from './image';

+ 1
- 1
packages/web-kitchensink-reactnext/src/categories/blob/react/index.ts View File

@@ -7,4 +7,4 @@ export * from './components/ImageFilePreview';
export * from './components/VideoFilePreview'; export * from './components/VideoFilePreview';


export * from './hooks/blob'; export * from './hooks/blob';
export * from './hooks/media';
export * from './hooks/interactive';

+ 4
- 12
packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveSurferCanvas/index.tsx View File

@@ -69,6 +69,7 @@ export const WaveSurferCanvas = React.forwardRef<WaveSurferCanvasDerivedComponen
return; return;
} }
const { default: WaveSurfer } = await import('wavesurfer.js'); const { default: WaveSurfer } = await import('wavesurfer.js');
//const a = await import('wavesurfer.js/dist/plugins/spectrogram');
const waveSurferInstance = WaveSurfer.create({ const waveSurferInstance = WaveSurfer.create({
container: containerRef.current, container: containerRef.current,
height: containerRef.current.clientHeight, height: containerRef.current.clientHeight,
@@ -97,8 +98,6 @@ export const WaveSurferCanvas = React.forwardRef<WaveSurferCanvasDerivedComponen
cursorWidth, cursorWidth,
media: media ?? undefined, media: media ?? undefined,
}); });
await waveSurferInstance.load(ref.current.currentSrc);
waveSurferInstance.setTime(ref.current.currentTime);
waveSurferInstance.on('ready', () => { waveSurferInstance.on('ready', () => {
if (!container) { if (!container) {
return; return;
@@ -107,6 +106,8 @@ export const WaveSurferCanvas = React.forwardRef<WaveSurferCanvasDerivedComponen
container.removeChild(container.children[0]); container.removeChild(container.children[0]);
} }
}); });
await waveSurferInstance.load(ref.current.currentSrc);
waveSurferInstance.setTime(ref.current.currentTime);
waveSurferRef.current = waveSurferInstance; waveSurferRef.current = waveSurferInstance;
}; };
void load(ref); void load(ref);
@@ -147,19 +148,10 @@ export const WaveSurferCanvas = React.forwardRef<WaveSurferCanvasDerivedComponen
return ( return (
<div <div
className={clsx( className={clsx(
'relative h-full w-full flex flex-col',
'relative flex flex-col',
className, className,
)} )}
> >
<audio
{...etcProps}
controls={controls}
className={className}
ref={ref}
autoPlay={autoPlay}
>
{children}
</audio>
<div <div
className="flex-auto relative" className="flex-auto relative"
> >


Loading…
Cancel
Save