Merge pull request #311 from Stremio/feat/player-options-menu

feat(Player): add options menu
This commit is contained in:
Alexandru Branza 2022-12-21 16:30:07 +02:00 committed by GitHub
commit 9a52f3cbc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 184 additions and 17 deletions

14
package-lock.json generated
View file

@ -12,7 +12,7 @@
"@babel/runtime": "7.16.0",
"@sentry/browser": "6.13.3",
"@stremio/stremio-colors": "5.0.1",
"@stremio/stremio-core-web": "0.44.7",
"@stremio/stremio-core-web": "0.44.8",
"@stremio/stremio-icons": "4.0.0",
"@stremio/stremio-video": "0.0.24",
"a-color-picker": "1.2.1",
@ -2699,9 +2699,9 @@
"integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA=="
},
"node_modules/@stremio/stremio-core-web": {
"version": "0.44.7",
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.7.tgz",
"integrity": "sha512-hkeYLfL1On4TMBHn87Onrp93aeRuTh4YXMKdDR1Vz5YikPOiPEq/JRoLLmmSSsFEdifs6Egu+A0qiggTttepOA==",
"version": "0.44.8",
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.8.tgz",
"integrity": "sha512-c7ARfOxf0yy73b6sZauCLPUjpNYacF9tZXW7bxxg18IBGv7kXDBvLnk1rc54XKhJfHRgAPJuY5I1ijA0bM4nBQ==",
"dependencies": {
"@babel/runtime": "7.16.0"
}
@ -16716,9 +16716,9 @@
"integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA=="
},
"@stremio/stremio-core-web": {
"version": "0.44.7",
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.7.tgz",
"integrity": "sha512-hkeYLfL1On4TMBHn87Onrp93aeRuTh4YXMKdDR1Vz5YikPOiPEq/JRoLLmmSSsFEdifs6Egu+A0qiggTttepOA==",
"version": "0.44.8",
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.8.tgz",
"integrity": "sha512-c7ARfOxf0yy73b6sZauCLPUjpNYacF9tZXW7bxxg18IBGv7kXDBvLnk1rc54XKhJfHRgAPJuY5I1ijA0bM4nBQ==",
"requires": {
"@babel/runtime": "7.16.0"
}

View file

@ -15,7 +15,7 @@
"@babel/runtime": "7.16.0",
"@sentry/browser": "6.13.3",
"@stremio/stremio-colors": "5.0.1",
"@stremio/stremio-core-web": "0.44.7",
"@stremio/stremio-core-web": "0.44.8",
"@stremio/stremio-icons": "4.0.0",
"@stremio/stremio-video": "0.0.24",
"a-color-picker": "1.2.1",

View file

@ -33,6 +33,7 @@ const ControlBar = ({
onToggleInfoMenu,
onToggleSpeedMenu,
onToggleVideosMenu,
onToggleOptionsMenu,
...props
}) => {
const { chromecast } = useServices();
@ -50,6 +51,9 @@ const ControlBar = ({
const onVideosButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.videosMenuClosePrevented = true;
}, []);
const onOptionsButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.optionsMenuClosePrevented = true;
}, []);
const onPlayPauseButtonClick = React.useCallback(() => {
if (paused) {
if (typeof onPlayRequested === 'function') {
@ -101,6 +105,11 @@ const ControlBar = ({
onToggleVideosMenu();
}
}, [onToggleVideosMenu]);
const onOptionsButtonClick = React.useCallback(() => {
if (typeof onToggleOptionsMenu === 'function') {
onToggleOptionsMenu();
}
}, [onToggleOptionsMenu]);
const onChromecastButtonClick = React.useCallback(() => {
chromecast.transport.requestSession();
}, []);
@ -178,6 +187,9 @@ const ControlBar = ({
:
null
}
<Button className={styles['control-bar-button']} tabIndex={-1} onMouseDown={onOptionsButtonMouseDown} onClick={onOptionsButtonClick}>
<Icon className={styles['icon']} icon={'ic_more'} />
</Button>
</div>
</div>
</div>
@ -205,7 +217,8 @@ ControlBar.propTypes = {
onToggleSubtitlesMenu: PropTypes.func,
onToggleInfoMenu: PropTypes.func,
onToggleSpeedMenu: PropTypes.func,
onToggleVideosMenu: PropTypes.func
onToggleVideosMenu: PropTypes.func,
onToggleOptionsMenu: PropTypes.func,
};
module.exports = ControlBar;

View file

@ -0,0 +1,92 @@
// Copyright (C) 2017-2022 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom');
const { Button, useToast } = require('stremio/common');
// const { useServices } = require('stremio/services');
const styles = require('./styles');
const OptionsMenu = ({ className, stream }) => {
// const { core } = useServices();
const toast = useToast();
const streamUrl = React.useMemo(() => {
return stream !== null ?
stream.deepLinks &&
stream.deepLinks.externalPlayer &&
typeof stream.deepLinks.externalPlayer.download === 'string' ?
stream.deepLinks.externalPlayer.download
:
null
:
null;
}, [stream]);
const onCopyStreamButtonClick = React.useCallback(() => {
if (streamUrl !== null) {
navigator.clipboard.writeText(streamUrl)
.then(() => {
toast.show({
type: 'success',
title: 'Copied',
message: 'Stream link was copied to your clipboard',
timeout: 3000
});
})
.catch((e) => {
console.error(e);
toast.show({
type: 'error',
title: 'Error',
message: `Failed to copy stream link: ${streamUrl}`,
timeout: 3000
});
});
}
}, [streamUrl]);
const onDownloadVideoButtonClick = React.useCallback(() => {
if (streamUrl !== null) {
window.open(streamUrl);
}
}, [streamUrl]);
// const onExternalPlayerButtonClick = React.useCallback(() => {
// if (streamUrl !== null) {
// core.transport.dispatch({
// action: 'StreamingServer',
// args: {
// action: 'PlayOnDevice',
// args: {
// device: 'vlc',
// source: streamUrl,
// }
// }
// });
// }
// }, [streamUrl]);
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.optionsMenuClosePrevented = true;
}, []);
return (
<div className={classnames(className, styles['options-menu-container'])} onMouseDown={onMouseDown}>
<Button className={classnames(styles['option-container'], { 'disabled': stream === null })} disabled={stream === null} onClick={onCopyStreamButtonClick}>
<Icon className={styles['icon']} icon={'ic_link'} />
<div className={styles['label']}>Copy Stream Link</div>
</Button>
<Button className={classnames(styles['option-container'], { 'disabled': stream === null })} disabled={stream === null}onClick={onDownloadVideoButtonClick}>
<Icon className={styles['icon']} icon={'ic_downloads'} />
<div className={styles['label']}>Download Video</div>
</Button>
{/* <Button className={classnames(styles['option-container'], { 'disabled': stream === null })} disabled={stream === null} onClick={onExternalPlayerButtonClick}>
<Icon className={styles['icon']} icon={'ic_vlc'} />
<div className={styles['label']}>Play in External Player</div>
</Button> */}
</div>
);
};
OptionsMenu.propTypes = {
className: PropTypes.string,
stream: PropTypes.object
};
module.exports = OptionsMenu;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2022 Smart code 203358507
const OptionsMenu = require('./OptionsMenu');
module.exports = OptionsMenu;

View file

@ -0,0 +1,37 @@
// Copyright (C) 2017-2022 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
.options-menu-container {
width: 15rem;
.option-container {
display: flex;
flex-direction: row;
align-items: center;
height: 4rem;
.icon {
flex: none;
width: 1.4rem;
height: 1.4rem;
margin: 1.3rem;
fill: @color-surface-light5-90;
}
.label {
flex: 1;
max-height: 2.4em;
font-weight: 400;
color: @color-surface-light5-90;
}
&:hover {
background-color: @color-background-light2;
}
&:global(.disabled) {
opacity: 0.5;
}
}
}

View file

@ -13,6 +13,7 @@ const BufferingLoader = require('./BufferingLoader');
const ControlBar = require('./ControlBar');
const NextVideoPopup = require('./NextVideoPopup');
const InfoMenu = require('./InfoMenu');
const OptionsMenu = require('./OptionsMenu');
const VideosMenu = require('./VideosMenu');
const SubtitlesMenu = require('./SubtitlesMenu');
const SpeedMenu = require('./SpeedMenu');
@ -40,6 +41,7 @@ const Player = ({ urlParams, queryParams }) => {
});
const [immersed, setImmersed] = React.useState(true);
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
@ -215,6 +217,9 @@ const Player = ({ urlParams, queryParams }) => {
toggleFullscreen();
}, [toggleFullscreen]);
const onContainerMouseDown = React.useCallback((event) => {
if (!event.nativeEvent.optionsMenuClosePrevented) {
closeOptionsMenu();
}
if (!event.nativeEvent.subtitlesMenuClosePrevented) {
closeSubtitlesMenu();
}
@ -439,7 +444,7 @@ const Player = ({ urlParams, queryParams }) => {
const onKeyDown = (event) => {
switch (event.code) {
case 'Space': {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen&& videoState.paused !== null) {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.paused !== null) {
if (videoState.paused) {
onPlayRequested();
} else {
@ -450,7 +455,7 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'ArrowRight': {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.time !== null) {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.time !== null) {
const seekTimeMultiplier = event.shiftKey ? 3 : 1;
onSeekRequested(videoState.time + (settings.seekTimeDuration * seekTimeMultiplier));
}
@ -458,7 +463,7 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'ArrowLeft': {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.time !== null) {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.time !== null) {
const seekTimeMultiplier = event.shiftKey ? 3 : 1;
onSeekRequested(videoState.time - (settings.seekTimeDuration * seekTimeMultiplier));
}
@ -466,20 +471,21 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'ArrowUp': {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.volume !== null) {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.volume !== null) {
onVolumeChangeRequested(videoState.volume + 5);
}
break;
}
case 'ArrowDown': {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.volume !== null) {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.volume !== null) {
onVolumeChangeRequested(videoState.volume - 5);
}
break;
}
case 'KeyS': {
closeOptionsMenu();
closeInfoMenu();
closeSpeedMenu();
closeVideosMenu();
@ -492,6 +498,7 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'KeyI': {
closeOptionsMenu();
closeSubtitlesMenu();
closeSpeedMenu();
closeVideosMenu();
@ -502,6 +509,7 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'KeyR': {
closeOptionsMenu();
closeInfoMenu();
closeSubtitlesMenu();
closeVideosMenu();
@ -512,6 +520,7 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'KeyV': {
closeOptionsMenu();
closeInfoMenu();
closeSubtitlesMenu();
closeSpeedMenu();
@ -522,6 +531,7 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'Escape': {
closeOptionsMenu();
closeSubtitlesMenu();
closeInfoMenu();
closeSpeedMenu();
@ -537,7 +547,7 @@ const Player = ({ urlParams, queryParams }) => {
return () => {
window.removeEventListener('keydown', onKeyDown);
};
}, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videosMenuOpen, speedMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, videoState.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu]);
}, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videosMenuOpen, speedMenuOpen, optionsMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, videoState.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu]);
React.useLayoutEffect(() => {
return () => {
setImmersedDebounced.cancel();
@ -546,7 +556,7 @@ const Player = ({ urlParams, queryParams }) => {
};
}, []);
return (
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !speedMenuOpen && !videosMenuOpen && !nextVideoPopupOpen })}
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !speedMenuOpen && !videosMenuOpen && !nextVideoPopupOpen && !optionsMenuOpen })}
onMouseDown={onContainerMouseDown}
onMouseMove={onContainerMouseMove}
onMouseOver={onContainerMouseMove}
@ -591,7 +601,7 @@ const Player = ({ urlParams, queryParams }) => {
null
}
{
subtitlesMenuOpen || infoMenuOpen || videosMenuOpen || speedMenuOpen ?
subtitlesMenuOpen || infoMenuOpen || videosMenuOpen || speedMenuOpen || optionsMenuOpen ?
<div className={styles['layer']} />
:
null
@ -622,6 +632,7 @@ const Player = ({ urlParams, queryParams }) => {
onUnmuteRequested={onUnmuteRequested}
onVolumeChangeRequested={onVolumeChangeRequested}
onSeekRequested={onSeekRequested}
onToggleOptionsMenu={toggleOptionsMenu}
onToggleSubtitlesMenu={toggleSubtitlesMenu}
onToggleInfoMenu={toggleInfoMenu}
onToggleSpeedMenu={toggleSpeedMenu}
@ -699,6 +710,15 @@ const Player = ({ urlParams, queryParams }) => {
:
null
}
{
optionsMenuOpen ?
<OptionsMenu
className={classnames(styles['layer'], styles['menu-layer'])}
stream={player.selected.stream}
/>
:
null
}
</div>
);
};