Media Viewer: Add volume and playback rate controls (#1768)
This commit is contained in:
parent
36bdd5dd97
commit
12f5acb7e0
@ -75,19 +75,16 @@
|
||||
|
||||
.theme-dark body.initial & {
|
||||
background-color: #0f0f0f;
|
||||
background-image: url('../../assets/chat-bg-dark.png');
|
||||
background-position: top left;
|
||||
background-size: 650px;
|
||||
background-repeat: repeat;
|
||||
}
|
||||
|
||||
.theme-light body.initial &,
|
||||
body:not(.initial) & {
|
||||
background-image: url('../../assets/chat-bg.jpg');
|
||||
}
|
||||
|
||||
.theme-dark body.initial & {
|
||||
background-image: url('../../assets/chat-bg-dark.png');
|
||||
background-position: top left;
|
||||
background-size: 650px;
|
||||
background-repeat: repeat;
|
||||
}
|
||||
}
|
||||
|
||||
html.theme-light body.animation-level-2 &.with-right-column::before {
|
||||
|
||||
@ -64,6 +64,9 @@ type StateProps = {
|
||||
message?: ApiMessage;
|
||||
origin?: MediaViewerOrigin;
|
||||
isProtected?: boolean;
|
||||
volume: number;
|
||||
isMuted: boolean;
|
||||
playbackRate: number;
|
||||
};
|
||||
|
||||
const ANIMATION_DURATION = 350;
|
||||
@ -78,10 +81,13 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
profilePhotoIndex,
|
||||
origin,
|
||||
animationLevel,
|
||||
onClose,
|
||||
onFooterClick,
|
||||
isFooterHidden,
|
||||
isProtected,
|
||||
volume,
|
||||
playbackRate,
|
||||
isMuted,
|
||||
onClose,
|
||||
onFooterClick,
|
||||
setIsFooterHidden,
|
||||
} = props;
|
||||
/* Content */
|
||||
@ -211,6 +217,9 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
toggleControls={toggleControls}
|
||||
noPlay={!isActive}
|
||||
onClose={onClose}
|
||||
isMuted={isMuted}
|
||||
volume={volume}
|
||||
playbackRate={playbackRate}
|
||||
/>
|
||||
))}
|
||||
{textParts && (
|
||||
@ -236,14 +245,20 @@ export default memo(withGlobal<OwnProps>(
|
||||
origin,
|
||||
} = ownProps;
|
||||
|
||||
const {
|
||||
volume,
|
||||
isMuted,
|
||||
playbackRate,
|
||||
} = global.mediaViewer;
|
||||
|
||||
if (origin === MediaViewerOrigin.SearchResult) {
|
||||
if (!(chatId && messageId)) {
|
||||
return {};
|
||||
return { volume, isMuted, playbackRate };
|
||||
}
|
||||
|
||||
const message = selectChatMessage(global, chatId, messageId);
|
||||
if (!message) {
|
||||
return {};
|
||||
return { volume, isMuted, playbackRate };
|
||||
}
|
||||
|
||||
return {
|
||||
@ -253,6 +268,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
origin,
|
||||
message,
|
||||
isProtected: selectIsMessageProtected(global, message),
|
||||
volume,
|
||||
isMuted,
|
||||
playbackRate,
|
||||
};
|
||||
}
|
||||
|
||||
@ -265,11 +283,14 @@ export default memo(withGlobal<OwnProps>(
|
||||
avatarOwner: sender,
|
||||
profilePhotoIndex: profilePhotoIndex || 0,
|
||||
origin,
|
||||
volume,
|
||||
isMuted,
|
||||
playbackRate,
|
||||
};
|
||||
}
|
||||
|
||||
if (!(chatId && threadId && messageId)) {
|
||||
return {};
|
||||
return { volume, isMuted, playbackRate };
|
||||
}
|
||||
|
||||
let message: ApiMessage | undefined;
|
||||
@ -280,7 +301,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
return {};
|
||||
return { volume, isMuted, playbackRate };
|
||||
}
|
||||
|
||||
return {
|
||||
@ -291,6 +312,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
origin,
|
||||
message,
|
||||
isProtected: selectIsMessageProtected(global, message),
|
||||
volume,
|
||||
isMuted,
|
||||
playbackRate,
|
||||
};
|
||||
},
|
||||
)(MediaViewerContent));
|
||||
|
||||
@ -567,7 +567,10 @@ function checkIfInsideSelector(element: HTMLElement, selector: string) {
|
||||
function checkIfControlTarget(e: TouchEvent | MouseEvent) {
|
||||
const target = e.target as HTMLElement;
|
||||
if (checkIfInsideSelector(target, '.VideoPlayerControls')) {
|
||||
if (checkIfInsideSelector(target, '.play, .fullscreen')) {
|
||||
if (checkIfInsideSelector(
|
||||
target,
|
||||
'.play, .fullscreen, .volume, .volume-slider, .playback-rate, .playback-rate-menu',
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, {
|
||||
FC, memo, useCallback, useEffect, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import { ApiDimensions } from '../../api/types';
|
||||
|
||||
@ -28,6 +29,9 @@ type OwnProps = {
|
||||
isMediaViewerOpen?: boolean;
|
||||
noPlay?: boolean;
|
||||
areControlsVisible: boolean;
|
||||
volume: number;
|
||||
isMuted: boolean;
|
||||
playbackRate: number;
|
||||
toggleControls: (isVisible: boolean) => void;
|
||||
onClose: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
};
|
||||
@ -43,10 +47,18 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
fileSize,
|
||||
isMediaViewerOpen,
|
||||
noPlay,
|
||||
volume,
|
||||
isMuted,
|
||||
playbackRate,
|
||||
onClose,
|
||||
toggleControls,
|
||||
areControlsVisible,
|
||||
}) => {
|
||||
const {
|
||||
setMediaViewerVolume,
|
||||
setMediaViewerMuted,
|
||||
setMediaViewerPlaybackRate,
|
||||
} = getActions();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const [isPlayed, setIsPlayed] = useState(!IS_TOUCH_ENV || !IS_IOS);
|
||||
@ -84,6 +96,14 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
}
|
||||
}, [currentTime]);
|
||||
|
||||
useEffect(() => {
|
||||
videoRef.current!.volume = volume;
|
||||
}, [volume]);
|
||||
|
||||
useEffect(() => {
|
||||
videoRef.current!.playbackRate = playbackRate;
|
||||
}, [playbackRate]);
|
||||
|
||||
const togglePlayState = useCallback((e: React.MouseEvent<HTMLElement, MouseEvent> | KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
if (isPlayed) {
|
||||
@ -129,6 +149,18 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
videoRef.current!.currentTime = position;
|
||||
}, []);
|
||||
|
||||
const handleVolumeChange = useCallback((newVolume: number) => {
|
||||
setMediaViewerVolume({ volume: newVolume / 100 });
|
||||
}, [setMediaViewerVolume]);
|
||||
|
||||
const handleVolumeMuted = useCallback(() => {
|
||||
setMediaViewerMuted({ isMuted: !isMuted });
|
||||
}, [isMuted, setMediaViewerMuted]);
|
||||
|
||||
const handlePlaybackRateChange = useCallback((newPlaybackRate: number) => {
|
||||
setMediaViewerPlaybackRate({ playbackRate: newPlaybackRate });
|
||||
}, [setMediaViewerPlaybackRate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMediaViewerOpen) return undefined;
|
||||
const togglePayingStateBySpace = (e: KeyboardEvent) => {
|
||||
@ -164,7 +196,7 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
playsInline
|
||||
loop={isGif}
|
||||
// This is to force auto playing on mobiles
|
||||
muted={isGif}
|
||||
muted={isGif || isMuted}
|
||||
id="media-viewer-video"
|
||||
style={videoStyle}
|
||||
onPlay={IS_IOS ? () => setIsPlayed(true) : undefined}
|
||||
@ -198,6 +230,7 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
<VideoPlayerControls
|
||||
isPlayed={isPlayed}
|
||||
bufferedProgress={bufferedProgress}
|
||||
isBuffered={isBuffered}
|
||||
currentTime={currentTime}
|
||||
isFullscreenSupported={Boolean(setFullscreen)}
|
||||
isFullscreen={isFullscreen}
|
||||
@ -209,6 +242,12 @@ const VideoPlayer: FC<OwnProps> = ({
|
||||
onSeek={handleSeek}
|
||||
onChangeFullscreen={handleFullscreenChange}
|
||||
onPlayPause={togglePlayState}
|
||||
volume={volume}
|
||||
playbackRate={playbackRate}
|
||||
isMuted={isMuted}
|
||||
onVolumeClick={handleVolumeMuted}
|
||||
onVolumeChange={handleVolumeChange}
|
||||
onPlaybackRateChange={handlePlaybackRateChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -28,77 +28,67 @@
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
.player-file-size {
|
||||
position: static;
|
||||
transform: none;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.player-time + .fullscreen {
|
||||
margin-left: auto;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.Button {
|
||||
width: 2.25rem;
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.Button.round {
|
||||
width: 2rem;
|
||||
padding: 0;
|
||||
margin: 0.25rem;
|
||||
height: 1.75rem;
|
||||
@media (max-width: 600px) {
|
||||
height: 2.25rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
margin-bottom: 0;
|
||||
margin-left: -0.75rem;
|
||||
padding: 0.5rem 0.5rem 0.5rem 0.5rem;
|
||||
width: 0;
|
||||
|
||||
--volume-slider-width: 4rem;
|
||||
--slider-color: #fff;
|
||||
--color-borders: rgba(255, 255, 255, 0.5);
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
transition: width 0.2s ease-in-out;
|
||||
|
||||
&:hover{
|
||||
overflow: hidden;
|
||||
width: var(--volume-slider-width);
|
||||
.RangeSlider__input {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.RangeSlider__input {
|
||||
margin-bottom: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.play i {
|
||||
line-height: 1.5rem;
|
||||
.volume:hover + .volume-slider {
|
||||
overflow: hidden;
|
||||
width: var(--volume-slider-width);
|
||||
.RangeSlider__input {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.player-time {
|
||||
margin: 0 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
margin-left: auto;
|
||||
@media (max-width: 600px) {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
i {
|
||||
line-height: 1.75rem;
|
||||
@media (max-width: 600px) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.player-time + .fullscreen {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.player-file-size {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
position: static;
|
||||
transform: none;
|
||||
margin-left: auto;
|
||||
margin-right: 1rem;
|
||||
& + .fullscreen {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.player-seekline {
|
||||
@ -155,4 +145,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playback-rate-menu .bubble {
|
||||
min-width: 4rem;
|
||||
margin-right: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,23 @@
|
||||
import React, {
|
||||
FC, useEffect, useRef, useCallback,
|
||||
FC, useEffect, useRef, useCallback, useMemo,
|
||||
} from '../../lib/teact/teact';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import { formatMediaDuration } from '../../util/dateFormat';
|
||||
import formatFileSize from './helpers/formatFileSize';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import { captureEvents } from '../../util/captureEvents';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import RangeSlider from '../ui/RangeSlider';
|
||||
import Menu from '../ui/Menu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
|
||||
import './VideoPlayerControls.scss';
|
||||
|
||||
type IProps = {
|
||||
type OwnProps = {
|
||||
bufferedProgress: number;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
@ -22,9 +26,16 @@ type IProps = {
|
||||
isPlayed: boolean;
|
||||
isFullscreenSupported: boolean;
|
||||
isFullscreen: boolean;
|
||||
onChangeFullscreen: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onPlayPause: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
isVisible: boolean;
|
||||
isBuffered: boolean;
|
||||
volume: number;
|
||||
isMuted: boolean;
|
||||
playbackRate: number;
|
||||
onChangeFullscreen: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onVolumeClick: () => void;
|
||||
onVolumeChange: (volume: number) => void;
|
||||
onPlaybackRateChange: (playbackRate: number) => void;
|
||||
onPlayPause: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
setVisibility: (isVisible: boolean) => void;
|
||||
onSeek: (position: number) => void;
|
||||
};
|
||||
@ -33,9 +44,16 @@ const stopEvent = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const PLAYBACK_RATES = [
|
||||
0.5,
|
||||
1,
|
||||
1.5,
|
||||
2,
|
||||
];
|
||||
|
||||
const HIDE_CONTROLS_TIMEOUT_MS = 1500;
|
||||
|
||||
const VideoPlayerControls: FC<IProps> = ({
|
||||
const VideoPlayerControls: FC<OwnProps> = ({
|
||||
bufferedProgress,
|
||||
currentTime,
|
||||
duration,
|
||||
@ -44,12 +62,20 @@ const VideoPlayerControls: FC<IProps> = ({
|
||||
isPlayed,
|
||||
isFullscreenSupported,
|
||||
isFullscreen,
|
||||
onChangeFullscreen,
|
||||
onPlayPause,
|
||||
isVisible,
|
||||
isBuffered,
|
||||
volume,
|
||||
isMuted,
|
||||
playbackRate,
|
||||
onChangeFullscreen,
|
||||
onVolumeClick,
|
||||
onVolumeChange,
|
||||
onPlaybackRateChange,
|
||||
onPlayPause,
|
||||
setVisibility,
|
||||
onSeek,
|
||||
}) => {
|
||||
const [isPlaybackMenuOpen, openPlaybackMenu, closePlaybackMenu] = useFlag();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const seekerRef = useRef<HTMLDivElement>(null);
|
||||
const isSeekingRef = useRef<boolean>(false);
|
||||
@ -57,7 +83,7 @@ const VideoPlayerControls: FC<IProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: number | undefined;
|
||||
if (!isVisible || !isPlayed || isSeeking) {
|
||||
if (!isVisible || !isPlayed || isSeeking || isPlaybackMenuOpen) {
|
||||
if (timeout) window.clearTimeout(timeout);
|
||||
return undefined;
|
||||
}
|
||||
@ -67,7 +93,7 @@ const VideoPlayerControls: FC<IProps> = ({
|
||||
return () => {
|
||||
if (timeout) window.clearTimeout(timeout);
|
||||
};
|
||||
}, [isPlayed, isVisible, isSeeking, setVisibility]);
|
||||
}, [isPlayed, isVisible, isSeeking, setVisibility, isPlaybackMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
@ -80,6 +106,12 @@ const VideoPlayerControls: FC<IProps> = ({
|
||||
};
|
||||
}, [isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
closePlaybackMenu();
|
||||
}
|
||||
}, [closePlaybackMenu, isVisible]);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const handleSeek = useCallback((e: MouseEvent | TouchEvent) => {
|
||||
@ -112,35 +144,84 @@ const VideoPlayerControls: FC<IProps> = ({
|
||||
});
|
||||
}, [isVisible, handleStartSeek, handleSeek, handleStopSeek]);
|
||||
|
||||
const volumeIcon = useMemo(() => {
|
||||
if (volume === 0 || isMuted) return 'icon-muted';
|
||||
if (volume < 0.3) return 'icon-volume-1';
|
||||
if (volume < 0.6) return 'icon-volume-2';
|
||||
return 'icon-volume-3';
|
||||
}, [volume, isMuted]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('VideoPlayerControls', isForceMobileVersion && 'mobile', isVisible && 'active')}
|
||||
onClick={stopEvent}
|
||||
>
|
||||
{renderSeekLine(currentTime, duration, bufferedProgress, seekerRef)}
|
||||
<Button
|
||||
ariaLabel={lang('AccActionPlay')}
|
||||
size="tiny"
|
||||
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||||
color="translucent-white"
|
||||
className="play"
|
||||
onClick={onPlayPause}
|
||||
>
|
||||
<i className={isPlayed ? 'icon-pause' : 'icon-play'} />
|
||||
</Button>
|
||||
{renderTime(currentTime, duration)}
|
||||
{bufferedProgress < 1 && renderFileSize(bufferedProgress, fileSize)}
|
||||
{isFullscreenSupported && (
|
||||
<div className="buttons">
|
||||
<Button
|
||||
ariaLabel="Fullscreen"
|
||||
ariaLabel={lang('AccActionPlay')}
|
||||
size="tiny"
|
||||
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||||
color="translucent-white"
|
||||
className="play"
|
||||
round
|
||||
onClick={onPlayPause}
|
||||
>
|
||||
<i className={isPlayed ? 'icon-pause' : 'icon-play'} />
|
||||
</Button>
|
||||
<Button
|
||||
ariaLabel="Volume"
|
||||
size="tiny"
|
||||
color="translucent-white"
|
||||
className="fullscreen"
|
||||
onClick={onChangeFullscreen}
|
||||
className="volume"
|
||||
round
|
||||
onClick={onVolumeClick}
|
||||
>
|
||||
<i className={`${isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen'}`} />
|
||||
<i className={volumeIcon} />
|
||||
</Button>
|
||||
)}
|
||||
{!IS_IOS && (
|
||||
<RangeSlider bold className="volume-slider" value={isMuted ? 0 : volume * 100} onChange={onVolumeChange} />
|
||||
)}
|
||||
{renderTime(currentTime, duration)}
|
||||
{!isBuffered && renderFileSize(bufferedProgress, fileSize)}
|
||||
<div className="spacer" />
|
||||
<Button
|
||||
ariaLabel="Playback rate"
|
||||
size="tiny"
|
||||
color="translucent-white"
|
||||
className="playback-rate"
|
||||
round
|
||||
onClick={openPlaybackMenu}
|
||||
>
|
||||
{`${playbackRate}x`}
|
||||
</Button>
|
||||
{isFullscreenSupported && (
|
||||
<Button
|
||||
ariaLabel="Fullscreen"
|
||||
size="tiny"
|
||||
color="translucent-white"
|
||||
className="fullscreen"
|
||||
round
|
||||
onClick={onChangeFullscreen}
|
||||
>
|
||||
<i className={isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen'} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Menu
|
||||
isOpen={isPlaybackMenuOpen}
|
||||
className="playback-rate-menu"
|
||||
positionX="right"
|
||||
positionY="bottom"
|
||||
autoClose
|
||||
onClose={closePlaybackMenu}
|
||||
>
|
||||
{PLAYBACK_RATES.map((rate) => (
|
||||
<MenuItem disabled={playbackRate === rate} onClick={() => onPlaybackRateChange(rate)}>
|
||||
{`${rate}x`}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -216,7 +216,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
<>
|
||||
<div className="volume-slider-spacer" />
|
||||
<div className="volume-slider">
|
||||
<RangeSlider value={isMuted ? 0 : volume * 100} onChange={handleVolumeChange} />
|
||||
<RangeSlider bold value={isMuted ? 0 : volume * 100} onChange={handleVolumeChange} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -73,6 +73,18 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.bold {
|
||||
.slider-main::before {
|
||||
top: 0.25rem;
|
||||
height: 0.25rem;
|
||||
}
|
||||
|
||||
.slider-fill-track {
|
||||
top: 0.25rem;
|
||||
height: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset range input browser styles
|
||||
@include reset-range();
|
||||
|
||||
|
||||
@ -15,8 +15,10 @@ type OwnProps = {
|
||||
step?: number;
|
||||
label?: string;
|
||||
value: number;
|
||||
renderValue?: (value: number) => string;
|
||||
disabled?: boolean;
|
||||
bold?: boolean;
|
||||
className?: string;
|
||||
renderValue?: (value: number) => string;
|
||||
onChange: (value: number) => void;
|
||||
};
|
||||
|
||||
@ -27,8 +29,10 @@ const RangeSlider: FC<OwnProps> = ({
|
||||
step = 1,
|
||||
label,
|
||||
value,
|
||||
renderValue,
|
||||
disabled,
|
||||
bold,
|
||||
className,
|
||||
renderValue,
|
||||
onChange,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
@ -36,9 +40,11 @@ const RangeSlider: FC<OwnProps> = ({
|
||||
onChange(Number(event.currentTarget.value));
|
||||
}, [onChange]);
|
||||
|
||||
const className = buildClassName(
|
||||
const mainClassName = buildClassName(
|
||||
className,
|
||||
'RangeSlider',
|
||||
disabled && 'disabled',
|
||||
bold && 'bold',
|
||||
);
|
||||
|
||||
const trackWidth = useMemo(() => {
|
||||
@ -51,7 +57,7 @@ const RangeSlider: FC<OwnProps> = ({
|
||||
}, [options, value, max, min, step]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={mainClassName}>
|
||||
{label && (
|
||||
<div className="slider-top-row" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<span className="label" dir="auto">{label}</span>
|
||||
@ -71,6 +77,7 @@ const RangeSlider: FC<OwnProps> = ({
|
||||
value={value}
|
||||
step={step}
|
||||
type="range"
|
||||
className="RangeSlider__input"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{options && (
|
||||
|
||||
@ -9,6 +9,7 @@ import './ui/settings';
|
||||
import './ui/misc';
|
||||
import './ui/payments';
|
||||
import './ui/calls';
|
||||
import './ui/mediaViewer';
|
||||
|
||||
import './api/initial';
|
||||
import './api/chats';
|
||||
|
||||
78
src/global/actions/ui/mediaViewer.ts
Normal file
78
src/global/actions/ui/mediaViewer.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { addActionHandler } from '../../index';
|
||||
|
||||
addActionHandler('openMediaViewer', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, threadId, messageId, avatarOwnerId, profilePhotoIndex, origin, volume, playbackRate, isMuted,
|
||||
} = payload;
|
||||
|
||||
return {
|
||||
...global,
|
||||
mediaViewer: {
|
||||
...global.mediaViewer,
|
||||
chatId,
|
||||
threadId,
|
||||
messageId,
|
||||
avatarOwnerId,
|
||||
profilePhotoIndex,
|
||||
origin,
|
||||
volume: volume ?? global.mediaViewer.volume,
|
||||
playbackRate: playbackRate || global.mediaViewer.playbackRate,
|
||||
isMuted: isMuted || global.mediaViewer.isMuted,
|
||||
},
|
||||
forwardMessages: {},
|
||||
};
|
||||
});
|
||||
|
||||
addActionHandler('closeMediaViewer', (global) => {
|
||||
const { volume, isMuted, playbackRate } = global.mediaViewer;
|
||||
return {
|
||||
...global,
|
||||
mediaViewer: {
|
||||
volume,
|
||||
isMuted,
|
||||
playbackRate,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
addActionHandler('setMediaViewerVolume', (global, actions, payload) => {
|
||||
const {
|
||||
volume,
|
||||
} = payload;
|
||||
|
||||
return {
|
||||
...global,
|
||||
mediaViewer: {
|
||||
...global.mediaViewer,
|
||||
volume,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
addActionHandler('setMediaViewerPlaybackRate', (global, actions, payload) => {
|
||||
const {
|
||||
playbackRate,
|
||||
} = payload;
|
||||
|
||||
return {
|
||||
...global,
|
||||
mediaViewer: {
|
||||
...global.mediaViewer,
|
||||
playbackRate,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
addActionHandler('setMediaViewerMuted', (global, actions, payload) => {
|
||||
const {
|
||||
isMuted,
|
||||
} = payload;
|
||||
|
||||
return {
|
||||
...global,
|
||||
mediaViewer: {
|
||||
...global.mediaViewer,
|
||||
isMuted,
|
||||
},
|
||||
};
|
||||
});
|
||||
@ -156,36 +156,10 @@ addActionHandler('replyToNextMessage', (global, actions, payload) => {
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('openMediaViewer', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, threadId, messageId, avatarOwnerId, profilePhotoIndex, origin,
|
||||
} = payload!;
|
||||
|
||||
return {
|
||||
...global,
|
||||
mediaViewer: {
|
||||
chatId,
|
||||
threadId,
|
||||
messageId,
|
||||
avatarOwnerId,
|
||||
profilePhotoIndex,
|
||||
origin,
|
||||
},
|
||||
forwardMessages: {},
|
||||
};
|
||||
});
|
||||
|
||||
addActionHandler('closeMediaViewer', (global) => {
|
||||
return {
|
||||
...global,
|
||||
mediaViewer: {},
|
||||
};
|
||||
});
|
||||
|
||||
addActionHandler('openAudioPlayer', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, threadId, messageId, origin, volume, playbackRate, isMuted,
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
return {
|
||||
...global,
|
||||
@ -204,7 +178,7 @@ addActionHandler('openAudioPlayer', (global, actions, payload) => {
|
||||
addActionHandler('setAudioPlayerVolume', (global, actions, payload) => {
|
||||
const {
|
||||
volume,
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
return {
|
||||
...global,
|
||||
@ -218,7 +192,7 @@ addActionHandler('setAudioPlayerVolume', (global, actions, payload) => {
|
||||
addActionHandler('setAudioPlayerPlaybackRate', (global, actions, payload) => {
|
||||
const {
|
||||
playbackRate,
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
return {
|
||||
...global,
|
||||
@ -232,7 +206,7 @@ addActionHandler('setAudioPlayerPlaybackRate', (global, actions, payload) => {
|
||||
addActionHandler('setAudioPlayerMuted', (global, actions, payload) => {
|
||||
const {
|
||||
isMuted,
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
return {
|
||||
...global,
|
||||
@ -246,7 +220,7 @@ addActionHandler('setAudioPlayerMuted', (global, actions, payload) => {
|
||||
addActionHandler('setAudioPlayerOrigin', (global, actions, payload) => {
|
||||
const {
|
||||
origin,
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
return {
|
||||
...global,
|
||||
|
||||
@ -196,6 +196,14 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
cached.audioPlayer.playbackRate = DEFAULT_PLAYBACK_RATE;
|
||||
}
|
||||
|
||||
if (cached.mediaViewer.volume === undefined) {
|
||||
cached.mediaViewer.volume = DEFAULT_VOLUME;
|
||||
}
|
||||
|
||||
if (cached.mediaViewer.playbackRate === undefined) {
|
||||
cached.mediaViewer.playbackRate = DEFAULT_PLAYBACK_RATE;
|
||||
}
|
||||
|
||||
if (!cached.groupCalls) {
|
||||
cached.groupCalls = initialState.groupCalls;
|
||||
}
|
||||
@ -246,6 +254,11 @@ function updateCache() {
|
||||
playbackRate: global.audioPlayer.playbackRate,
|
||||
isMuted: global.audioPlayer.isMuted,
|
||||
},
|
||||
mediaViewer: {
|
||||
volume: global.mediaViewer.volume,
|
||||
playbackRate: global.mediaViewer.playbackRate,
|
||||
isMuted: global.mediaViewer.isMuted,
|
||||
},
|
||||
isChatInfoShown: reduceShowChatInfo(global),
|
||||
users: reduceUsers(global),
|
||||
chats: reduceChats(global),
|
||||
|
||||
@ -116,7 +116,11 @@ export const INITIAL_STATE: GlobalState = {
|
||||
|
||||
topInlineBots: {},
|
||||
|
||||
mediaViewer: {},
|
||||
mediaViewer: {
|
||||
volume: DEFAULT_VOLUME,
|
||||
playbackRate: DEFAULT_PLAYBACK_RATE,
|
||||
isMuted: false,
|
||||
},
|
||||
|
||||
audioPlayer: {
|
||||
volume: DEFAULT_VOLUME,
|
||||
|
||||
@ -374,6 +374,9 @@ export type GlobalState = {
|
||||
avatarOwnerId?: string;
|
||||
profilePhotoIndex?: number;
|
||||
origin?: MediaViewerOrigin;
|
||||
volume: number;
|
||||
playbackRate: number;
|
||||
isMuted: boolean;
|
||||
};
|
||||
|
||||
audioPlayer: {
|
||||
@ -521,12 +524,60 @@ export interface ActionPayloads {
|
||||
type?: MessageListType;
|
||||
shouldReplaceHistory?: boolean;
|
||||
};
|
||||
|
||||
// Messages
|
||||
setEditingDraft: {
|
||||
text?: ApiFormattedText;
|
||||
chatId: string;
|
||||
threadId: number;
|
||||
type: MessageListType;
|
||||
};
|
||||
|
||||
// Media Viewer & Audio Player
|
||||
openMediaViewer: {
|
||||
chatId?: string;
|
||||
threadId?: number;
|
||||
messageId?: number;
|
||||
avatarOwnerId?: string;
|
||||
profilePhotoIndex?: number;
|
||||
origin?: MediaViewerOrigin;
|
||||
volume?: number;
|
||||
playbackRate?: number;
|
||||
isMuted?: boolean;
|
||||
};
|
||||
closeMediaViewer: {};
|
||||
setMediaViewerVolume: {
|
||||
volume: number;
|
||||
};
|
||||
setMediaViewerPlaybackRate: {
|
||||
playbackRate: number;
|
||||
};
|
||||
setMediaViewerMuted: {
|
||||
isMuted: boolean;
|
||||
};
|
||||
|
||||
openAudioPlayer: {
|
||||
chatId: string;
|
||||
threadId?: number;
|
||||
messageId: number;
|
||||
origin?: AudioOrigin;
|
||||
volume?: number;
|
||||
playbackRate?: number;
|
||||
isMuted?: boolean;
|
||||
};
|
||||
closeAudioPlayer: {};
|
||||
setAudioPlayerVolume: {
|
||||
volume: number;
|
||||
};
|
||||
setAudioPlayerPlaybackRate: {
|
||||
playbackRate: number;
|
||||
};
|
||||
setAudioPlayerMuted: {
|
||||
isMuted: boolean;
|
||||
};
|
||||
setAudioPlayerOrigin: {
|
||||
origin: AudioOrigin;
|
||||
};
|
||||
}
|
||||
|
||||
export type NonTypedActionNames = (
|
||||
@ -613,10 +664,6 @@ export type NonTypedActionNames = (
|
||||
// bots
|
||||
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
|
||||
'resetInlineBot' | 'restartBot' | 'startBot' |
|
||||
// media viewer & audio player
|
||||
'openMediaViewer' | 'closeMediaViewer' |
|
||||
'openAudioPlayer' | 'setAudioPlayerVolume' | 'setAudioPlayerPlaybackRate' |
|
||||
'setAudioPlayerMuted' | 'setAudioPlayerOrigin' | 'closeAudioPlayer' |
|
||||
// misc
|
||||
'openPollModal' | 'closePollModal' |
|
||||
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user