Merge branch 'fix-feature-video' into 'main'
Implement video feature using only tailwind See merge request soapbox-pub/soapbox!3187
This commit is contained in:
commit
4a114f4e91
|
@ -5,7 +5,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import Blurhash from 'soapbox/components/blurhash';
|
import Blurhash from 'soapbox/components/blurhash';
|
||||||
import Icon from 'soapbox/components/icon';
|
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||||
import { isPanoramic, isPortrait, minimumAspectRatio, maximumAspectRatio } from 'soapbox/utils/media-aspect-ratio';
|
import { isPanoramic, isPortrait, minimumAspectRatio, maximumAspectRatio } from 'soapbox/utils/media-aspect-ratio';
|
||||||
|
|
||||||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
|
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
|
||||||
|
@ -138,11 +138,13 @@ const Video: React.FC<IVideo> = ({
|
||||||
const [currentTime, setCurrentTime] = useState(0);
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
const [duration, setDuration] = useState(0);
|
const [duration, setDuration] = useState(0);
|
||||||
const [volume, setVolume] = useState(0.5);
|
const [volume, setVolume] = useState(0.5);
|
||||||
|
const [preVolume, setPreVolume] = useState(0);
|
||||||
const [paused, setPaused] = useState(true);
|
const [paused, setPaused] = useState(true);
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
const [containerWidth, setContainerWidth] = useState(width);
|
const [containerWidth, setContainerWidth] = useState(width);
|
||||||
const [fullscreen, setFullscreen] = useState(false);
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
|
const [seekHovered, setSeekHovered] = useState(false);
|
||||||
const [muted, setMuted] = useState(false);
|
const [muted, setMuted] = useState(false);
|
||||||
const [buffer, setBuffer] = useState(0);
|
const [buffer, setBuffer] = useState(0);
|
||||||
|
|
||||||
|
@ -387,12 +389,28 @@ const Video: React.FC<IVideo> = ({
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
setHovered(false);
|
setHovered(false);
|
||||||
};
|
};
|
||||||
|
const handleSeekEnter = () => {
|
||||||
|
setSeekHovered(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSeekLeave = () => {
|
||||||
|
setSeekHovered(false);
|
||||||
|
};
|
||||||
|
|
||||||
const toggleMute = () => {
|
const toggleMute = () => {
|
||||||
if (video.current) {
|
if (video.current) {
|
||||||
const muted = !video.current.muted;
|
const muted = !video.current.muted;
|
||||||
setMuted(!muted);
|
setMuted(!muted);
|
||||||
video.current.muted = muted;
|
video.current.muted = muted;
|
||||||
|
|
||||||
|
if (muted) {
|
||||||
|
setPreVolume(video.current.volume);
|
||||||
|
video.current.volume = 0;
|
||||||
|
setVolume(0);
|
||||||
|
} else {
|
||||||
|
video.current.volume = preVolume;
|
||||||
|
setVolume(preVolume);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -463,17 +481,15 @@ const Video: React.FC<IVideo> = ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role='menuitem'
|
role='menuitem'
|
||||||
className={clsx('video-player', { detailed, 'video-player--inline': inline && !fullscreen, fullscreen })}
|
className={clsx('relative box-border max-w-full overflow-hidden rounded-[10px] bg-black text-white focus:outline-0', { detailed, 'w-full h-full m-0': fullscreen })}
|
||||||
style={playerStyle}
|
style={playerStyle}
|
||||||
ref={player}
|
ref={player}
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
onClick={handleClickRoot}
|
onClick={handleClickRoot}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
{!fullscreen && (
|
{!fullscreen && (
|
||||||
<Blurhash hash={blurhash} className='media-gallery__preview' />
|
<Blurhash hash={blurhash} className='absolute left-0 top-0 z-0 size-full rounded-lg bg-gray-200 object-cover dark:bg-gray-900' />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<video
|
<video
|
||||||
|
@ -484,6 +500,10 @@ const Video: React.FC<IVideo> = ({
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={alt}
|
aria-label={alt}
|
||||||
title={alt}
|
title={alt}
|
||||||
|
className={clsx('relative z-10 block', {
|
||||||
|
'max-h-full object-contain': inline && !fullscreen,
|
||||||
|
'max-w-full max-h-full w-full h-full outline-none': fullscreen,
|
||||||
|
})}
|
||||||
width={width}
|
width={width}
|
||||||
height={height || DEFAULT_HEIGHT}
|
height={height || DEFAULT_HEIGHT}
|
||||||
onClick={togglePlay}
|
onClick={togglePlay}
|
||||||
|
@ -496,74 +516,110 @@ const Video: React.FC<IVideo> = ({
|
||||||
onVolumeChange={handleVolumeChange}
|
onVolumeChange={handleVolumeChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={clsx('video-player__controls', { active: paused || hovered })}>
|
<div className={clsx('absolute inset-x-0 bottom-0 z-20 box-border bg-gradient-to-t from-black/70 to-transparent px-[15px] opacity-0 transition-opacity duration-100 ease-linear', { 'opacity-100': paused || hovered })}>
|
||||||
<div className='video-player__seek' onMouseDown={handleMouseDown} ref={seek}>
|
<div className='relative h-6 cursor-pointer' onMouseDown={handleMouseDown} onMouseEnter={handleSeekEnter} onMouseLeave={handleSeekLeave} ref={seek}>
|
||||||
<div className='video-player__seek__buffer' style={{ width: `${buffer}%` }} />
|
<div
|
||||||
<div className='video-player__seek__progress' style={{ width: `${progress}%` }} />
|
style={{
|
||||||
|
content: '',
|
||||||
|
width: '100%',
|
||||||
|
background: 'rgba(255, 255, 255, 0.35)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
display: 'block',
|
||||||
|
position: 'absolute',
|
||||||
|
height: '4px',
|
||||||
|
top: '14px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className='absolute top-3.5 block h-1 rounded-md bg-white/20' style={{ width: `${buffer}%` }} />
|
||||||
|
<div className='absolute top-3.5 block h-1 rounded-md bg-accent-500' style={{ width: `${progress}%` }} />
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className={clsx('video-player__seek__handle', { active: dragging })}
|
className={clsx('absolute top-2.5 z-30 -ml-1.5 size-3 rounded-full bg-accent-500 opacity-0 shadow-[1px_2px_6px_rgba(0,0,0,0.3)] transition-opacity duration-100', { 'opacity-100': dragging || seekHovered })}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={{ left: `${progress}%` }}
|
style={{ left: `${progress}%` }}
|
||||||
onKeyDown={handleVideoKeyDown}
|
onKeyDown={handleVideoKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='video-player__buttons-bar'>
|
<div className='my-[-5px] flex justify-between pb-2'>
|
||||||
<div className='video-player__buttons left'>
|
<div className='flex w-full flex-auto items-center truncate text-[16px]'>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
title={intl.formatMessage(paused ? messages.play : messages.pause)}
|
title={intl.formatMessage(paused ? messages.play : messages.pause)}
|
||||||
aria-label={intl.formatMessage(paused ? messages.play : messages.pause)}
|
aria-label={intl.formatMessage(paused ? messages.play : messages.pause)}
|
||||||
className='player-button'
|
className={clsx('inline-block flex-none border-0 bg-transparent px-[6px] py-[5px] text-[16px] text-white/75 opacity-75 outline-none hover:text-white hover:opacity-100 focus:text-white focus:opacity-100 active:text-white active:opacity-100 '
|
||||||
|
, { 'py-[10px]': fullscreen })}
|
||||||
onClick={togglePlay}
|
onClick={togglePlay}
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
>
|
>
|
||||||
<Icon src={paused ? require('@tabler/icons/outline/player-play.svg') : require('@tabler/icons/outline/player-pause.svg')} />
|
<SvgIcon className='w-[20px]' src={paused ? require('@tabler/icons/outline/player-play.svg') : require('@tabler/icons/outline/player-pause.svg')} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
title={intl.formatMessage(muted ? messages.unmute : messages.mute)}
|
title={intl.formatMessage(muted ? messages.unmute : messages.mute)}
|
||||||
aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)}
|
aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)}
|
||||||
className='player-button'
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
className={clsx('inline-block flex-none border-0 bg-transparent px-[6px] py-[5px] text-[16px] text-white/75 opacity-75 outline-none hover:text-white hover:opacity-100 focus:text-white focus:opacity-100 active:text-white active:opacity-100 '
|
||||||
|
, { 'py-[10px]': fullscreen })}
|
||||||
onClick={toggleMute}
|
onClick={toggleMute}
|
||||||
>
|
>
|
||||||
<Icon src={muted ? require('@tabler/icons/outline/volume-3.svg') : require('@tabler/icons/outline/volume.svg')} />
|
<SvgIcon className='w-[20px]' src={muted ? require('@tabler/icons/outline/volume-3.svg') : require('@tabler/icons/outline/volume.svg')} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className={clsx('video-player__volume', { active: hovered })} onMouseDown={handleVolumeMouseDown} ref={slider}>
|
<div
|
||||||
<div className='video-player__volume__current' style={{ width: `${volume * 100}%` }} />
|
className={clsx('relative inline-flex h-6 flex-none cursor-pointer overflow-hidden transition-all duration-100 ease-linear', { 'overflow-visible w-[50px] mr-[16px]': hovered })} onMouseDown={handleVolumeMouseDown} ref={slider}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx({ 'bottom-[27px]': fullscreen || detailed })}
|
||||||
|
style={{
|
||||||
|
content: '',
|
||||||
|
width: '50px',
|
||||||
|
background: 'rgba(255, 255, 255, 0.35)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
display: 'block',
|
||||||
|
position: 'absolute',
|
||||||
|
height: '4px',
|
||||||
|
left: '0',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className={clsx('absolute left-0 top-1/2 block h-1 -translate-y-1/2 rounded-md bg-accent-500', { 'bottom-[27px]': fullscreen || detailed })} style={{ width: `${volume * 100}%` }} />
|
||||||
<span
|
<span
|
||||||
className={clsx('video-player__volume__handle')}
|
className={clsx('absolute left-0 top-1/2 z-30 -ml-1.5 size-3 -translate-y-1/2 rounded-full bg-accent-500 opacity-0 shadow-[1px_2px_6px_rgba(0,0,0,0.3)] transition-opacity duration-100', { 'opacity-100': hovered, 'bottom-[23px]': fullscreen || detailed })}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={{ left: `${volume * 100}%` }}
|
style={{ left: `${volume * 100}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(detailed || fullscreen) && (
|
<span>
|
||||||
<span>
|
<span className='text-sm font-medium text-white/75'>{formatTime(currentTime)}</span>
|
||||||
<span className='video-player__time-current'>{formatTime(currentTime)}</span>
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
<span className='mx-1.5 inline-block text-sm font-medium text-white/75'>/</span>
|
||||||
<span className='video-player__time-sep'>/</span>
|
<span className='text-sm font-medium text-white/75'>{formatTime(duration)}</span>
|
||||||
<span className='video-player__time-total'>{formatTime(duration)}</span>
|
</span>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{link && (
|
{link && (
|
||||||
<span className='video-player__link'>{link}</span>
|
<span className='px-[2px] py-[10px] text-[14px] font-medium text-white no-underline hover:underline focus:underline active:underline'>
|
||||||
|
{link}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='video-player__buttons right'>
|
<div className='flex min-w-[30px] flex-auto items-center truncate text-[16px]'>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)}
|
title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)}
|
||||||
aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)}
|
aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)}
|
||||||
className='player-button'
|
className={clsx('inline-block flex-none border-0 bg-transparent px-[6px] py-[5px] text-[16px] text-white/75 opacity-75 outline-none hover:text-white hover:opacity-100 focus:text-white focus:opacity-100 active:text-white active:opacity-100 '
|
||||||
|
, { 'py-[10px]': fullscreen })}
|
||||||
onClick={toggleFullscreen}
|
onClick={toggleFullscreen}
|
||||||
>
|
>
|
||||||
<Icon src={fullscreen ? require('@tabler/icons/outline/arrows-minimize.svg') : require('@tabler/icons/outline/arrows-maximize.svg')} />
|
<SvgIcon className='w-[20px]' src={fullscreen ? require('@tabler/icons/outline/arrows-minimize.svg') : require('@tabler/icons/outline/arrows-maximize.svg')} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue