diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js index b63c453f4..d8fbdb1a8 100644 --- a/src/routes/Player/ControlBar/ControlBar.js +++ b/src/routes/Player/ControlBar/ControlBar.js @@ -29,6 +29,7 @@ const ControlBar = ({ onSeekRequested, onToggleSubtitlesMenu, onToggleInfoMenu, + onToggleOptionsMenu, ...props }) => { const { chromecast } = useServices(); @@ -40,6 +41,9 @@ const ControlBar = ({ const onInfoButtonMouseDown = React.useCallback((event) => { event.nativeEvent.infoMenuClosePrevented = true; }, []); + const onOptionsButtonMouseDown = React.useCallback((event) => { + event.nativeEvent.optionsMenuClosePrevented = true; + }, []); const onPlayPauseButtonClick = React.useCallback(() => { if (paused) { if (typeof onPlayRequested === 'function') { @@ -72,6 +76,11 @@ const ControlBar = ({ onToggleInfoMenu(); } }, [onToggleInfoMenu]); + const onOptionsButtonClick = React.useCallback(() => { + if (typeof onToggleOptionsMenu === 'function') { + onToggleOptionsMenu(); + } + }, [onToggleOptionsMenu]); const onChromecastButtonClick = React.useCallback(() => { chromecast.transport.requestSession(); }, []); @@ -133,6 +142,9 @@ const ControlBar = ({ + @@ -156,7 +168,8 @@ ControlBar.propTypes = { onVolumeChangeRequested: PropTypes.func, onSeekRequested: PropTypes.func, onToggleSubtitlesMenu: PropTypes.func, - onToggleInfoMenu: PropTypes.func + onToggleInfoMenu: PropTypes.func, + onToggleOptionsMenu: PropTypes.func }; module.exports = ControlBar; diff --git a/src/routes/Player/OptionsMenu/OptionsMenu.js b/src/routes/Player/OptionsMenu/OptionsMenu.js new file mode 100644 index 000000000..83ba37e9c --- /dev/null +++ b/src/routes/Player/OptionsMenu/OptionsMenu.js @@ -0,0 +1,86 @@ +// 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, useStreamingServer, useToast } = require('stremio/common'); +const styles = require('./styles'); + +const OptionsMenu = ({ className, stream }) => { + const streamingServer = useStreamingServer(); + const toast = useToast(); + const streamUrl = React.useMemo(() => { + return stream !== null ? + typeof stream.url === 'string' ? + stream.url + : + typeof stream.infoHash === 'string' && + typeof stream.fileIdx === 'number' && + typeof streamingServer.selected === 'object' && + typeof streamingServer.selected.transportUrl === 'string' ? + `${streamingServer.selected.transportUrl}${stream.infoHash}/${stream.fileIdx}` + : + null + : + null; + }, [stream, streamingServer]); + 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. Try to enable your browser permission.', + timeout: 3000 + }); + }); + } + }, [streamUrl]); + const onDownloadVideoButtonClick = React.useCallback(() => { + if (streamUrl !== null) { + window.open(streamUrl); + } + }, [streamUrl]); + const onExternalPlayerButtonClick = React.useCallback(() => { + if (streamUrl !== null) { + window.open(`vlc://${encodeURIComponent(streamUrl)}`); + } + }, [streamUrl]); + const onMouseDown = React.useCallback((event) => { + event.nativeEvent.optionsMenuClosePrevented = true; + }, []); + return ( +
+ + + +
+ ); +}; + +OptionsMenu.propTypes = { + className: PropTypes.string, + stream: PropTypes.object +}; + +module.exports = OptionsMenu; diff --git a/src/routes/Player/OptionsMenu/index.js b/src/routes/Player/OptionsMenu/index.js new file mode 100644 index 000000000..069b002b3 --- /dev/null +++ b/src/routes/Player/OptionsMenu/index.js @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2022 Smart code 203358507 + +const OptionsMenu = require('./OptionsMenu'); + +module.exports = OptionsMenu; diff --git a/src/routes/Player/OptionsMenu/styles.less b/src/routes/Player/OptionsMenu/styles.less new file mode 100644 index 000000000..54ebd0368 --- /dev/null +++ b/src/routes/Player/OptionsMenu/styles.less @@ -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; + } + } +} \ No newline at end of file diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index b16334c49..07914b63d 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -11,6 +11,7 @@ const Icon = require('@stremio/stremio-icons/dom'); const BufferingLoader = require('./BufferingLoader'); const ControlBar = require('./ControlBar'); const InfoMenu = require('./InfoMenu'); +const OptionsMenu = require('./OptionsMenu'); const SubtitlesMenu = require('./SubtitlesMenu'); const Video = require('./Video'); const usePlayer = require('./usePlayer'); @@ -38,6 +39,7 @@ const Player = ({ urlParams, queryParams }) => { const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []); const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false); const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false); + const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false); const [error, setError] = React.useState(null); const [videoState, setVideoState] = React.useReducer( (videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }), @@ -198,6 +200,9 @@ const Player = ({ urlParams, queryParams }) => { if (!event.nativeEvent.infoMenuClosePrevented) { closeInfoMenu(); } + if (!event.nativeEvent.optionsMenuClosePrevented) { + closeOptionsMenu(); + } }, []); const onContainerMouseMove = React.useCallback((event) => { setImmersed(false); @@ -399,6 +404,7 @@ const Player = ({ urlParams, queryParams }) => { } case 'KeyS': { closeInfoMenu(); + closeOptionsMenu(); if ((Array.isArray(videoState.subtitlesTracks) && videoState.subtitlesTracks.length > 0) || (Array.isArray(videoState.extraSubtitlesTracks) && videoState.extraSubtitlesTracks.length > 0) || (Array.isArray(videoState.audioTracks) && videoState.audioTracks.length > 0)) { @@ -409,6 +415,7 @@ const Player = ({ urlParams, queryParams }) => { } case 'KeyI': { closeSubtitlesMenu(); + closeOptionsMenu(); if (player.metaItem !== null && player.metaItem.type === 'Ready') { toggleInfoMenu(); } @@ -418,6 +425,7 @@ const Player = ({ urlParams, queryParams }) => { case 'Escape': { closeSubtitlesMenu(); closeInfoMenu(); + closeOptionsMenu(); break; } } @@ -437,7 +445,7 @@ const Player = ({ urlParams, queryParams }) => { }; }, []); return ( -
{ onSeekRequested={onSeekRequested} onToggleSubtitlesMenu={toggleSubtitlesMenu} onToggleInfoMenu={toggleInfoMenu} + onToggleOptionsMenu={toggleOptionsMenu} onMouseMove={onBarMouseMove} onMouseOver={onBarMouseMove} /> @@ -554,6 +563,15 @@ const Player = ({ urlParams, queryParams }) => { : null } + { + optionsMenuOpen ? + + : + null + }
); };