phanpy/src/components/media.jsx
2023-02-23 17:01:33 +08:00

198 lines
5.5 KiB
JavaScript

import { getBlurHashAverageColor } from 'fast-blurhash';
import { useRef } from 'preact/hooks';
import { formatDuration } from './status';
/*
Media type
===
unknown = unsupported or unrecognized file type
image = Static image
gifv = Looping, soundless animation
video = Video clip
audio = Audio track
*/
function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
const { blurhash, description, meta, previewUrl, remoteUrl, url, type } =
media;
const { original = {}, small, focus } = meta || {};
const width = showOriginal ? original?.width : small?.width;
const height = showOriginal ? original?.height : small?.height;
const mediaURL = showOriginal ? url : previewUrl;
const rgbAverageColor = blurhash ? getBlurHashAverageColor(blurhash) : null;
const videoRef = useRef();
let focalBackgroundPosition;
if (focus) {
// Convert focal point to CSS background position
// Formula from jquery-focuspoint
// x = -1, y = 1 => 0% 0%
// x = 0, y = 0 => 50% 50%
// x = 1, y = -1 => 100% 100%
const x = ((focus.x + 1) / 2) * 100;
const y = ((1 - focus.y) / 2) * 100;
focalBackgroundPosition = `${x.toFixed(0)}% ${y.toFixed(0)}%`;
}
if (type === 'image' || (type === 'unknown' && previewUrl && url)) {
// Note: type: unknown might not have width/height
return (
<div
class={`media media-image`}
onClick={onClick}
style={
showOriginal && {
backgroundImage: `url(${previewUrl})`,
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
aspectRatio: `${width}/${height}`,
width,
height,
maxWidth: '100%',
maxHeight: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}
}
>
<img
src={mediaURL}
alt={description}
width={width}
height={height}
loading={showOriginal ? 'eager' : 'lazy'}
style={
!showOriginal && {
backgroundColor:
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
backgroundPosition: focalBackgroundPosition || 'center',
}
}
/>
</div>
);
} else if (type === 'gifv' || type === 'video') {
const shortDuration = original.duration < 31;
const isGIF = type === 'gifv' && shortDuration;
// If GIF is too long, treat it as a video
const loopable = original.duration < 61;
const formattedDuration = formatDuration(original.duration);
const hoverAnimate = !showOriginal && !autoAnimate && isGIF;
const autoGIFAnimate = !showOriginal && autoAnimate && isGIF;
return (
<div
class={`media media-${isGIF ? 'gif' : 'video'} ${
autoGIFAnimate ? 'media-contain' : ''
}`}
data-formatted-duration={formattedDuration}
data-label={isGIF && !showOriginal && !autoGIFAnimate ? 'GIF' : ''}
style={{
backgroundColor:
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
}}
onClick={(e) => {
if (hoverAnimate) {
try {
videoRef.current.pause();
} catch (e) {}
}
onClick(e);
}}
onMouseEnter={() => {
if (hoverAnimate) {
try {
videoRef.current.play();
} catch (e) {}
}
}}
onMouseLeave={() => {
if (hoverAnimate) {
try {
videoRef.current.pause();
} catch (e) {}
}
}}
>
{showOriginal || autoGIFAnimate ? (
<div
style={{
width: '100%',
height: '100%',
}}
dangerouslySetInnerHTML={{
__html: `
<video
src="${url}"
poster="${previewUrl}"
width="${width}"
height="${height}"
preload="auto"
autoplay
muted="${isGIF}"
${isGIF ? '' : 'controls'}
playsinline
loop="${loopable}"
${
isGIF
? 'ondblclick="this.paused ? this.play() : this.pause()"'
: ''
}
></video>
`,
}}
/>
) : isGIF ? (
<video
ref={videoRef}
src={url}
poster={previewUrl}
width={width}
height={height}
preload="auto"
// controls
playsinline
loop
muted
/>
) : (
<img
src={previewUrl}
alt={description}
width={width}
height={height}
loading="lazy"
/>
)}
</div>
);
} else if (type === 'audio') {
const formattedDuration = formatDuration(original.duration);
return (
<div
class="media media-audio"
data-formatted-duration={formattedDuration}
onClick={onClick}
>
{showOriginal ? (
<audio src={remoteUrl || url} preload="none" controls autoplay />
) : previewUrl ? (
<img
src={previewUrl}
alt={description}
width={width}
height={height}
loading="lazy"
/>
) : null}
</div>
);
}
}
export default Media;