Change design of audio player in web UI (#34520)
This commit is contained in:
parent
24c25ec4f5
commit
b4394ec129
26 changed files with 1476 additions and 1088 deletions
840
app/javascript/mastodon/features/audio/index.tsx
Normal file
840
app/javascript/mastodon/features/audio/index.tsx
Normal file
|
@ -0,0 +1,840 @@
|
|||
import { useEffect, useRef, useCallback, useState, useId } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useSpring, animated, config } from '@react-spring/web';
|
||||
|
||||
import DownloadIcon from '@/material-icons/400-24px/download.svg?react';
|
||||
import Forward5Icon from '@/material-icons/400-24px/forward_5-fill.svg?react';
|
||||
import PauseIcon from '@/material-icons/400-24px/pause-fill.svg?react';
|
||||
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
|
||||
import Replay5Icon from '@/material-icons/400-24px/replay_5-fill.svg?react';
|
||||
import VolumeOffIcon from '@/material-icons/400-24px/volume_off-fill.svg?react';
|
||||
import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react';
|
||||
import { Blurhash } from 'mastodon/components/blurhash';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { SpoilerButton } from 'mastodon/components/spoiler_button';
|
||||
import { formatTime, getPointerPosition } from 'mastodon/features/video';
|
||||
import { useAudioVisualizer } from 'mastodon/hooks/useAudioVisualizer';
|
||||
import {
|
||||
displayMedia,
|
||||
useBlurhash,
|
||||
reduceMotion,
|
||||
} from 'mastodon/initial_state';
|
||||
import { playerSettings } from 'mastodon/settings';
|
||||
|
||||
const messages = defineMessages({
|
||||
play: { id: 'video.play', defaultMessage: 'Play' },
|
||||
pause: { id: 'video.pause', defaultMessage: 'Pause' },
|
||||
mute: { id: 'video.mute', defaultMessage: 'Mute' },
|
||||
unmute: { id: 'video.unmute', defaultMessage: 'Unmute' },
|
||||
download: { id: 'video.download', defaultMessage: 'Download file' },
|
||||
hide: { id: 'audio.hide', defaultMessage: 'Hide audio' },
|
||||
skipForward: { id: 'video.skip_forward', defaultMessage: 'Skip forward' },
|
||||
skipBackward: { id: 'video.skip_backward', defaultMessage: 'Skip backward' },
|
||||
});
|
||||
|
||||
const persistVolume = (volume: number, muted: boolean) => {
|
||||
playerSettings.set('volume', volume);
|
||||
playerSettings.set('muted', muted);
|
||||
};
|
||||
|
||||
const restoreVolume = (audio: HTMLAudioElement) => {
|
||||
const volume = (playerSettings.get('volume') as number | undefined) ?? 0.5;
|
||||
const muted = (playerSettings.get('muted') as boolean | undefined) ?? false;
|
||||
|
||||
audio.volume = volume;
|
||||
audio.muted = muted;
|
||||
};
|
||||
|
||||
const HOVER_FADE_DELAY = 4000;
|
||||
|
||||
export const Audio: React.FC<{
|
||||
src: string;
|
||||
alt?: string;
|
||||
lang?: string;
|
||||
poster?: string;
|
||||
sensitive?: boolean;
|
||||
editable?: boolean;
|
||||
blurhash?: string;
|
||||
visible?: boolean;
|
||||
duration?: number;
|
||||
onToggleVisibility?: () => void;
|
||||
backgroundColor?: string;
|
||||
foregroundColor?: string;
|
||||
accentColor?: string;
|
||||
startTime?: number;
|
||||
startPlaying?: boolean;
|
||||
startVolume?: number;
|
||||
startMuted?: boolean;
|
||||
deployPictureInPicture?: (
|
||||
type: string,
|
||||
mediaProps: {
|
||||
src: string;
|
||||
muted: boolean;
|
||||
volume: number;
|
||||
currentTime: number;
|
||||
poster?: string;
|
||||
backgroundColor: string;
|
||||
foregroundColor: string;
|
||||
accentColor: string;
|
||||
},
|
||||
) => void;
|
||||
matchedFilters?: string[];
|
||||
}> = ({
|
||||
src,
|
||||
alt,
|
||||
lang,
|
||||
poster,
|
||||
duration,
|
||||
sensitive,
|
||||
editable,
|
||||
blurhash,
|
||||
visible,
|
||||
onToggleVisibility,
|
||||
backgroundColor = '#000000',
|
||||
foregroundColor = '#ffffff',
|
||||
accentColor = '#ffffff',
|
||||
startTime,
|
||||
startPlaying,
|
||||
startVolume,
|
||||
startMuted,
|
||||
deployPictureInPicture,
|
||||
matchedFilters,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [loadedDuration, setDuration] = useState(duration ?? 0);
|
||||
const [paused, setPaused] = useState(true);
|
||||
const [muted, setMuted] = useState(false);
|
||||
const [volume, setVolume] = useState(0.5);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
|
||||
const playerRef = useRef<HTMLDivElement>(null);
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const seekRef = useRef<HTMLDivElement>(null);
|
||||
const volumeRef = useRef<HTMLDivElement>(null);
|
||||
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
|
||||
const [resumeAudio, suspendAudio, frequencyBands] = useAudioVisualizer(
|
||||
audioRef,
|
||||
3,
|
||||
);
|
||||
const accessibilityId = useId();
|
||||
|
||||
const [style, spring] = useSpring(() => ({
|
||||
progress: '0%',
|
||||
buffer: '0%',
|
||||
volume: '0%',
|
||||
}));
|
||||
|
||||
const handleAudioRef = useCallback(
|
||||
(c: HTMLVideoElement | null) => {
|
||||
if (audioRef.current && !audioRef.current.paused && c === null) {
|
||||
deployPictureInPicture?.('audio', {
|
||||
src,
|
||||
poster,
|
||||
backgroundColor,
|
||||
foregroundColor,
|
||||
accentColor,
|
||||
currentTime: audioRef.current.currentTime,
|
||||
muted: audioRef.current.muted,
|
||||
volume: audioRef.current.volume,
|
||||
});
|
||||
}
|
||||
|
||||
audioRef.current = c;
|
||||
|
||||
if (audioRef.current) {
|
||||
restoreVolume(audioRef.current);
|
||||
setVolume(audioRef.current.volume);
|
||||
setMuted(audioRef.current.muted);
|
||||
void spring.start({
|
||||
volume: `${audioRef.current.volume * 100}%`,
|
||||
immediate: reduceMotion,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
spring,
|
||||
setVolume,
|
||||
setMuted,
|
||||
src,
|
||||
poster,
|
||||
backgroundColor,
|
||||
accentColor,
|
||||
foregroundColor,
|
||||
deployPictureInPicture,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!audioRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
audioRef.current.volume = volume;
|
||||
audioRef.current.muted = muted;
|
||||
}, [volume, muted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof visible !== 'undefined') {
|
||||
setRevealed(visible);
|
||||
} else {
|
||||
setRevealed(
|
||||
displayMedia === 'show_all' ||
|
||||
(displayMedia !== 'hide_all' && !sensitive),
|
||||
);
|
||||
}
|
||||
}, [visible, sensitive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!revealed && audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
suspendAudio();
|
||||
}
|
||||
}, [suspendAudio, revealed]);
|
||||
|
||||
useEffect(() => {
|
||||
let nextFrame: ReturnType<typeof requestAnimationFrame>;
|
||||
|
||||
const updateProgress = () => {
|
||||
nextFrame = requestAnimationFrame(() => {
|
||||
if (audioRef.current && audioRef.current.duration > 0) {
|
||||
void spring.start({
|
||||
progress: `${(audioRef.current.currentTime / audioRef.current.duration) * 100}%`,
|
||||
immediate: reduceMotion,
|
||||
config: config.stiff,
|
||||
});
|
||||
}
|
||||
|
||||
updateProgress();
|
||||
});
|
||||
};
|
||||
|
||||
updateProgress();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(nextFrame);
|
||||
};
|
||||
}, [spring]);
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
if (!audioRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioRef.current.paused) {
|
||||
resumeAudio();
|
||||
void audioRef.current.play();
|
||||
} else {
|
||||
audioRef.current.pause();
|
||||
suspendAudio();
|
||||
}
|
||||
}, [resumeAudio, suspendAudio]);
|
||||
|
||||
const handlePlay = useCallback(() => {
|
||||
setPaused(false);
|
||||
}, []);
|
||||
|
||||
const handlePause = useCallback(() => {
|
||||
setPaused(true);
|
||||
}, []);
|
||||
|
||||
const handleProgress = useCallback(() => {
|
||||
if (!audioRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastTimeRange = audioRef.current.buffered.length - 1;
|
||||
|
||||
if (lastTimeRange > -1) {
|
||||
void spring.start({
|
||||
buffer: `${Math.ceil(audioRef.current.buffered.end(lastTimeRange) / audioRef.current.duration) * 100}%`,
|
||||
immediate: reduceMotion,
|
||||
});
|
||||
}
|
||||
}, [spring]);
|
||||
|
||||
const handleVolumeChange = useCallback(() => {
|
||||
if (!audioRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setVolume(audioRef.current.volume);
|
||||
setMuted(audioRef.current.muted);
|
||||
|
||||
void spring.start({
|
||||
volume: `${audioRef.current.muted ? 0 : audioRef.current.volume * 100}%`,
|
||||
immediate: reduceMotion,
|
||||
});
|
||||
|
||||
persistVolume(audioRef.current.volume, audioRef.current.muted);
|
||||
}, [spring, setVolume, setMuted]);
|
||||
|
||||
const handleTimeUpdate = useCallback(() => {
|
||||
if (!audioRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentTime(audioRef.current.currentTime);
|
||||
}, [setCurrentTime]);
|
||||
|
||||
const toggleMute = useCallback(() => {
|
||||
if (!audioRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const effectivelyMuted =
|
||||
audioRef.current.muted || audioRef.current.volume === 0;
|
||||
|
||||
if (effectivelyMuted) {
|
||||
audioRef.current.muted = false;
|
||||
|
||||
if (audioRef.current.volume === 0) {
|
||||
audioRef.current.volume = 0.05;
|
||||
}
|
||||
} else {
|
||||
audioRef.current.muted = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleReveal = useCallback(() => {
|
||||
if (onToggleVisibility) {
|
||||
onToggleVisibility();
|
||||
} else {
|
||||
setRevealed((value) => !value);
|
||||
}
|
||||
}, [onToggleVisibility, setRevealed]);
|
||||
|
||||
const handleVolumeMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
const handleVolumeMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleVolumeMouseMove, true);
|
||||
document.removeEventListener('mouseup', handleVolumeMouseUp, true);
|
||||
};
|
||||
|
||||
const handleVolumeMouseMove = (e: MouseEvent) => {
|
||||
if (!volumeRef.current || !audioRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { x } = getPointerPosition(volumeRef.current, e);
|
||||
|
||||
if (!isNaN(x)) {
|
||||
audioRef.current.volume = x;
|
||||
audioRef.current.muted = x > 0 ? false : true;
|
||||
void spring.start({ volume: `${x * 100}%`, immediate: true });
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleVolumeMouseMove, true);
|
||||
document.addEventListener('mouseup', handleVolumeMouseUp, true);
|
||||
|
||||
handleVolumeMouseMove(e.nativeEvent);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
},
|
||||
[spring],
|
||||
);
|
||||
|
||||
const handleSeekMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
const handleSeekMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleSeekMouseMove, true);
|
||||
document.removeEventListener('mouseup', handleSeekMouseUp, true);
|
||||
|
||||
setDragging(false);
|
||||
resumeAudio();
|
||||
void audioRef.current?.play();
|
||||
};
|
||||
|
||||
const handleSeekMouseMove = (e: MouseEvent) => {
|
||||
if (!seekRef.current || !audioRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { x } = getPointerPosition(seekRef.current, e);
|
||||
const newTime = audioRef.current.duration * x;
|
||||
|
||||
if (!isNaN(newTime)) {
|
||||
audioRef.current.currentTime = newTime;
|
||||
void spring.start({ progress: `${x * 100}%`, immediate: true });
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleSeekMouseMove, true);
|
||||
document.addEventListener('mouseup', handleSeekMouseUp, true);
|
||||
|
||||
setDragging(true);
|
||||
audioRef.current?.pause();
|
||||
handleSeekMouseMove(e.nativeEvent);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
},
|
||||
[setDragging, spring, resumeAudio],
|
||||
);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setHovered(true);
|
||||
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
}
|
||||
|
||||
hoverTimeoutRef.current = setTimeout(() => {
|
||||
setHovered(false);
|
||||
}, HOVER_FADE_DELAY);
|
||||
}, [setHovered]);
|
||||
|
||||
const handleMouseMove = useCallback(() => {
|
||||
setHovered(true);
|
||||
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
}
|
||||
|
||||
hoverTimeoutRef.current = setTimeout(() => {
|
||||
setHovered(false);
|
||||
}, HOVER_FADE_DELAY);
|
||||
}, [setHovered]);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setHovered(false);
|
||||
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
}
|
||||
}, [setHovered]);
|
||||
|
||||
const handleTouchEnd = useCallback(() => {
|
||||
setHovered(true);
|
||||
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
}
|
||||
|
||||
hoverTimeoutRef.current = setTimeout(() => {
|
||||
setHovered(false);
|
||||
}, HOVER_FADE_DELAY);
|
||||
}, [setHovered]);
|
||||
|
||||
const handleLoadedData = useCallback(() => {
|
||||
if (!audioRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDuration(audioRef.current.duration);
|
||||
|
||||
if (typeof startTime !== 'undefined') {
|
||||
audioRef.current.currentTime = startTime;
|
||||
}
|
||||
|
||||
if (typeof startVolume !== 'undefined') {
|
||||
audioRef.current.volume = startVolume;
|
||||
}
|
||||
|
||||
if (typeof startMuted !== 'undefined') {
|
||||
audioRef.current.muted = startMuted;
|
||||
}
|
||||
|
||||
if (startPlaying) {
|
||||
void audioRef.current.play();
|
||||
}
|
||||
}, [setDuration, startTime, startVolume, startMuted, startPlaying]);
|
||||
|
||||
const seekBy = (time: number) => {
|
||||
if (!audioRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTime = audioRef.current.currentTime + time;
|
||||
|
||||
if (!isNaN(newTime)) {
|
||||
audioRef.current.currentTime = newTime;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAudioKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
// On the audio element or the seek bar, we can safely use the space bar
|
||||
// for playback control because there are no buttons to press
|
||||
|
||||
if (e.key === ' ') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
togglePlay();
|
||||
}
|
||||
},
|
||||
[togglePlay],
|
||||
);
|
||||
|
||||
const handleSkipBackward = useCallback(() => {
|
||||
seekBy(-5);
|
||||
}, []);
|
||||
|
||||
const handleSkipForward = useCallback(() => {
|
||||
seekBy(5);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
const updateVolumeBy = (step: number) => {
|
||||
if (!audioRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newVolume = audioRef.current.volume + step;
|
||||
|
||||
if (!isNaN(newVolume)) {
|
||||
audioRef.current.volume = newVolume;
|
||||
audioRef.current.muted = newVolume > 0 ? false : true;
|
||||
}
|
||||
};
|
||||
|
||||
switch (e.key) {
|
||||
case 'k':
|
||||
case ' ':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
togglePlay();
|
||||
break;
|
||||
case 'm':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleMute();
|
||||
break;
|
||||
case 'j':
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
seekBy(-5);
|
||||
break;
|
||||
case 'l':
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
seekBy(5);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
updateVolumeBy(0.15);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
updateVolumeBy(-0.15);
|
||||
break;
|
||||
}
|
||||
},
|
||||
[togglePlay, toggleMute],
|
||||
);
|
||||
|
||||
const springForBand0 = useSpring({
|
||||
to: { r: 50 + (frequencyBands[0] ?? 0) * 10 },
|
||||
config: config.wobbly,
|
||||
});
|
||||
const springForBand1 = useSpring({
|
||||
to: { r: 50 + (frequencyBands[1] ?? 0) * 10 },
|
||||
config: config.wobbly,
|
||||
});
|
||||
const springForBand2 = useSpring({
|
||||
to: { r: 50 + (frequencyBands[2] ?? 0) * 10 },
|
||||
config: config.wobbly,
|
||||
});
|
||||
|
||||
const progress = Math.min((currentTime / loadedDuration) * 100, 100);
|
||||
const effectivelyMuted = muted || volume === 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('audio-player', { inactive: !revealed })}
|
||||
ref={playerRef}
|
||||
style={
|
||||
{
|
||||
'--player-background-color': backgroundColor,
|
||||
'--player-foreground-color': foregroundColor,
|
||||
'--player-accent-color': accentColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
aria-label={alt}
|
||||
lang={lang}
|
||||
>
|
||||
{blurhash && (
|
||||
<Blurhash
|
||||
hash={blurhash}
|
||||
className={classNames('media-gallery__preview', {
|
||||
'media-gallery__preview--hidden': revealed,
|
||||
})}
|
||||
dummy={!useBlurhash}
|
||||
/>
|
||||
)}
|
||||
|
||||
<audio /* eslint-disable-line jsx-a11y/media-has-caption */
|
||||
src={src}
|
||||
ref={handleAudioRef}
|
||||
preload={startPlaying ? 'auto' : 'none'}
|
||||
onPlay={handlePlay}
|
||||
onPause={handlePause}
|
||||
onProgress={handleProgress}
|
||||
onLoadedData={handleLoadedData}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
onVolumeChange={handleVolumeChange}
|
||||
crossOrigin='anonymous'
|
||||
/>
|
||||
|
||||
<div
|
||||
className='video-player__seek'
|
||||
aria-valuemin={0}
|
||||
aria-valuenow={progress}
|
||||
aria-valuemax={100}
|
||||
onMouseDown={handleSeekMouseDown}
|
||||
onKeyDownCapture={handleAudioKeyDown}
|
||||
ref={seekRef}
|
||||
role='slider'
|
||||
tabIndex={0}
|
||||
>
|
||||
<animated.div
|
||||
className='video-player__seek__buffer'
|
||||
style={{ width: style.buffer }}
|
||||
/>
|
||||
<animated.div
|
||||
className='video-player__seek__progress'
|
||||
style={{ width: style.progress }}
|
||||
/>
|
||||
|
||||
<animated.span
|
||||
className={classNames('video-player__seek__handle', {
|
||||
active: dragging,
|
||||
})}
|
||||
style={{ left: style.progress }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='audio-player__controls'>
|
||||
<div className='audio-player__controls__play'>
|
||||
<button
|
||||
type='button'
|
||||
title={intl.formatMessage(messages.skipBackward)}
|
||||
aria-label={intl.formatMessage(messages.skipBackward)}
|
||||
className='player-button'
|
||||
onClick={handleSkipBackward}
|
||||
>
|
||||
<Icon id='' icon={Replay5Icon} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='audio-player__controls__play'>
|
||||
<svg
|
||||
className='audio-player__visualizer'
|
||||
viewBox='0 0 124 124'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<animated.circle
|
||||
opacity={0.5}
|
||||
cx={57}
|
||||
cy={62.5}
|
||||
r={springForBand0.r}
|
||||
fill='var(--player-accent-color)'
|
||||
/>
|
||||
<animated.circle
|
||||
opacity={0.5}
|
||||
cx={65}
|
||||
cy={57.5}
|
||||
r={springForBand1.r}
|
||||
fill='var(--player-accent-color)'
|
||||
/>
|
||||
<animated.circle
|
||||
opacity={0.5}
|
||||
cx={63}
|
||||
cy={66.5}
|
||||
r={springForBand2.r}
|
||||
fill='var(--player-accent-color)'
|
||||
/>
|
||||
|
||||
<g clipPath={`url(#${accessibilityId}-clip)`}>
|
||||
<rect
|
||||
x={14}
|
||||
y={14}
|
||||
width={96}
|
||||
height={96}
|
||||
fill={`url(#${accessibilityId}-pattern)`}
|
||||
/>
|
||||
<rect
|
||||
x={14}
|
||||
y={14}
|
||||
width={96}
|
||||
height={96}
|
||||
fill='var(--player-background-color'
|
||||
opacity={0.45}
|
||||
/>
|
||||
</g>
|
||||
|
||||
<defs>
|
||||
<pattern
|
||||
id={`${accessibilityId}-pattern`}
|
||||
patternContentUnits='objectBoundingBox'
|
||||
width='1'
|
||||
height='1'
|
||||
>
|
||||
<use href={`#${accessibilityId}-image`} />
|
||||
</pattern>
|
||||
|
||||
<clipPath id={`${accessibilityId}-clip`}>
|
||||
<rect
|
||||
x={14}
|
||||
y={14}
|
||||
width={96}
|
||||
height={96}
|
||||
rx={48}
|
||||
fill='white'
|
||||
/>
|
||||
</clipPath>
|
||||
|
||||
<image
|
||||
id={`${accessibilityId}-image`}
|
||||
href={poster}
|
||||
width={1}
|
||||
height={1}
|
||||
preserveAspectRatio='none'
|
||||
/>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
title={intl.formatMessage(paused ? messages.play : messages.pause)}
|
||||
aria-label={intl.formatMessage(
|
||||
paused ? messages.play : messages.pause,
|
||||
)}
|
||||
className='player-button'
|
||||
onClick={togglePlay}
|
||||
>
|
||||
<Icon
|
||||
id={paused ? 'play' : 'pause'}
|
||||
icon={paused ? PlayArrowIcon : PauseIcon}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='audio-player__controls__play'>
|
||||
<button
|
||||
type='button'
|
||||
title={intl.formatMessage(messages.skipForward)}
|
||||
aria-label={intl.formatMessage(messages.skipForward)}
|
||||
className='player-button'
|
||||
onClick={handleSkipForward}
|
||||
>
|
||||
<Icon id='' icon={Forward5Icon} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SpoilerButton
|
||||
hidden={revealed || editable}
|
||||
sensitive={sensitive ?? false}
|
||||
onClick={toggleReveal}
|
||||
matchedFilters={matchedFilters}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classNames('video-player__controls', { active: hovered })}
|
||||
>
|
||||
<div className='video-player__buttons-bar'>
|
||||
<div className='video-player__buttons left'>
|
||||
<button
|
||||
type='button'
|
||||
title={intl.formatMessage(
|
||||
muted ? messages.unmute : messages.mute,
|
||||
)}
|
||||
aria-label={intl.formatMessage(
|
||||
muted ? messages.unmute : messages.mute,
|
||||
)}
|
||||
className='player-button'
|
||||
onClick={toggleMute}
|
||||
>
|
||||
<Icon
|
||||
id={muted ? 'volume-off' : 'volume-up'}
|
||||
icon={muted ? VolumeOffIcon : VolumeUpIcon}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div
|
||||
className='video-player__volume active'
|
||||
ref={volumeRef}
|
||||
onMouseDown={handleVolumeMouseDown}
|
||||
role='slider'
|
||||
aria-valuemin={0}
|
||||
aria-valuenow={effectivelyMuted ? 0 : volume * 100}
|
||||
aria-valuemax={100}
|
||||
tabIndex={0}
|
||||
>
|
||||
<animated.div
|
||||
className='video-player__volume__current'
|
||||
style={{ width: style.volume }}
|
||||
/>
|
||||
|
||||
<animated.span
|
||||
className={classNames('video-player__volume__handle')}
|
||||
style={{ left: style.volume }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className='video-player__time'>
|
||||
<span className='video-player__time-current'>
|
||||
{formatTime(Math.floor(currentTime))}
|
||||
</span>
|
||||
<span className='video-player__time-sep'>/</span>
|
||||
<span className='video-player__time-total'>
|
||||
{formatTime(Math.floor(loadedDuration))}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='video-player__buttons right'>
|
||||
{!editable && (
|
||||
<>
|
||||
<button
|
||||
type='button'
|
||||
className='player-button'
|
||||
onClick={toggleReveal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='media_gallery.hide'
|
||||
defaultMessage='Hide'
|
||||
/>
|
||||
</button>
|
||||
|
||||
<a
|
||||
title={intl.formatMessage(messages.download)}
|
||||
aria-label={intl.formatMessage(messages.download)}
|
||||
className='video-player__download__icon player-button'
|
||||
href={src}
|
||||
download
|
||||
>
|
||||
<Icon id='download' icon={DownloadIcon} />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default Audio;
|
Loading…
Add table
Add a link
Reference in a new issue