Audio Player: More playback speed options (#2164)
This commit is contained in:
parent
d0055d6653
commit
28d87bd580
@ -8,7 +8,7 @@
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
> .Button {
|
||||
> .Button, & > .playback-rate-menu {
|
||||
flex-shrink: 0;
|
||||
margin: 0.125rem;
|
||||
}
|
||||
@ -21,14 +21,9 @@
|
||||
}
|
||||
|
||||
.player-button {
|
||||
&.smaller {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
|
||||
i {
|
||||
font-size: 1.625rem;
|
||||
margin-top: -0.0625rem;
|
||||
}
|
||||
&.smaller i {
|
||||
font-size: 1.625rem;
|
||||
margin-top: -0.0625rem;
|
||||
}
|
||||
|
||||
i {
|
||||
@ -108,6 +103,8 @@
|
||||
}
|
||||
|
||||
.playback-button {
|
||||
overflow: visible;
|
||||
|
||||
&.applied {
|
||||
--color-text-secondary: var(--color-primary);
|
||||
}
|
||||
@ -185,4 +182,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playback-rate-menu .bubble {
|
||||
min-width: auto;
|
||||
|
||||
.icon-check, .icon-placeholder {
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
width: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
useCallback, useEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
@ -19,14 +21,18 @@ import buildClassName from '../../util/buildClassName';
|
||||
import { makeTrackId } from '../../util/audioPlayer';
|
||||
import { clearMediaSession } from '../../util/mediaSession';
|
||||
import windowSize from '../../util/windowSize';
|
||||
import useAudioPlayer from '../../hooks/useAudioPlayer';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useMessageMediaMetadata from '../../hooks/useMessageMediaMetadata';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import useAudioPlayer from '../../hooks/useAudioPlayer';
|
||||
import useMessageMediaMetadata from '../../hooks/useMessageMediaMetadata';
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
|
||||
import RippleEffect from '../ui/RippleEffect';
|
||||
import Button from '../ui/Button';
|
||||
import RangeSlider from '../ui/RangeSlider';
|
||||
import DropdownMenu from '../ui/DropdownMenu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
|
||||
import './AudioPlayer.scss';
|
||||
|
||||
@ -45,7 +51,13 @@ type StateProps = {
|
||||
isMuted: boolean;
|
||||
};
|
||||
|
||||
const FAST_PLAYBACK_RATE = 1.8;
|
||||
const PLAYBACK_RATES: Record<number, number> = {
|
||||
0.5: 0.66,
|
||||
0.75: 0.8,
|
||||
1: 1,
|
||||
1.5: 1.4,
|
||||
2: 1.8,
|
||||
};
|
||||
|
||||
const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
message,
|
||||
@ -65,6 +77,8 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
closeAudioPlayer,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const lang = useLang();
|
||||
const { audio, voice, video } = getMessageContent(message);
|
||||
const isVoice = Boolean(voice || video);
|
||||
@ -113,6 +127,12 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
}, [isVoicePlaying]);
|
||||
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
focusMessage({ chatId: message.chatId, messageId: message.id });
|
||||
}, [focusMessage, message.chatId, message.id]);
|
||||
@ -138,15 +158,37 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
setAudioPlayerMuted({ isMuted: !isMuted });
|
||||
}, [isMuted, setAudioPlayerMuted, toggleMuted]);
|
||||
|
||||
const updatePlaybackRate = useCallback((newRate: number) => {
|
||||
const rate = PLAYBACK_RATES[newRate];
|
||||
setAudioPlayerPlaybackRate({ playbackRate: rate });
|
||||
setPlaybackRate(rate);
|
||||
}, [setAudioPlayerPlaybackRate, setPlaybackRate]);
|
||||
|
||||
const handlePlaybackClick = useCallback(() => {
|
||||
if (playbackRate === 1) {
|
||||
setPlaybackRate(FAST_PLAYBACK_RATE);
|
||||
setAudioPlayerPlaybackRate({ playbackRate: FAST_PLAYBACK_RATE });
|
||||
} else {
|
||||
setPlaybackRate(1);
|
||||
setAudioPlayerPlaybackRate({ playbackRate: 1 });
|
||||
}
|
||||
}, [playbackRate, setAudioPlayerPlaybackRate, setPlaybackRate]);
|
||||
if (isContextMenuOpen) return;
|
||||
updatePlaybackRate(playbackRate === 1 ? 2 : 1);
|
||||
}, [isContextMenuOpen, playbackRate, updatePlaybackRate]);
|
||||
|
||||
const PlaybackRateButton = useCallback(() => {
|
||||
const displayRate = Object.entries(PLAYBACK_RATES).find(([, rate]) => rate === playbackRate)?.[0] || 1;
|
||||
return (
|
||||
<Button
|
||||
round
|
||||
className={buildClassName('playback-button', playbackRate !== 1 && 'applied')}
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
ariaLabel="Playback Rate"
|
||||
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||||
onClick={handlePlaybackClick}
|
||||
onMouseDown={handleBeforeContextMenu}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<span className="playback-button-inner">
|
||||
{playbackRate === 1 ? 2 : displayRate}Х
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}, [handleBeforeContextMenu, handleContextMenu, handlePlaybackClick, playbackRate]);
|
||||
|
||||
const volumeIcon = useMemo(() => {
|
||||
if (volume === 0 || isMuted) return 'icon-muted';
|
||||
@ -160,7 +202,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={buildClassName('AudioPlayer', className)} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<div className={buildClassName('AudioPlayer', className)} dir={lang.isRtl ? 'rtl' : undefined} ref={ref}>
|
||||
<div className="AudioPlayer-content" onClick={handleClick}>
|
||||
{audio ? renderAudio(audio) : renderVoice(lang('AttachAudio'), senderName)}
|
||||
<RippleEffect />
|
||||
@ -223,17 +265,21 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
</Button>
|
||||
|
||||
{shouldRenderPlaybackButton && (
|
||||
<Button
|
||||
round
|
||||
className={buildClassName('playback-button', playbackRate !== 1 && 'applied')}
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
ariaLabel="Playback Rate"
|
||||
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||||
onClick={handlePlaybackClick}
|
||||
<DropdownMenu
|
||||
forceOpen={isContextMenuOpen}
|
||||
positionX="right"
|
||||
positionY="top"
|
||||
className="playback-rate-menu"
|
||||
trigger={PlaybackRateButton}
|
||||
onClose={handleContextMenuClose}
|
||||
onHide={handleContextMenuHide}
|
||||
>
|
||||
<span className="playback-button-inner">2Х</span>
|
||||
</Button>
|
||||
{renderPlaybackRateMenuItem(0.5, playbackRate, updatePlaybackRate)}
|
||||
{renderPlaybackRateMenuItem(0.75, playbackRate, updatePlaybackRate)}
|
||||
{renderPlaybackRateMenuItem(1, playbackRate, updatePlaybackRate)}
|
||||
{renderPlaybackRateMenuItem(1.5, playbackRate, updatePlaybackRate)}
|
||||
{renderPlaybackRateMenuItem(2, playbackRate, updatePlaybackRate)}
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
<Button
|
||||
@ -272,6 +318,19 @@ function renderVoice(subtitle: string, senderName?: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderPlaybackRateMenuItem(rate: number, currentRate: number, onClick: (rate: number) => void) {
|
||||
return (
|
||||
<MenuItem
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onClick(rate)}
|
||||
icon={currentRate === PLAYBACK_RATES[rate] ? 'check' : undefined}
|
||||
customIcon={currentRate !== PLAYBACK_RATES[rate] ? <i className="icon-placeholder" /> : undefined}
|
||||
>
|
||||
{rate}X
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default withGlobal<OwnProps>(
|
||||
(global, { message }): StateProps => {
|
||||
const sender = selectSender(global, message);
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
> .Button {
|
||||
> .Button, > .playback-rate-menu {
|
||||
margin: -0.0625rem 0 0;
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ type OwnProps = {
|
||||
forceOpen?: boolean;
|
||||
onOpen?: NoneToVoidFunction;
|
||||
onClose?: NoneToVoidFunction;
|
||||
onHide?: NoneToVoidFunction;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
@ -27,6 +28,7 @@ const DropdownMenu: FC<OwnProps> = ({
|
||||
forceOpen,
|
||||
onOpen,
|
||||
onClose,
|
||||
onHide,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
@ -37,8 +39,10 @@ const DropdownMenu: FC<OwnProps> = ({
|
||||
const toggleIsOpen = () => {
|
||||
setIsOpen(!isOpen);
|
||||
if (isOpen) {
|
||||
if (onClose) onClose();
|
||||
} else if (onOpen) onOpen();
|
||||
onClose?.();
|
||||
} else {
|
||||
onOpen?.();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<any>) => {
|
||||
@ -58,7 +62,7 @@ const DropdownMenu: FC<OwnProps> = ({
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
if (onClose) onClose();
|
||||
onClose?.();
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
@ -80,6 +84,7 @@ const DropdownMenu: FC<OwnProps> = ({
|
||||
autoClose
|
||||
onClose={handleClose}
|
||||
shouldSkipTransition={forceOpen}
|
||||
onCloseAnimationEnd={onHide}
|
||||
>
|
||||
{children}
|
||||
</Menu>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user