refactor player menus

This commit is contained in:
nklhrstv 2020-03-31 18:19:39 +03:00
parent 1ccca352d6
commit 89be4e8d16
12 changed files with 120 additions and 214 deletions

View file

@ -3,33 +3,68 @@ const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const Icon = require('stremio-icons/dom'); const Icon = require('stremio-icons/dom');
const { Button } = require('stremio/common'); const { Button } = require('stremio/common');
const InfoMenuButton = require('./InfoMenuButton');
const MuteButton = require('./MuteButton');
const PlayPauseButton = require('./PlayPauseButton');
const SeekBar = require('./SeekBar'); const SeekBar = require('./SeekBar');
const SubtitlesButton = require('./SubtitlesButton');
const VolumeSlider = require('./VolumeSlider'); const VolumeSlider = require('./VolumeSlider');
const styles = require('./styles'); const styles = require('./styles');
const ControlBar = ({ const ControlBar = ({
className, className,
infoAvailable,
paused, paused,
time, time,
duration, duration,
volume, volume,
muted, muted,
subtitlesTracks, subtitlesTracks,
metaItem,
stream,
onPlayRequested, onPlayRequested,
onPauseRequested, onPauseRequested,
onMuteRequested, onMuteRequested,
onUnmuteRequested, onUnmuteRequested,
onVolumeChangeRequested, onVolumeChangeRequested,
onSeekRequested, onSeekRequested,
onToggleSubtitlesPicker, onToggleSubtitlesMenu,
onToggleInfoMenu, onToggleInfoMenu,
...props ...props
}) => { }) => {
const onSubtitlesButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.subtitlesMenuClosePrevented = true;
}, []);
const onInfoButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.infoMenuClosePrevented = true;
}, []);
const onPlayPauseButtonClick = React.useCallback(() => {
if (paused) {
if (typeof onPlayRequested === 'function') {
onPlayRequested();
}
} else {
if (typeof onPauseRequested === 'function') {
onPauseRequested();
}
}
}, [paused, onPlayRequested, onPauseRequested]);
const onMuteButtonClick = React.useCallback(() => {
if (muted) {
if (typeof onUnmuteRequested === 'function') {
onUnmuteRequested();
}
} else {
if (typeof onMuteRequested === 'function') {
onMuteRequested();
}
}
}, [muted, onMuteRequested, onUnmuteRequested]);
const onSubtitlesButtonClick = React.useCallback(() => {
if (typeof onToggleSubtitlesMenu === 'function') {
onToggleSubtitlesMenu();
}
}, [onToggleSubtitlesMenu]);
const onInfoButtonClick = React.useCallback(() => {
if (typeof onToggleInfoMenu === 'function') {
onToggleInfoMenu();
}
}, [onToggleInfoMenu]);
return ( return (
<div {...props} className={classnames(className, styles['control-bar-container'])}> <div {...props} className={classnames(className, styles['control-bar-container'])}>
<SeekBar <SeekBar
@ -39,19 +74,21 @@ const ControlBar = ({
onSeekRequested={onSeekRequested} onSeekRequested={onSeekRequested}
/> />
<div className={styles['control-bar-buttons-container']}> <div className={styles['control-bar-buttons-container']}>
<PlayPauseButton <Button className={classnames(styles['control-bar-button'], { 'disabled': typeof paused !== 'boolean' })} title={paused ? 'Play' : 'Pause'} tabIndex={-1} onClick={onPlayPauseButtonClick}>
className={styles['control-bar-button']} <Icon className={styles['icon']} icon={typeof paused !== 'boolean' || paused ? 'ic_play' : 'ic_pause'} />
paused={paused} </Button>
onPlayRequested={onPlayRequested} <Button className={classnames(styles['control-bar-button'], { 'disabled': typeof muted !== 'boolean' })} title={muted ? 'Unmute' : 'Mute'} tabIndex={-1} onClick={onMuteButtonClick}>
onPauseRequested={onPauseRequested} <Icon
/> className={styles['icon']}
<MuteButton icon={
className={styles['control-bar-button']} (typeof muted === 'boolean' && muted) ? 'ic_volume0' :
volume={volume} (volume === null || isNaN(volume)) ? 'ic_volume3' :
muted={muted} volume < 30 ? 'ic_volume1' :
onMuteRequested={onMuteRequested} volume < 70 ? 'ic_volume2' :
onUnmuteRequested={onUnmuteRequested} 'ic_volume3'
/> }
/>
</Button>
<VolumeSlider <VolumeSlider
className={styles['volume-slider']} className={styles['volume-slider']}
volume={volume} volume={volume}
@ -59,22 +96,19 @@ const ControlBar = ({
/> />
<div className={styles['spacing']} /> <div className={styles['spacing']} />
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}> <Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
<Icon className={'icon'} icon={'ic_network'} /> <Icon className={styles['icon']} icon={'ic_network'} />
</Button> </Button>
<InfoMenuButton <Button className={classnames(styles['control-bar-button'], { 'disabled': typeof metaItem !== 'object' || metaItem === null || typeof stream !== 'object' || stream === null })} tabIndex={-1} onMouseDown={onInfoButtonMouseDown} onClick={onInfoButtonClick}>
className={classnames(styles['control-bar-button'], { 'disabled': !infoAvailable })} <Icon className={styles['icon']} icon={'ic_info'} />
onToggleInfoMenu={onToggleInfoMenu}
/>
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
<Icon className={'icon'} icon={'ic_cast'} />
</Button> </Button>
<SubtitlesButton
className={styles['control-bar-button']}
subtitlesTracks={subtitlesTracks}
onToggleSubtitlesPicker={onToggleSubtitlesPicker}
/>
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}> <Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
<Icon className={'icon'} icon={'ic_videos'} /> <Icon className={styles['icon']} icon={'ic_cast'} />
</Button>
<Button className={classnames(styles['control-bar-button'], { 'disabled': !Array.isArray(subtitlesTracks) || subtitlesTracks.length === 0 })} tabIndex={-1} onMouseDown={onSubtitlesButtonMouseDown} onClick={onSubtitlesButtonClick}>
<Icon className={styles['icon']} icon={'ic_sub'} />
</Button>
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
<Icon className={styles['icon']} icon={'ic_videos'} />
</Button> </Button>
</div> </div>
</div> </div>
@ -83,21 +117,22 @@ const ControlBar = ({
ControlBar.propTypes = { ControlBar.propTypes = {
className: PropTypes.string, className: PropTypes.string,
infoAvailable: PropTypes.bool, paused: PropTypes.bool,
paused: PropTypes.any, time: PropTypes.number,
time: PropTypes.any, duration: PropTypes.number,
duration: PropTypes.any, volume: PropTypes.number,
volume: PropTypes.any, muted: PropTypes.bool,
muted: PropTypes.any, subtitlesTracks: PropTypes.array,
subtitlesTracks: PropTypes.any, metaItem: PropTypes.object,
onPlayRequested: PropTypes.any, stream: PropTypes.object,
onPauseRequested: PropTypes.any, onPlayRequested: PropTypes.func,
onMuteRequested: PropTypes.any, onPauseRequested: PropTypes.func,
onUnmuteRequested: PropTypes.any, onMuteRequested: PropTypes.func,
onVolumeChangeRequested: PropTypes.any, onUnmuteRequested: PropTypes.func,
onSeekRequested: PropTypes.any, onVolumeChangeRequested: PropTypes.func,
onToggleSubtitlesPicker: PropTypes.any, onSeekRequested: PropTypes.func,
onToggleInfoMenu: PropTypes.any onToggleSubtitlesMenu: PropTypes.func,
onToggleInfoMenu: PropTypes.func
}; };
module.exports = ControlBar; module.exports = ControlBar;

View file

@ -1,27 +0,0 @@
const React = require('react');
const PropTypes = require('prop-types');
const Icon = require('stremio-icons/dom');
const { Button } = require('stremio/common');
const InfoMenuButton = ({ className, onToggleInfoMenu }) => {
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.infoMenuClosePrevented = true;
}, []);
const onClick = React.useCallback(() => {
if (typeof onToggleInfoMenu === 'function') {
onToggleInfoMenu();
}
}, [onToggleInfoMenu]);
return (
<Button className={className} tabIndex={-1} onMouseDown={onMouseDown} onClick={onClick}>
<Icon className={'icon'} icon={'ic_info'} />
</Button>
);
};
InfoMenuButton.propTypes = {
className: PropTypes.string,
onToggleInfoMenu: PropTypes.func
};
module.exports = InfoMenuButton;

View file

@ -1,3 +0,0 @@
const InfoMenuButton = require('./InfoMenuButton');
module.exports = InfoMenuButton;

View file

@ -1,39 +0,0 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const Icon = require('stremio-icons/dom');
const { Button } = require('stremio/common');
const MuteButton = ({ className, muted, volume, onMuteRequested, onUnmuteRequested }) => {
const toggleMuted = React.useCallback(() => {
if (muted) {
if (typeof onUnmuteRequested === 'function') {
onUnmuteRequested();
}
} else {
if (typeof onMuteRequested === 'function') {
onMuteRequested();
}
}
}, [muted, onMuteRequested, onUnmuteRequested]);
const icon = (typeof muted === 'boolean' && muted) ? 'ic_volume0' :
(volume === null || isNaN(volume)) ? 'ic_volume3' :
volume < 30 ? 'ic_volume1' :
volume < 70 ? 'ic_volume2' :
'ic_volume3';
return (
<Button className={classnames(className, { 'disabled': typeof muted !== 'boolean' })} title={muted ? 'Unmute' : 'Mute'} tabIndex={-1} onClick={toggleMuted}>
<Icon className={'icon'} icon={icon} />
</Button>
);
};
MuteButton.propTypes = {
className: PropTypes.string,
muted: PropTypes.bool,
volume: PropTypes.number,
onMuteRequested: PropTypes.func,
onUnmuteRequested: PropTypes.func
};
module.exports = MuteButton;

View file

@ -1,3 +0,0 @@
const MuteButton = require('./MuteButton');
module.exports = MuteButton;

View file

@ -1,36 +0,0 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const Icon = require('stremio-icons/dom');
const { Button } = require('stremio/common');
const PlayPauseButton = ({ className, paused, onPlayRequested, onPauseRequested }) => {
const togglePaused = React.useCallback(() => {
if (paused) {
if (typeof onPlayRequested === 'function') {
onPlayRequested();
}
} else {
if (typeof onPauseRequested === 'function') {
onPauseRequested();
}
}
}, [paused, onPlayRequested, onPauseRequested]);
return (
<Button className={classnames(className, { 'disabled': typeof paused !== 'boolean' })} title={paused ? 'Play' : 'Pause'} tabIndex={-1} onClick={togglePaused}>
<Icon
className={'icon'}
icon={typeof paused !== 'boolean' || paused ? 'ic_play' : 'ic_pause'}
/>
</Button>
);
};
PlayPauseButton.propTypes = {
className: PropTypes.string,
paused: PropTypes.bool,
onPlayRequested: PropTypes.func,
onPauseRequested: PropTypes.func
};
module.exports = PlayPauseButton;

View file

@ -1,3 +0,0 @@
const PlayPauseButton = require('./PlayPauseButton');
module.exports = PlayPauseButton;

View file

@ -1,29 +0,0 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const Icon = require('stremio-icons/dom');
const { Button } = require('stremio/common');
const SubtitlesButton = ({ className, subtitlesTracks, onToggleSubtitlesPicker }) => {
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.subtitlesPickerClosePrevented = true;
}, []);
const onClick = React.useCallback(() => {
if (typeof onToggleSubtitlesPicker === 'function') {
onToggleSubtitlesPicker();
}
}, [onToggleSubtitlesPicker]);
return (
<Button className={classnames(className, { 'disabled': !Array.isArray(subtitlesTracks) || subtitlesTracks.length === 0 })} tabIndex={-1} onMouseDown={onMouseDown} onClick={onClick}>
<Icon className={'icon'} icon={'ic_sub'} />
</Button>
);
};
SubtitlesButton.propTypes = {
className: PropTypes.string,
subtitlesTracks: PropTypes.array,
onToggleSubtitlesPicker: PropTypes.func
};
module.exports = SubtitlesButton;

View file

@ -1,3 +0,0 @@
const SubtitlesButton = require('./SubtitlesButton');
module.exports = SubtitlesButton;

View file

@ -21,12 +21,12 @@
align-items: center; align-items: center;
&:global(.disabled) { &:global(.disabled) {
:global(.icon) { .icon {
fill: @color-surface; fill: @color-surface;
} }
} }
:global(.icon) { .icon {
flex: none; flex: none;
width: 3rem; width: 3rem;
height: 2rem; height: 2rem;

View file

@ -18,17 +18,26 @@ const Player = ({ urlParams }) => {
const { core } = useServices(); const { core } = useServices();
const [player, updateLibraryItemState, pushToLibrary] = usePlayer(urlParams); const [player, updateLibraryItemState, pushToLibrary] = usePlayer(urlParams);
const [settings, updateSettings] = useSettings(); const [settings, updateSettings] = useSettings();
const stream = React.useMemo(() => {
return player.selected !== null ?
player.selected.stream
:
null;
}, [player]);
const metaItem = React.useMemo(() => {
return player.meta_resource !== null && player.meta_resource.content.type === 'Ready' ?
player.meta_resource.content.content
:
null;
}, [player]);
const routeFocused = useRouteFocused(); const routeFocused = useRouteFocused();
const toast = useToast(); const toast = useToast();
const [, , , toggleFullscreen] = useFullscreen(); const [, , , toggleFullscreen] = useFullscreen();
const [immersed, setImmersed] = React.useState(true); const [immersed, setImmersed] = React.useState(true);
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []); const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
const [subtitlesPickerOpen, , closeSubtitlesPicker, toggleSubtitlesPicker] = useBinaryState(false); const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false); const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
const [error, setError] = React.useState(null); const [error, setError] = React.useState(null);
const infoAvailable = React.useMemo(() => {
return player.meta_resource !== null && player.meta_resource.content.type === 'Ready';
}, [player]);
const [videoState, setVideoState] = React.useReducer( const [videoState, setVideoState] = React.useReducer(
(videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }), (videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }),
{ {
@ -150,8 +159,8 @@ const Player = ({ urlParams }) => {
toggleFullscreen(); toggleFullscreen();
}, [toggleFullscreen]); }, [toggleFullscreen]);
const onContainerMouseDown = React.useCallback((event) => { const onContainerMouseDown = React.useCallback((event) => {
if (!event.nativeEvent.subtitlesPickerClosePrevented) { if (!event.nativeEvent.subtitlesMenuClosePrevented) {
closeSubtitlesPicker(); closeSubtitlesMenu();
} }
if (!event.nativeEvent.infoMenuClosePrevented) { if (!event.nativeEvent.infoMenuClosePrevented) {
closeInfoMenu(); closeInfoMenu();
@ -239,7 +248,7 @@ const Player = ({ urlParams }) => {
const onKeyDown = (event) => { const onKeyDown = (event) => {
switch (event.code) { switch (event.code) {
case 'Space': { case 'Space': {
if (!subtitlesPickerOpen && !infoMenuOpen && videoState.paused !== null) { if (!subtitlesMenuOpen && !infoMenuOpen && videoState.paused !== null) {
if (videoState.paused) { if (videoState.paused) {
onPlayRequested(); onPlayRequested();
} else { } else {
@ -250,28 +259,28 @@ const Player = ({ urlParams }) => {
break; break;
} }
case 'ArrowRight': { case 'ArrowRight': {
if (!subtitlesPickerOpen && !infoMenuOpen && videoState.time !== null) { if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) {
onSeekRequested(videoState.time + 15000); onSeekRequested(videoState.time + 15000);
} }
break; break;
} }
case 'ArrowLeft': { case 'ArrowLeft': {
if (!subtitlesPickerOpen && !infoMenuOpen && videoState.time !== null) { if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) {
onSeekRequested(videoState.time - 15000); onSeekRequested(videoState.time - 15000);
} }
break; break;
} }
case 'ArrowUp': { case 'ArrowUp': {
if (!subtitlesPickerOpen && !infoMenuOpen && videoState.volume !== null) { if (!subtitlesMenuOpen && !infoMenuOpen && videoState.volume !== null) {
onVolumeChangeRequested(videoState.volume + 5); onVolumeChangeRequested(videoState.volume + 5);
} }
break; break;
} }
case 'ArrowDown': { case 'ArrowDown': {
if (!subtitlesPickerOpen && !infoMenuOpen && videoState.volume !== null) { if (!subtitlesMenuOpen && !infoMenuOpen && videoState.volume !== null) {
onVolumeChangeRequested(videoState.volume - 5); onVolumeChangeRequested(videoState.volume - 5);
} }
@ -279,18 +288,22 @@ const Player = ({ urlParams }) => {
} }
case 'KeyS': { case 'KeyS': {
closeInfoMenu(); closeInfoMenu();
toggleSubtitlesPicker(); if (Array.isArray(videoState.subtitlesTracks) && videoState.subtitlesTracks.length > 0) {
toggleSubtitlesMenu();
}
break; break;
} }
case 'KeyM': { case 'KeyM': {
closeSubtitlesPicker(); closeSubtitlesMenu();
if (infoAvailable) { if (typeof metaItem === 'object' && metaItem !== null && typeof stream === 'object' && stream !== null) {
toggleInfoMenu(); toggleInfoMenu();
} }
break; break;
} }
case 'Escape': { case 'Escape': {
closeSubtitlesPicker(); closeSubtitlesMenu();
closeInfoMenu(); closeInfoMenu();
break; break;
} }
@ -302,7 +315,7 @@ const Player = ({ urlParams }) => {
return () => { return () => {
window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keydown', onKeyDown);
}; };
}, [routeFocused, subtitlesPickerOpen, infoAvailable, infoMenuOpen, videoState.paused, videoState.time, videoState.volume, toggleSubtitlesPicker, toggleInfoMenu]); }, [routeFocused, subtitlesMenuOpen, infoMenuOpen, stream, metaItem, videoState.paused, videoState.time, videoState.volume, videoState.subtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu]);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
return () => { return () => {
setImmersedDebounced.cancel(); setImmersedDebounced.cancel();
@ -311,7 +324,7 @@ const Player = ({ urlParams }) => {
}; };
}, []); }, []);
return ( return (
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && videoState.paused !== null && !videoState.paused && !subtitlesPickerOpen && !infoMenuOpen })} <div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen })}
onMouseDown={onContainerMouseDown} onMouseDown={onContainerMouseDown}
onMouseMove={onContainerMouseMove} onMouseMove={onContainerMouseMove}
onMouseOver={onContainerMouseMove} onMouseOver={onContainerMouseMove}
@ -343,7 +356,7 @@ const Player = ({ urlParams }) => {
onDoubleClick={onVideoDoubleClick} onDoubleClick={onVideoDoubleClick}
/> />
{ {
subtitlesPickerOpen || infoMenuOpen ? subtitlesMenuOpen || infoMenuOpen ?
<div className={styles['layer']} /> <div className={styles['layer']} />
: :
null null
@ -370,20 +383,21 @@ const Player = ({ urlParams }) => {
volume={videoState.volume} volume={videoState.volume}
muted={videoState.muted} muted={videoState.muted}
subtitlesTracks={videoState.subtitlesTracks} subtitlesTracks={videoState.subtitlesTracks}
infoAvailable={infoAvailable} stream={stream}
metaItem={metaItem}
onPlayRequested={onPlayRequested} onPlayRequested={onPlayRequested}
onPauseRequested={onPauseRequested} onPauseRequested={onPauseRequested}
onMuteRequested={onMuteRequested} onMuteRequested={onMuteRequested}
onUnmuteRequested={onUnmuteRequested} onUnmuteRequested={onUnmuteRequested}
onVolumeChangeRequested={onVolumeChangeRequested} onVolumeChangeRequested={onVolumeChangeRequested}
onSeekRequested={onSeekRequested} onSeekRequested={onSeekRequested}
onToggleSubtitlesPicker={toggleSubtitlesPicker} onToggleSubtitlesMenu={toggleSubtitlesMenu}
onToggleInfoMenu={toggleInfoMenu} onToggleInfoMenu={toggleInfoMenu}
onMouseMove={onBarMouseMove} onMouseMove={onBarMouseMove}
onMouseOver={onBarMouseMove} onMouseOver={onBarMouseMove}
/> />
{ {
subtitlesPickerOpen ? subtitlesMenuOpen ?
<SubtitlesPicker <SubtitlesPicker
className={classnames(styles['layer'], styles['menu-layer'])} className={classnames(styles['layer'], styles['menu-layer'])}
tracks={videoState.subtitlesTracks} tracks={videoState.subtitlesTracks}

View file

@ -51,7 +51,7 @@ const SubtitlesPicker = (props) => {
[]; [];
}, [props.tracks, selectedLanguage]); }, [props.tracks, selectedLanguage]);
const onMouseDown = React.useCallback((event) => { const onMouseDown = React.useCallback((event) => {
event.nativeEvent.subtitlesPickerClosePrevented = true; event.nativeEvent.subtitlesMenuClosePrevented = true;
}, []); }, []);
const languageOnClick = React.useCallback((event) => { const languageOnClick = React.useCallback((event) => {
const trackId = Array.isArray(props.tracks) ? const trackId = Array.isArray(props.tracks) ?