diff --git a/src/components/middle/AudioPlayer.scss b/src/components/middle/AudioPlayer.scss index 21965a4fc..febdc92c4 100644 --- a/src/components/middle/AudioPlayer.scss +++ b/src/components/middle/AudioPlayer.scss @@ -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); + } } } diff --git a/src/components/middle/AudioPlayer.tsx b/src/components/middle/AudioPlayer.tsx index 54e46fd31..3e4b14e46 100644 --- a/src/components/middle/AudioPlayer.tsx +++ b/src/components/middle/AudioPlayer.tsx @@ -46,6 +46,7 @@ type StateProps = { chat?: ApiChat; volume: number; playbackRate: number; + isPlaybackRateActive?: boolean; isMuted: boolean; }; @@ -56,6 +57,10 @@ const PLAYBACK_RATES: Record = { 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 = ({ message, @@ -65,6 +70,7 @@ const AudioPlayer: FC = ({ chat, volume, playbackRate, + isPlaybackRateActive, isMuted, }) => { const { @@ -143,37 +149,62 @@ const AudioPlayer: FC = ({ 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 ( - +
+ {isContextMenuOpen &&
} + + +
); - }, [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 = ({ - + {!IS_IOS && ( - <> +
- +
)} - +
{shouldRenderPlaybackButton && ( = ({ 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); + })} )} @@ -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 ( onClick(rate)} - icon={currentRate === PLAYBACK_RATES[rate] ? 'check' : undefined} - customIcon={currentRate !== PLAYBACK_RATES[rate] ? : undefined} + icon={isSelected ? 'check' : undefined} + customIcon={!isSelected ? : undefined} > {rate}X @@ -320,13 +360,16 @@ export default withGlobal( (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, }; }, diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 531d2d7c7..c882c3882 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -46,7 +46,7 @@ export type OwnProps = { onClick?: (e: ReactMouseEvent) => void; onContextMenu?: (e: ReactMouseEvent) => void; onMouseDown?: (e: ReactMouseEvent) => void; - onMouseEnter?: NoneToVoidFunction; + onMouseEnter?: (e: ReactMouseEvent) => void; onMouseLeave?: NoneToVoidFunction; onFocus?: NoneToVoidFunction; onTransitionEnd?: NoneToVoidFunction; diff --git a/src/components/ui/DropdownMenu.tsx b/src/components/ui/DropdownMenu.tsx index 6f1e0fa96..2351fe5dd 100644 --- a/src/components/ui/DropdownMenu.tsx +++ b/src/components/ui/DropdownMenu.tsx @@ -16,6 +16,7 @@ type OwnProps = { onClose?: NoneToVoidFunction; onHide?: NoneToVoidFunction; onTransitionEnd?: NoneToVoidFunction; + onMouseEnterBackdrop?: (e: React.MouseEvent) => void; children: React.ReactNode; }; @@ -30,6 +31,7 @@ const DropdownMenu: FC = ({ onOpen, onClose, onTransitionEnd, + onMouseEnterBackdrop, onHide, }) => { // eslint-disable-next-line no-null/no-null @@ -88,6 +90,7 @@ const DropdownMenu: FC = ({ onClose={handleClose} shouldSkipTransition={forceOpen} onCloseAnimationEnd={onHide} + onMouseEnterBackdrop={onMouseEnterBackdrop} > {children} diff --git a/src/components/ui/Menu.tsx b/src/components/ui/Menu.tsx index 2b86df583..0156b94f7 100644 --- a/src/components/ui/Menu.tsx +++ b/src/components/ui/Menu.tsx @@ -40,6 +40,7 @@ type OwnProps = { onCloseAnimationEnd?: () => void; onClose: () => void; onMouseEnter?: (e: React.MouseEvent) => void; + onMouseEnterBackdrop?: (e: React.MouseEvent) => void; onMouseLeave?: (e: React.MouseEvent) => void; withPortal?: boolean; children: React.ReactNode; @@ -71,6 +72,7 @@ const Menu: FC = ({ onMouseLeave, shouldSkipTransition, withPortal, + onMouseEnterBackdrop, }) => { // eslint-disable-next-line no-null/no-null let menuRef = useRef(null); @@ -143,7 +145,11 @@ const Menu: FC = ({ > {isOpen && ( // This only prevents click events triggering on underlying elements -
+
)}
{ 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); diff --git a/src/global/types.ts b/src/global/types.ts index 258982e36..0ff7c7e4b 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -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;