Audio Player: Various fixes (#2376)
This commit is contained in:
parent
1c2a9a4a3c
commit
bf4e2ed994
@ -13,14 +13,14 @@
|
|||||||
margin: 0.125rem;
|
margin: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .player-button {
|
.player-button {
|
||||||
--color-text-secondary: var(--color-primary);
|
--color-text-secondary: var(--color-primary);
|
||||||
--color-text-secondary-rgb: var(--color-primary-shade-rgb);
|
--color-text-secondary-rgb: var(--color-primary-shade-rgb);
|
||||||
--color-primary-shade: var(--color-green);
|
--color-primary-shade: var(--color-green);
|
||||||
--color-white: var(--color-background-own);
|
--color-white: var(--color-background-own);
|
||||||
}
|
|
||||||
|
|
||||||
.player-button {
|
margin: 0.125rem;
|
||||||
|
|
||||||
&.smaller i {
|
&.smaller i {
|
||||||
font-size: 1.625rem;
|
font-size: 1.625rem;
|
||||||
margin-top: -0.0625rem;
|
margin-top: -0.0625rem;
|
||||||
@ -52,9 +52,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume-button {
|
.volume-button-wrapper {
|
||||||
position: relative;
|
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 {
|
.volume-slider-spacer {
|
||||||
position: absolute;
|
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 {
|
.playback-button {
|
||||||
overflow: visible;
|
&.on-top {
|
||||||
|
z-index: calc(var(--z-menu-backdrop) + 2);
|
||||||
|
}
|
||||||
|
|
||||||
&.applied {
|
&.applied {
|
||||||
--color-text-secondary: var(--color-primary);
|
--color-text-secondary: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.playback-button-inner {
|
.playback-button-inner {
|
||||||
transition: 0.2s color ease-in-out;
|
transition: 0.15s color ease-out;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
padding: 0.125rem 0.25rem;
|
padding: 0.125rem 0.25rem;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
transform: scale(0.83);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tiny {
|
||||||
|
transform: scale(0.75);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,7 @@ type StateProps = {
|
|||||||
chat?: ApiChat;
|
chat?: ApiChat;
|
||||||
volume: number;
|
volume: number;
|
||||||
playbackRate: number;
|
playbackRate: number;
|
||||||
|
isPlaybackRateActive?: boolean;
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,6 +57,10 @@ const PLAYBACK_RATES: Record<number, number> = {
|
|||||||
1.5: 1.4,
|
1.5: 1.4,
|
||||||
2: 1.8,
|
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> = ({
|
const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||||
message,
|
message,
|
||||||
@ -65,6 +70,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
|||||||
chat,
|
chat,
|
||||||
volume,
|
volume,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
|
isPlaybackRateActive,
|
||||||
isMuted,
|
isMuted,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -143,37 +149,62 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
|||||||
setAudioPlayerMuted({ isMuted: !isMuted });
|
setAudioPlayerMuted({ isMuted: !isMuted });
|
||||||
}, [isMuted, setAudioPlayerMuted, toggleMuted]);
|
}, [isMuted, setAudioPlayerMuted, toggleMuted]);
|
||||||
|
|
||||||
const updatePlaybackRate = useCallback((newRate: number) => {
|
const updatePlaybackRate = useCallback((newRate: number, isActive = true) => {
|
||||||
const rate = PLAYBACK_RATES[newRate];
|
const rate = PLAYBACK_RATES[newRate];
|
||||||
setAudioPlayerPlaybackRate({ playbackRate: rate });
|
const shouldBeActive = newRate !== REGULAR_PLAYBACK_RATE && isActive;
|
||||||
setPlaybackRate(rate);
|
setAudioPlayerPlaybackRate({ playbackRate: rate, isPlaybackRateActive: shouldBeActive });
|
||||||
|
setPlaybackRate(shouldBeActive ? rate : REGULAR_PLAYBACK_RATE);
|
||||||
}, [setAudioPlayerPlaybackRate, setPlaybackRate]);
|
}, [setAudioPlayerPlaybackRate, setPlaybackRate]);
|
||||||
|
|
||||||
const handlePlaybackClick = useCallback(() => {
|
const handlePlaybackClick = useCallback(() => {
|
||||||
if (isContextMenuOpen) return;
|
handleContextMenuClose();
|
||||||
updatePlaybackRate(playbackRate === 1 ? 2 : 1);
|
const oldRate = Number(Object.entries(PLAYBACK_RATES).find(([, rate]) => rate === playbackRate)?.[0])
|
||||||
}, [isContextMenuOpen, playbackRate, updatePlaybackRate]);
|
|| 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 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 (
|
return (
|
||||||
<Button
|
<div className="playback-wrapper">
|
||||||
round
|
{isContextMenuOpen && <div className="playback-backdrop" onClick={handleContextMenuClose} />}
|
||||||
className={buildClassName('playback-button', playbackRate !== 1 && 'applied')}
|
|
||||||
color="translucent"
|
<Button
|
||||||
size="smaller"
|
round
|
||||||
ariaLabel="Playback Rate"
|
className={buildClassName(
|
||||||
ripple={!isMobile}
|
'playback-button', isPlaybackRateActive && 'applied', isContextMenuOpen && 'on-top',
|
||||||
onClick={handlePlaybackClick}
|
)}
|
||||||
onMouseDown={handleBeforeContextMenu}
|
color="translucent"
|
||||||
onContextMenu={handleContextMenu}
|
size="smaller"
|
||||||
>
|
ariaLabel="Playback Rate"
|
||||||
<span className="playback-button-inner">
|
ripple={!isMobile}
|
||||||
{playbackRate === 1 ? 2 : displayRate}Х
|
onMouseEnter={handleContextMenu}
|
||||||
</span>
|
onClick={handlePlaybackClick}
|
||||||
</Button>
|
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(() => {
|
const volumeIcon = useMemo(() => {
|
||||||
if (volume === 0 || isMuted) return 'icon-muted';
|
if (volume === 0 || isMuted) return 'icon-muted';
|
||||||
@ -230,24 +261,28 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
|||||||
<i className="icon-skip-next" />
|
<i className="icon-skip-next" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<div className="volume-button-wrapper">
|
||||||
round
|
<Button
|
||||||
className="player-button volume-button"
|
round
|
||||||
color="translucent"
|
className="player-button volume-button"
|
||||||
size="smaller"
|
color="translucent"
|
||||||
ariaLabel="Volume"
|
size="smaller"
|
||||||
noPreventDefault
|
ariaLabel="Volume"
|
||||||
>
|
onClick={handleVolumeClick}
|
||||||
<i className={volumeIcon} onClick={handleVolumeClick} />
|
ripple={!isMobile}
|
||||||
|
>
|
||||||
|
<i className={volumeIcon} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
{!IS_IOS && (
|
{!IS_IOS && (
|
||||||
<>
|
<div className="volume-slider-wrapper">
|
||||||
<div className="volume-slider-spacer" />
|
<div className="volume-slider-spacer" />
|
||||||
<div className="volume-slider">
|
<div className="volume-slider">
|
||||||
<RangeSlider bold value={isMuted ? 0 : volume * 100} onChange={handleVolumeChange} />
|
<RangeSlider bold value={isMuted ? 0 : volume * 100} onChange={handleVolumeChange} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</div>
|
||||||
|
|
||||||
{shouldRenderPlaybackButton && (
|
{shouldRenderPlaybackButton && (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
@ -258,12 +293,11 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
|||||||
trigger={PlaybackRateButton}
|
trigger={PlaybackRateButton}
|
||||||
onClose={handleContextMenuClose}
|
onClose={handleContextMenuClose}
|
||||||
onHide={handleContextMenuHide}
|
onHide={handleContextMenuHide}
|
||||||
|
onMouseEnterBackdrop={handleContextMenuClose}
|
||||||
>
|
>
|
||||||
{renderPlaybackRateMenuItem(0.5, playbackRate, updatePlaybackRate)}
|
{PLAYBACK_RATE_VALUES.map((rate) => {
|
||||||
{renderPlaybackRateMenuItem(0.75, playbackRate, updatePlaybackRate)}
|
return renderPlaybackRateMenuItem(rate, playbackRate, updatePlaybackRate, isPlaybackRateActive);
|
||||||
{renderPlaybackRateMenuItem(1, playbackRate, updatePlaybackRate)}
|
})}
|
||||||
{renderPlaybackRateMenuItem(1.5, playbackRate, updatePlaybackRate)}
|
|
||||||
{renderPlaybackRateMenuItem(2, playbackRate, updatePlaybackRate)}
|
|
||||||
</DropdownMenu>
|
</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 (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
key={rate}
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onClick(rate)}
|
onClick={() => onClick(rate)}
|
||||||
icon={currentRate === PLAYBACK_RATES[rate] ? 'check' : undefined}
|
icon={isSelected ? 'check' : undefined}
|
||||||
customIcon={currentRate !== PLAYBACK_RATES[rate] ? <i className="icon-placeholder" /> : undefined}
|
customIcon={!isSelected ? <i className="icon-placeholder" /> : undefined}
|
||||||
>
|
>
|
||||||
{rate}X
|
{rate}X
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -320,13 +360,16 @@ export default withGlobal<OwnProps>(
|
|||||||
(global, { message }): StateProps => {
|
(global, { message }): StateProps => {
|
||||||
const sender = selectSender(global, message);
|
const sender = selectSender(global, message);
|
||||||
const chat = selectChat(global, message.chatId);
|
const chat = selectChat(global, message.chatId);
|
||||||
const { volume, playbackRate, isMuted } = selectTabState(global).audioPlayer;
|
const {
|
||||||
|
volume, playbackRate, isMuted, isPlaybackRateActive,
|
||||||
|
} = selectTabState(global).audioPlayer;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sender,
|
sender,
|
||||||
chat,
|
chat,
|
||||||
volume,
|
volume,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
|
isPlaybackRateActive,
|
||||||
isMuted,
|
isMuted,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -46,7 +46,7 @@ export type OwnProps = {
|
|||||||
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
onContextMenu?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onContextMenu?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
|
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
|
||||||
onMouseEnter?: NoneToVoidFunction;
|
onMouseEnter?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
|
||||||
onMouseLeave?: NoneToVoidFunction;
|
onMouseLeave?: NoneToVoidFunction;
|
||||||
onFocus?: NoneToVoidFunction;
|
onFocus?: NoneToVoidFunction;
|
||||||
onTransitionEnd?: NoneToVoidFunction;
|
onTransitionEnd?: NoneToVoidFunction;
|
||||||
|
|||||||
@ -16,6 +16,7 @@ type OwnProps = {
|
|||||||
onClose?: NoneToVoidFunction;
|
onClose?: NoneToVoidFunction;
|
||||||
onHide?: NoneToVoidFunction;
|
onHide?: NoneToVoidFunction;
|
||||||
onTransitionEnd?: NoneToVoidFunction;
|
onTransitionEnd?: NoneToVoidFunction;
|
||||||
|
onMouseEnterBackdrop?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ const DropdownMenu: FC<OwnProps> = ({
|
|||||||
onOpen,
|
onOpen,
|
||||||
onClose,
|
onClose,
|
||||||
onTransitionEnd,
|
onTransitionEnd,
|
||||||
|
onMouseEnterBackdrop,
|
||||||
onHide,
|
onHide,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -88,6 +90,7 @@ const DropdownMenu: FC<OwnProps> = ({
|
|||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
shouldSkipTransition={forceOpen}
|
shouldSkipTransition={forceOpen}
|
||||||
onCloseAnimationEnd={onHide}
|
onCloseAnimationEnd={onHide}
|
||||||
|
onMouseEnterBackdrop={onMouseEnterBackdrop}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@ -40,6 +40,7 @@ type OwnProps = {
|
|||||||
onCloseAnimationEnd?: () => void;
|
onCloseAnimationEnd?: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onMouseEnter?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
onMouseEnter?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
|
onMouseEnterBackdrop?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
withPortal?: boolean;
|
withPortal?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -71,6 +72,7 @@ const Menu: FC<OwnProps> = ({
|
|||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
shouldSkipTransition,
|
shouldSkipTransition,
|
||||||
withPortal,
|
withPortal,
|
||||||
|
onMouseEnterBackdrop,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
let menuRef = useRef<HTMLDivElement>(null);
|
let menuRef = useRef<HTMLDivElement>(null);
|
||||||
@ -143,7 +145,11 @@ const Menu: FC<OwnProps> = ({
|
|||||||
>
|
>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
// This only prevents click events triggering on underlying elements
|
// This only prevents click events triggering on underlying elements
|
||||||
<div className="backdrop" onMouseDown={preventMessageInputBlurWithBubbling} />
|
<div
|
||||||
|
className="backdrop"
|
||||||
|
onMouseDown={preventMessageInputBlurWithBubbling}
|
||||||
|
onMouseEnter={onMouseEnterBackdrop}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
role="presentation"
|
role="presentation"
|
||||||
|
|||||||
@ -44,5 +44,6 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
animation: ripple-animation 700ms;
|
animation: ripple-animation 700ms;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -212,13 +212,14 @@ addActionHandler('setAudioPlayerVolume', (global, actions, payload): ActionRetur
|
|||||||
|
|
||||||
addActionHandler('setAudioPlayerPlaybackRate', (global, actions, payload): ActionReturnType => {
|
addActionHandler('setAudioPlayerPlaybackRate', (global, actions, payload): ActionReturnType => {
|
||||||
const {
|
const {
|
||||||
playbackRate, tabId = getCurrentTabId(),
|
playbackRate, isPlaybackRateActive, tabId = getCurrentTabId(),
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
return updateTabState(global, {
|
return updateTabState(global, {
|
||||||
audioPlayer: {
|
audioPlayer: {
|
||||||
...selectTabState(global, tabId).audioPlayer,
|
...selectTabState(global, tabId).audioPlayer,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
|
isPlaybackRateActive,
|
||||||
},
|
},
|
||||||
}, tabId);
|
}, tabId);
|
||||||
});
|
});
|
||||||
@ -256,6 +257,7 @@ addActionHandler('closeAudioPlayer', (global, actions, payload): ActionReturnTyp
|
|||||||
audioPlayer: {
|
audioPlayer: {
|
||||||
volume: tabState.audioPlayer.volume,
|
volume: tabState.audioPlayer.volume,
|
||||||
playbackRate: tabState.audioPlayer.playbackRate,
|
playbackRate: tabState.audioPlayer.playbackRate,
|
||||||
|
isPlaybackRateActive: undefined,
|
||||||
isMuted: tabState.audioPlayer.isMuted,
|
isMuted: tabState.audioPlayer.isMuted,
|
||||||
},
|
},
|
||||||
}, tabId);
|
}, tabId);
|
||||||
|
|||||||
@ -321,6 +321,7 @@ export type TabState = {
|
|||||||
origin?: AudioOrigin;
|
origin?: AudioOrigin;
|
||||||
volume: number;
|
volume: number;
|
||||||
playbackRate: number;
|
playbackRate: number;
|
||||||
|
isPlaybackRateActive?: boolean;
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1708,6 +1709,7 @@ export interface ActionPayloads {
|
|||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
setAudioPlayerPlaybackRate: {
|
setAudioPlayerPlaybackRate: {
|
||||||
playbackRate: number;
|
playbackRate: number;
|
||||||
|
isPlaybackRateActive?: boolean;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
setAudioPlayerMuted: {
|
setAudioPlayerMuted: {
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user