diff --git a/package.json b/package.json index f94739e42..034c65757 100755 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@sentry/browser": "6.13.3", "@stremio/stremio-colors": "4.0.1", "@stremio/stremio-core-web": "0.44.5", - "@stremio/stremio-icons": "3.0.5", + "@stremio/stremio-icons": "4.0.0", "@stremio/stremio-video": "0.0.23", "a-color-picker": "1.2.1", "bowser": "2.11.0", diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js index b63c453f4..d4b922281 100644 --- a/src/routes/Player/ControlBar/ControlBar.js +++ b/src/routes/Player/ControlBar/ControlBar.js @@ -18,6 +18,7 @@ const ControlBar = ({ duration, volume, muted, + playbackSpeed, subtitlesTracks, audioTracks, metaItem, @@ -29,6 +30,7 @@ const ControlBar = ({ onSeekRequested, onToggleSubtitlesMenu, onToggleInfoMenu, + onToggleSpeedMenu, ...props }) => { const { chromecast } = useServices(); @@ -72,6 +74,11 @@ const ControlBar = ({ onToggleInfoMenu(); } }, [onToggleInfoMenu]); + const onSpeedButtonClick = React.useCallback(() => { + if (typeof onToggleSpeedMenu === 'function') { + onToggleSpeedMenu(); + } + }, [onToggleSpeedMenu]); const onChromecastButtonClick = React.useCallback(() => { chromecast.transport.requestSession(); }, []); @@ -118,6 +125,9 @@ const ControlBar = ({
+ @@ -146,6 +156,7 @@ ControlBar.propTypes = { duration: PropTypes.number, volume: PropTypes.number, muted: PropTypes.bool, + playbackSpeed: PropTypes.number, subtitlesTracks: PropTypes.array, audioTracks: PropTypes.array, metaItem: PropTypes.object, @@ -156,7 +167,8 @@ ControlBar.propTypes = { onVolumeChangeRequested: PropTypes.func, onSeekRequested: PropTypes.func, onToggleSubtitlesMenu: PropTypes.func, - onToggleInfoMenu: PropTypes.func + onToggleInfoMenu: PropTypes.func, + onToggleSpeedMenu: PropTypes.func }; module.exports = ControlBar; diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index b16334c49..190d9458e 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -12,6 +12,7 @@ const BufferingLoader = require('./BufferingLoader'); const ControlBar = require('./ControlBar'); const InfoMenu = require('./InfoMenu'); const SubtitlesMenu = require('./SubtitlesMenu'); +const SpeedMenu = require('./SpeedMenu'); const Video = require('./Video'); const usePlayer = require('./usePlayer'); const useSettings = require('./useSettings'); @@ -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 [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false); const [error, setError] = React.useState(null); const [videoState, setVideoState] = React.useReducer( (videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }), @@ -157,6 +159,9 @@ const Player = ({ urlParams, queryParams }) => { const onSeekRequested = React.useCallback((time) => { dispatch({ type: 'setProp', propName: 'time', propValue: time }); }, []); + const onPlaybackSpeedChanged = React.useCallback((rate) => { + dispatch({ type: 'setProp', propName: 'playbackSpeed', propValue: rate }); + }, []); const onSubtitlesTrackSelected = React.useCallback((id) => { dispatch({ type: 'setProp', propName: 'selectedSubtitlesTrackId', propValue: id }); dispatch({ type: 'setProp', propName: 'selectedExtraSubtitlesTrackId', propValue: null }); @@ -198,6 +203,9 @@ const Player = ({ urlParams, queryParams }) => { if (!event.nativeEvent.infoMenuClosePrevented) { closeInfoMenu(); } + if (!event.nativeEvent.speedMenuClosePrevented) { + closeSpeedMenu(); + } }, []); const onContainerMouseMove = React.useCallback((event) => { setImmersed(false); @@ -357,7 +365,7 @@ const Player = ({ urlParams, queryParams }) => { const onKeyDown = (event) => { switch (event.code) { case 'Space': { - if (!subtitlesMenuOpen && !infoMenuOpen && videoState.paused !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && !speedMenuOpen && videoState.paused !== null) { if (videoState.paused) { onPlayRequested(); } else { @@ -368,7 +376,7 @@ const Player = ({ urlParams, queryParams }) => { break; } case 'ArrowRight': { - if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && !speedMenuOpen && videoState.time !== null) { const seekTimeMultiplier = event.shiftKey ? 3 : 1; onSeekRequested(videoState.time + (settings.seekTimeDuration * seekTimeMultiplier)); } @@ -376,7 +384,7 @@ const Player = ({ urlParams, queryParams }) => { break; } case 'ArrowLeft': { - if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && !speedMenuOpen && videoState.time !== null) { const seekTimeMultiplier = event.shiftKey ? 3 : 1; onSeekRequested(videoState.time - (settings.seekTimeDuration * seekTimeMultiplier)); } @@ -384,14 +392,14 @@ const Player = ({ urlParams, queryParams }) => { break; } case 'ArrowUp': { - if (!subtitlesMenuOpen && !infoMenuOpen && videoState.volume !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && !speedMenuOpen && videoState.volume !== null) { onVolumeChangeRequested(videoState.volume + 5); } break; } case 'ArrowDown': { - if (!subtitlesMenuOpen && !infoMenuOpen && videoState.volume !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && !speedMenuOpen && videoState.volume !== null) { onVolumeChangeRequested(videoState.volume - 5); } @@ -418,6 +426,7 @@ const Player = ({ urlParams, queryParams }) => { case 'Escape': { closeSubtitlesMenu(); closeInfoMenu(); + closeSpeedMenu(); break; } } @@ -428,7 +437,7 @@ const Player = ({ urlParams, queryParams }) => { return () => { window.removeEventListener('keydown', onKeyDown); }; - }, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu]); + }, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu]); React.useLayoutEffect(() => { return () => { setImmersedDebounced.cancel(); @@ -437,7 +446,7 @@ const Player = ({ urlParams, queryParams }) => { }; }, []); return ( -
{ null } { - subtitlesMenuOpen || infoMenuOpen ? + subtitlesMenuOpen || infoMenuOpen || speedMenuOpen ?
: null @@ -502,6 +511,7 @@ const Player = ({ urlParams, queryParams }) => { duration={videoState.duration} volume={videoState.volume} muted={videoState.muted} + playbackSpeed={videoState.playbackSpeed} subtitlesTracks={videoState.subtitlesTracks.concat(videoState.extraSubtitlesTracks)} audioTracks={videoState.audioTracks} metaItem={player.metaItem} @@ -513,6 +523,7 @@ const Player = ({ urlParams, queryParams }) => { onSeekRequested={onSeekRequested} onToggleSubtitlesMenu={toggleSubtitlesMenu} onToggleInfoMenu={toggleInfoMenu} + onToggleSpeedMenu={toggleSpeedMenu} onMouseMove={onBarMouseMove} onMouseOver={onBarMouseMove} /> @@ -554,6 +565,16 @@ const Player = ({ urlParams, queryParams }) => { : null } + { + speedMenuOpen ? + + : + null + }
); }; diff --git a/src/routes/Player/SpeedMenu/SpeedMenu.js b/src/routes/Player/SpeedMenu/SpeedMenu.js new file mode 100644 index 000000000..c03b0cb58 --- /dev/null +++ b/src/routes/Player/SpeedMenu/SpeedMenu.js @@ -0,0 +1,50 @@ +// Copyright (C) 2017-2022 Smart code 203358507 + +const React = require('react'); +const PropTypes = require('prop-types'); +const classnames = require('classnames'); +const { Multiselect } = require('stremio/common'); +const styles = require('./styles'); + +const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25).reverse(); + +const SpeedMenu = ({ className, playbackSpeed, onPlaybackSpeedChanged }) => { + const onMouseDown = React.useCallback((event) => { + event.nativeEvent.speedMenuClosePrevented = true; + }, []); + const onOptionSelect = React.useCallback((event) => { + if (typeof onPlaybackSpeedChanged === 'function' && event.value) { + onPlaybackSpeedChanged(parseFloat(event.value)); + } + }, [onPlaybackSpeedChanged]); + const selectableOptions = React.useMemo(() => ({ + title: 'Playback Speed', + options: RATES.map((rate) => ({ + value: `${rate}`, + label: `${rate}x`, + title: `${rate}x` + })), + selected: [`${playbackSpeed}`], + renderLabelText: () => `${playbackSpeed}x`, + onSelect: onOptionSelect + }), [playbackSpeed, onOptionSelect]); + return ( +
+
+ Playback Speed +
+ +
+ ); +}; + +SpeedMenu.propTypes = { + className: PropTypes.string, + playbackSpeed: PropTypes.number, + onPlaybackSpeedChanged: PropTypes.func, +}; + +module.exports = SpeedMenu; diff --git a/src/routes/Player/SpeedMenu/index.js b/src/routes/Player/SpeedMenu/index.js new file mode 100644 index 000000000..928b8acce --- /dev/null +++ b/src/routes/Player/SpeedMenu/index.js @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2022 Smart code 203358507 + +const SpeedMenu = require('./SpeedMenu'); + +module.exports = SpeedMenu; diff --git a/src/routes/Player/SpeedMenu/styles.less b/src/routes/Player/SpeedMenu/styles.less new file mode 100644 index 000000000..39fc5677f --- /dev/null +++ b/src/routes/Player/SpeedMenu/styles.less @@ -0,0 +1,21 @@ +// Copyright (C) 2017-2022 Smart code 203358507 + +@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; + +.speed-menu-container { + width: 12rem; + overflow: visible !important; + + .title { + flex: none; + align-self: stretch; + max-height: 2.4em; + font-weight: 600; + color: @color-surface-light5-90; + margin: 1rem; + } + + .select-input-container { + padding: 1rem 1.5rem; + } +} \ No newline at end of file