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