Audio Player: More playback speed options (#2164)

This commit is contained in:
Alexander Zinchuk 2022-12-06 13:29:41 +01:00
parent d0055d6653
commit 28d87bd580
4 changed files with 107 additions and 36 deletions

View File

@ -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;
}
}
}

View File

@ -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);

View File

@ -51,7 +51,7 @@
flex-grow: 1;
}
> .Button {
> .Button, > .playback-rate-menu {
margin: -0.0625rem 0 0;
}

View File

@ -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>