Audio Player: Various fixes (#2376)

This commit is contained in:
Alexander Zinchuk 2023-01-28 02:16:41 +01:00
parent 1c2a9a4a3c
commit bf4e2ed994
8 changed files with 141 additions and 54 deletions

View File

@ -13,14 +13,14 @@
margin: 0.125rem;
}
> .player-button {
.player-button {
--color-text-secondary: var(--color-primary);
--color-text-secondary-rgb: var(--color-primary-shade-rgb);
--color-primary-shade: var(--color-green);
--color-white: var(--color-background-own);
}
.player-button {
margin: 0.125rem;
&.smaller i {
font-size: 1.625rem;
margin-top: -0.0625rem;
@ -52,9 +52,17 @@
}
}
.volume-button {
.volume-button-wrapper {
position: relative;
overflow: visible;
display: flex;
flex-direction: column;
align-items: center;
.volume-slider-wrapper {
position: absolute;
width: 8rem;
top: 2.625rem;
}
.volume-slider-spacer {
position: absolute;
@ -102,21 +110,43 @@
}
}
.playback-wrapper {
position: relative;
}
.playback-backdrop {
position: absolute;
top: 0;
right: 0;
width: 5rem;
height: 4rem;
z-index: calc(var(--z-menu-backdrop) + 1);
}
.playback-button {
overflow: visible;
&.on-top {
z-index: calc(var(--z-menu-backdrop) + 2);
}
&.applied {
--color-text-secondary: var(--color-primary);
}
.playback-button-inner {
transition: 0.2s color ease-in-out;
transition: 0.15s color ease-out;
font-size: 0.75rem;
font-weight: bold;
border: 2px solid;
border-radius: 0.375rem;
padding: 0.125rem 0.25rem;
font-variant-numeric: tabular-nums;
&.small {
transform: scale(0.83);
}
&.tiny {
transform: scale(0.75);
}
}
}

View File

@ -46,6 +46,7 @@ type StateProps = {
chat?: ApiChat;
volume: number;
playbackRate: number;
isPlaybackRateActive?: boolean;
isMuted: boolean;
};
@ -56,6 +57,10 @@ const PLAYBACK_RATES: Record<number, number> = {
1.5: 1.4,
2: 1.8,
};
const PLAYBACK_RATE_VALUES = Object.keys(PLAYBACK_RATES).sort().map(Number);
const REGULAR_PLAYBACK_RATE = 1;
const DEFAULT_FAST_PLAYBACK_RATE = 2;
const AudioPlayer: FC<OwnProps & StateProps> = ({
message,
@ -65,6 +70,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
chat,
volume,
playbackRate,
isPlaybackRateActive,
isMuted,
}) => {
const {
@ -143,37 +149,62 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
setAudioPlayerMuted({ isMuted: !isMuted });
}, [isMuted, setAudioPlayerMuted, toggleMuted]);
const updatePlaybackRate = useCallback((newRate: number) => {
const updatePlaybackRate = useCallback((newRate: number, isActive = true) => {
const rate = PLAYBACK_RATES[newRate];
setAudioPlayerPlaybackRate({ playbackRate: rate });
setPlaybackRate(rate);
const shouldBeActive = newRate !== REGULAR_PLAYBACK_RATE && isActive;
setAudioPlayerPlaybackRate({ playbackRate: rate, isPlaybackRateActive: shouldBeActive });
setPlaybackRate(shouldBeActive ? rate : REGULAR_PLAYBACK_RATE);
}, [setAudioPlayerPlaybackRate, setPlaybackRate]);
const handlePlaybackClick = useCallback(() => {
if (isContextMenuOpen) return;
updatePlaybackRate(playbackRate === 1 ? 2 : 1);
}, [isContextMenuOpen, playbackRate, updatePlaybackRate]);
handleContextMenuClose();
const oldRate = Number(Object.entries(PLAYBACK_RATES).find(([, rate]) => rate === playbackRate)?.[0])
|| REGULAR_PLAYBACK_RATE;
const newIsActive = !isPlaybackRateActive;
updatePlaybackRate(
newIsActive && oldRate === REGULAR_PLAYBACK_RATE ? DEFAULT_FAST_PLAYBACK_RATE : oldRate,
newIsActive,
);
}, [handleContextMenuClose, isPlaybackRateActive, playbackRate, updatePlaybackRate]);
const PlaybackRateButton = useCallback(() => {
const displayRate = Object.entries(PLAYBACK_RATES).find(([, rate]) => rate === playbackRate)?.[0] || 1;
const displayRate = Object.entries(PLAYBACK_RATES).find(([, rate]) => rate === playbackRate)?.[0]
|| REGULAR_PLAYBACK_RATE;
const text = `${playbackRate === REGULAR_PLAYBACK_RATE ? DEFAULT_FAST_PLAYBACK_RATE : displayRate}Х`;
return (
<Button
round
className={buildClassName('playback-button', playbackRate !== 1 && 'applied')}
color="translucent"
size="smaller"
ariaLabel="Playback Rate"
ripple={!isMobile}
onClick={handlePlaybackClick}
onMouseDown={handleBeforeContextMenu}
onContextMenu={handleContextMenu}
>
<span className="playback-button-inner">
{playbackRate === 1 ? 2 : displayRate}Х
</span>
</Button>
<div className="playback-wrapper">
{isContextMenuOpen && <div className="playback-backdrop" onClick={handleContextMenuClose} />}
<Button
round
className={buildClassName(
'playback-button', isPlaybackRateActive && 'applied', isContextMenuOpen && 'on-top',
)}
color="translucent"
size="smaller"
ariaLabel="Playback Rate"
ripple={!isMobile}
onMouseEnter={handleContextMenu}
onClick={handlePlaybackClick}
onMouseDown={handleBeforeContextMenu}
onContextMenu={handleContextMenu}
>
<span className={buildClassName(
'playback-button-inner',
text.length === 4 && 'small',
text.length === 5 && 'tiny',
)}
>
{text}
</span>
</Button>
</div>
);
}, [handleBeforeContextMenu, handleContextMenu, handlePlaybackClick, isMobile, playbackRate]);
}, [
handleBeforeContextMenu, handleContextMenu, handleContextMenuClose, handlePlaybackClick, isContextMenuOpen,
isMobile, isPlaybackRateActive, playbackRate,
]);
const volumeIcon = useMemo(() => {
if (volume === 0 || isMuted) return 'icon-muted';
@ -230,24 +261,28 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
<i className="icon-skip-next" />
</Button>
<Button
round
className="player-button volume-button"
color="translucent"
size="smaller"
ariaLabel="Volume"
noPreventDefault
>
<i className={volumeIcon} onClick={handleVolumeClick} />
<div className="volume-button-wrapper">
<Button
round
className="player-button volume-button"
color="translucent"
size="smaller"
ariaLabel="Volume"
onClick={handleVolumeClick}
ripple={!isMobile}
>
<i className={volumeIcon} />
</Button>
{!IS_IOS && (
<>
<div className="volume-slider-wrapper">
<div className="volume-slider-spacer" />
<div className="volume-slider">
<RangeSlider bold value={isMuted ? 0 : volume * 100} onChange={handleVolumeChange} />
</div>
</>
</div>
)}
</Button>
</div>
{shouldRenderPlaybackButton && (
<DropdownMenu
@ -258,12 +293,11 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
trigger={PlaybackRateButton}
onClose={handleContextMenuClose}
onHide={handleContextMenuHide}
onMouseEnterBackdrop={handleContextMenuClose}
>
{renderPlaybackRateMenuItem(0.5, playbackRate, updatePlaybackRate)}
{renderPlaybackRateMenuItem(0.75, playbackRate, updatePlaybackRate)}
{renderPlaybackRateMenuItem(1, playbackRate, updatePlaybackRate)}
{renderPlaybackRateMenuItem(1.5, playbackRate, updatePlaybackRate)}
{renderPlaybackRateMenuItem(2, playbackRate, updatePlaybackRate)}
{PLAYBACK_RATE_VALUES.map((rate) => {
return renderPlaybackRateMenuItem(rate, playbackRate, updatePlaybackRate, isPlaybackRateActive);
})}
</DropdownMenu>
)}
@ -303,13 +337,19 @@ function renderVoice(subtitle: string, senderName?: string) {
);
}
function renderPlaybackRateMenuItem(rate: number, currentRate: number, onClick: (rate: number) => void) {
function renderPlaybackRateMenuItem(
rate: number, currentRate: number, onClick: (rate: number) => void,
isPlaybackRateActive?: boolean,
) {
const isSelected = (currentRate === PLAYBACK_RATES[rate] && isPlaybackRateActive)
|| (rate === REGULAR_PLAYBACK_RATE && !isPlaybackRateActive);
return (
<MenuItem
key={rate}
// 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}
icon={isSelected ? 'check' : undefined}
customIcon={!isSelected ? <i className="icon-placeholder" /> : undefined}
>
{rate}X
</MenuItem>
@ -320,13 +360,16 @@ export default withGlobal<OwnProps>(
(global, { message }): StateProps => {
const sender = selectSender(global, message);
const chat = selectChat(global, message.chatId);
const { volume, playbackRate, isMuted } = selectTabState(global).audioPlayer;
const {
volume, playbackRate, isMuted, isPlaybackRateActive,
} = selectTabState(global).audioPlayer;
return {
sender,
chat,
volume,
playbackRate,
isPlaybackRateActive,
isMuted,
};
},

View File

@ -46,7 +46,7 @@ export type OwnProps = {
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
onContextMenu?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
onMouseEnter?: NoneToVoidFunction;
onMouseEnter?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
onMouseLeave?: NoneToVoidFunction;
onFocus?: NoneToVoidFunction;
onTransitionEnd?: NoneToVoidFunction;

View File

@ -16,6 +16,7 @@ type OwnProps = {
onClose?: NoneToVoidFunction;
onHide?: NoneToVoidFunction;
onTransitionEnd?: NoneToVoidFunction;
onMouseEnterBackdrop?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
children: React.ReactNode;
};
@ -30,6 +31,7 @@ const DropdownMenu: FC<OwnProps> = ({
onOpen,
onClose,
onTransitionEnd,
onMouseEnterBackdrop,
onHide,
}) => {
// eslint-disable-next-line no-null/no-null
@ -88,6 +90,7 @@ const DropdownMenu: FC<OwnProps> = ({
onClose={handleClose}
shouldSkipTransition={forceOpen}
onCloseAnimationEnd={onHide}
onMouseEnterBackdrop={onMouseEnterBackdrop}
>
{children}
</Menu>

View File

@ -40,6 +40,7 @@ type OwnProps = {
onCloseAnimationEnd?: () => void;
onClose: () => void;
onMouseEnter?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
onMouseEnterBackdrop?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
withPortal?: boolean;
children: React.ReactNode;
@ -71,6 +72,7 @@ const Menu: FC<OwnProps> = ({
onMouseLeave,
shouldSkipTransition,
withPortal,
onMouseEnterBackdrop,
}) => {
// eslint-disable-next-line no-null/no-null
let menuRef = useRef<HTMLDivElement>(null);
@ -143,7 +145,11 @@ const Menu: FC<OwnProps> = ({
>
{isOpen && (
// This only prevents click events triggering on underlying elements
<div className="backdrop" onMouseDown={preventMessageInputBlurWithBubbling} />
<div
className="backdrop"
onMouseDown={preventMessageInputBlurWithBubbling}
onMouseEnter={onMouseEnterBackdrop}
/>
)}
<div
role="presentation"

View File

@ -44,5 +44,6 @@
border-radius: 50%;
transform: scale(0);
animation: ripple-animation 700ms;
pointer-events: none;
}
}

View File

@ -212,13 +212,14 @@ addActionHandler('setAudioPlayerVolume', (global, actions, payload): ActionRetur
addActionHandler('setAudioPlayerPlaybackRate', (global, actions, payload): ActionReturnType => {
const {
playbackRate, tabId = getCurrentTabId(),
playbackRate, isPlaybackRateActive, tabId = getCurrentTabId(),
} = payload;
return updateTabState(global, {
audioPlayer: {
...selectTabState(global, tabId).audioPlayer,
playbackRate,
isPlaybackRateActive,
},
}, tabId);
});
@ -256,6 +257,7 @@ addActionHandler('closeAudioPlayer', (global, actions, payload): ActionReturnTyp
audioPlayer: {
volume: tabState.audioPlayer.volume,
playbackRate: tabState.audioPlayer.playbackRate,
isPlaybackRateActive: undefined,
isMuted: tabState.audioPlayer.isMuted,
},
}, tabId);

View File

@ -321,6 +321,7 @@ export type TabState = {
origin?: AudioOrigin;
volume: number;
playbackRate: number;
isPlaybackRateActive?: boolean;
isMuted: boolean;
};
@ -1708,6 +1709,7 @@ export interface ActionPayloads {
} & WithTabId;
setAudioPlayerPlaybackRate: {
playbackRate: number;
isPlaybackRateActive?: boolean;
} & WithTabId;
setAudioPlayerMuted: {
isMuted: boolean;