diff --git a/package-lock.json b/package-lock.json index 4328efbd2..b08282964 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@sentry/browser": "6.13.3", "@stremio/stremio-colors": "5.0.1", "@stremio/stremio-core-web": "0.44.6", - "@stremio/stremio-icons": "3.0.5", + "@stremio/stremio-icons": "4.0.0", "@stremio/stremio-video": "0.0.24", "a-color-picker": "1.2.1", "bowser": "2.11.0", @@ -2706,9 +2706,9 @@ } }, "node_modules/@stremio/stremio-icons": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@stremio/stremio-icons/-/stremio-icons-3.0.5.tgz", - "integrity": "sha512-knlcBibqJW2mbEgid6YEeQN9FPkIGAEtozYWqzKWeHd2DPY2nl8kYX2pMQpa2Db/RVSqbVstu/gdey5TtSgGYw==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@stremio/stremio-icons/-/stremio-icons-4.0.0.tgz", + "integrity": "sha512-ku1ye/V6WtzCltLKZvLwa60nlEUR2YYT/upjZDzBOoA2VXZu1ubFeR83Hx10tBZPnjALHkG/1QZ5Eyg9PoBMbQ==" }, "node_modules/@stremio/stremio-video": { "version": "0.0.24", @@ -16718,9 +16718,9 @@ } }, "@stremio/stremio-icons": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@stremio/stremio-icons/-/stremio-icons-3.0.5.tgz", - "integrity": "sha512-knlcBibqJW2mbEgid6YEeQN9FPkIGAEtozYWqzKWeHd2DPY2nl8kYX2pMQpa2Db/RVSqbVstu/gdey5TtSgGYw==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@stremio/stremio-icons/-/stremio-icons-4.0.0.tgz", + "integrity": "sha512-ku1ye/V6WtzCltLKZvLwa60nlEUR2YYT/upjZDzBOoA2VXZu1ubFeR83Hx10tBZPnjALHkG/1QZ5Eyg9PoBMbQ==" }, "@stremio/stremio-video": { "version": "0.0.24", diff --git a/package.json b/package.json index dd7d4497c..d36ec6289 100755 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@sentry/browser": "6.13.3", "@stremio/stremio-colors": "5.0.1", "@stremio/stremio-core-web": "0.44.6", - "@stremio/stremio-icons": "3.0.5", + "@stremio/stremio-icons": "4.0.0", "@stremio/stremio-video": "0.0.24", "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 c46ea85b2..63854a30a 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, @@ -30,6 +31,7 @@ const ControlBar = ({ onSeekRequested, onToggleSubtitlesMenu, onToggleInfoMenu, + onToggleSpeedMenu, onToggleVideosMenu, ...props }) => { @@ -42,6 +44,9 @@ const ControlBar = ({ const onInfoButtonMouseDown = React.useCallback((event) => { event.nativeEvent.infoMenuClosePrevented = true; }, []); + const onSpeedButtonMouseDown = React.useCallback((event) => { + event.nativeEvent.speedMenuClosePrevented = true; + }, []); const onVideosButtonMouseDown = React.useCallback((event) => { event.nativeEvent.videosMenuClosePrevented = true; }, []); @@ -86,6 +91,11 @@ const ControlBar = ({ onToggleInfoMenu(); } }, [onToggleInfoMenu]); + const onSpeedButtonClick = React.useCallback(() => { + if (typeof onToggleSpeedMenu === 'function') { + onToggleSpeedMenu(); + } + }, [onToggleSpeedMenu]); const onVideosButtonClick = React.useCallback(() => { if (typeof onToggleVideosMenu === 'function') { onToggleVideosMenu(); @@ -145,6 +155,9 @@ const ControlBar = ({
+ @@ -178,6 +191,7 @@ ControlBar.propTypes = { duration: PropTypes.number, volume: PropTypes.number, muted: PropTypes.bool, + playbackSpeed: PropTypes.number, subtitlesTracks: PropTypes.array, audioTracks: PropTypes.array, metaItem: PropTypes.object, @@ -190,6 +204,7 @@ ControlBar.propTypes = { onSeekRequested: PropTypes.func, onToggleSubtitlesMenu: PropTypes.func, onToggleInfoMenu: PropTypes.func, + onToggleSpeedMenu: PropTypes.func, onToggleVideosMenu: PropTypes.func }; diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 2ded41905..54e6d37ea 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -13,6 +13,7 @@ const ControlBar = require('./ControlBar'); const InfoMenu = require('./InfoMenu'); const VideosMenu = require('./VideosMenu'); const SubtitlesMenu = require('./SubtitlesMenu'); +const SpeedMenu = require('./SpeedMenu'); const Video = require('./Video'); const usePlayer = require('./usePlayer'); const useSettings = require('./useSettings'); @@ -39,6 +40,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 [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false); const [error, setError] = React.useState(null); const [videoState, setVideoState] = React.useReducer( @@ -52,6 +54,7 @@ const Player = ({ urlParams, queryParams }) => { buffering: null, volume: null, muted: null, + playbackSpeed: null, audioTracks: [], selectedAudioTrackId: null, subtitlesTracks: [], @@ -159,6 +162,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 }); @@ -200,6 +206,9 @@ const Player = ({ urlParams, queryParams }) => { if (!event.nativeEvent.infoMenuClosePrevented) { closeInfoMenu(); } + if (!event.nativeEvent.speedMenuClosePrevented) { + closeSpeedMenu(); + } if (!event.nativeEvent.videosMenuClosePrevented) { closeVideosMenu(); } @@ -329,6 +338,11 @@ const Player = ({ urlParams, queryParams }) => { closeVideosMenu(); } }, [player.metaItem]); + React.useEffect(() => { + if (videoState.playbackSpeed === null) { + closeSpeedMenu(); + } + }, [videoState.playbackSpeed]); React.useEffect(() => { const intervalId = setInterval(() => { pushToLibrary(); @@ -369,7 +383,7 @@ const Player = ({ urlParams, queryParams }) => { const onKeyDown = (event) => { switch (event.code) { case 'Space': { - if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && videoState.paused !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen&& videoState.paused !== null) { if (videoState.paused) { onPlayRequested(); } else { @@ -380,7 +394,7 @@ const Player = ({ urlParams, queryParams }) => { break; } case 'ArrowRight': { - if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && videoState.time !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.time !== null) { const seekTimeMultiplier = event.shiftKey ? 3 : 1; onSeekRequested(videoState.time + (settings.seekTimeDuration * seekTimeMultiplier)); } @@ -388,7 +402,7 @@ const Player = ({ urlParams, queryParams }) => { break; } case 'ArrowLeft': { - if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && videoState.time !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.time !== null) { const seekTimeMultiplier = event.shiftKey ? 3 : 1; onSeekRequested(videoState.time - (settings.seekTimeDuration * seekTimeMultiplier)); } @@ -396,14 +410,14 @@ const Player = ({ urlParams, queryParams }) => { break; } case 'ArrowUp': { - if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && videoState.volume !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.volume !== null) { onVolumeChangeRequested(videoState.volume + 5); } break; } case 'ArrowDown': { - if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && videoState.volume !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.volume !== null) { onVolumeChangeRequested(videoState.volume - 5); } @@ -411,6 +425,7 @@ const Player = ({ urlParams, queryParams }) => { } case 'KeyS': { closeInfoMenu(); + closeSpeedMenu(); closeVideosMenu(); if ((Array.isArray(videoState.subtitlesTracks) && videoState.subtitlesTracks.length > 0) || (Array.isArray(videoState.extraSubtitlesTracks) && videoState.extraSubtitlesTracks.length > 0) || @@ -422,6 +437,7 @@ const Player = ({ urlParams, queryParams }) => { } case 'KeyI': { closeSubtitlesMenu(); + closeSpeedMenu(); closeVideosMenu(); if (player.metaItem !== null && player.metaItem.type === 'Ready') { toggleInfoMenu(); @@ -429,9 +445,20 @@ const Player = ({ urlParams, queryParams }) => { break; } + case 'KeyR': { + closeInfoMenu(); + closeSubtitlesMenu(); + closeVideosMenu(); + if (videoState.playbackSpeed !== null) { + toggleSpeedMenu(); + } + + break; + } case 'KeyV': { closeInfoMenu(); closeSubtitlesMenu(); + closeSpeedMenu(); if (player.metaItem !== null && player.metaItem.type === 'Ready') { toggleVideosMenu(); } @@ -441,6 +468,7 @@ const Player = ({ urlParams, queryParams }) => { case 'Escape': { closeSubtitlesMenu(); closeInfoMenu(); + closeSpeedMenu(); closeVideosMenu(); break; } @@ -452,7 +480,7 @@ const Player = ({ urlParams, queryParams }) => { return () => { window.removeEventListener('keydown', onKeyDown); }; - }, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videosMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu]); + }, [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]); React.useLayoutEffect(() => { return () => { setImmersedDebounced.cancel(); @@ -461,7 +489,7 @@ const Player = ({ urlParams, queryParams }) => { }; }, []); return ( -
{ null } { - subtitlesMenuOpen || infoMenuOpen || videosMenuOpen ? + subtitlesMenuOpen || infoMenuOpen || videosMenuOpen || speedMenuOpen ?
: null @@ -526,6 +554,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} @@ -538,6 +567,7 @@ const Player = ({ urlParams, queryParams }) => { onSeekRequested={onSeekRequested} onToggleSubtitlesMenu={toggleSubtitlesMenu} onToggleInfoMenu={toggleInfoMenu} + onToggleSpeedMenu={toggleSpeedMenu} onToggleVideosMenu={toggleVideosMenu} onMouseMove={onBarMouseMove} onMouseOver={onBarMouseMove} @@ -580,6 +610,16 @@ const Player = ({ urlParams, queryParams }) => { : null } + { + speedMenuOpen ? + + : + null + } { videosMenuOpen ? { + const onClick = React.useCallback(() => { + if (typeof onSelect === 'function') { + onSelect(value); + } + }, [onSelect, value]); + return ( + + ); +}; + +OptionButton.propTypes = { + className: PropTypes.string, + value: PropTypes.number, + selected: PropTypes.bool, + onSelect: PropTypes.func, +}; + +module.exports = OptionButton; diff --git a/src/routes/Player/SpeedMenu/Option/index.js b/src/routes/Player/SpeedMenu/Option/index.js new file mode 100644 index 000000000..2bf2d108d --- /dev/null +++ b/src/routes/Player/SpeedMenu/Option/index.js @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2022 Smart code 203358507 + +const Option = require('./Option'); + +module.exports = Option; diff --git a/src/routes/Player/SpeedMenu/Option/styles.less b/src/routes/Player/SpeedMenu/Option/styles.less new file mode 100644 index 000000000..1de1f65ff --- /dev/null +++ b/src/routes/Player/SpeedMenu/Option/styles.less @@ -0,0 +1,38 @@ +// Copyright (C) 2017-2022 Smart code 203358507 + +@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; + +.option { + display: flex; + flex-direction: row; + align-items: center; + padding: 0 1.5em; + + &:global(.selected) { + background-color: @color-background; + + .icon { + display: block; + } + } + + &:hover, &:focus { + background-color: @color-background-light2; + } + + .label { + flex: 1; + font-weight: 400; + color: @color-surface-light5-90; + } + + .icon { + flex: none; + display: none; + width: 0.5rem; + height: 0.5rem; + border-radius: 100%; + margin-left: 1rem; + background-color: @color-accent3-90; + } +} \ No newline at end of file diff --git a/src/routes/Player/SpeedMenu/SpeedMenu.js b/src/routes/Player/SpeedMenu/SpeedMenu.js new file mode 100644 index 000000000..836a9ac80 --- /dev/null +++ b/src/routes/Player/SpeedMenu/SpeedMenu.js @@ -0,0 +1,48 @@ +// Copyright (C) 2017-2022 Smart code 203358507 + +const React = require('react'); +const PropTypes = require('prop-types'); +const classnames = require('classnames'); +const Option = require('./Option'); +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((value) => { + if (typeof onPlaybackSpeedChanged === 'function') { + onPlaybackSpeedChanged(value); + } + }, [onPlaybackSpeedChanged]); + return ( +
+
+ Playback Speed +
+
+ { + RATES.map((rate) => ( +
+
+ ); +}; + +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..fbb47ed00 --- /dev/null +++ b/src/routes/Player/SpeedMenu/styles.less @@ -0,0 +1,27 @@ +// 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; + } + + .options-container { + flex: 0 1 auto; + max-height: calc(3.2rem * 8); + overflow-y: auto; + + .option { + height: 3.2rem; + } + } +} \ No newline at end of file