From 595e411e06a322f439c65def72ca240716542d12 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 25 Oct 2022 16:07:46 +0200 Subject: [PATCH 01/10] feat(Player): implement playback speed controls --- package.json | 2 +- src/routes/Player/ControlBar/ControlBar.js | 14 +++++- src/routes/Player/Player.js | 37 ++++++++++++---- src/routes/Player/SpeedMenu/SpeedMenu.js | 50 ++++++++++++++++++++++ src/routes/Player/SpeedMenu/index.js | 5 +++ src/routes/Player/SpeedMenu/styles.less | 21 +++++++++ 6 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 src/routes/Player/SpeedMenu/SpeedMenu.js create mode 100644 src/routes/Player/SpeedMenu/index.js create mode 100644 src/routes/Player/SpeedMenu/styles.less 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 From ba30c5d0bcf9561e1cadeeb201f145cc6957d434 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 27 Oct 2022 15:22:21 +0200 Subject: [PATCH 02/10] refactor(Player): declare playbackSpeed in videoState --- src/routes/Player/Player.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 190d9458e..3c98f92c1 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -52,6 +52,7 @@ const Player = ({ urlParams, queryParams }) => { buffering: null, volume: null, muted: null, + playbackSpeed: null, audioTracks: [], selectedAudioTrackId: null, subtitlesTracks: [], From f1b852687a494bc8c942046f6d457fdc5544e1db Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 27 Oct 2022 15:24:57 +0200 Subject: [PATCH 03/10] fix(Player): handle mousedown event for speed button --- src/routes/Player/ControlBar/ControlBar.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js index d4b922281..c0efbc130 100644 --- a/src/routes/Player/ControlBar/ControlBar.js +++ b/src/routes/Player/ControlBar/ControlBar.js @@ -42,6 +42,9 @@ const ControlBar = ({ const onInfoButtonMouseDown = React.useCallback((event) => { event.nativeEvent.infoMenuClosePrevented = true; }, []); + const onSpeedButtonMouseDown = React.useCallback((event) => { + event.nativeEvent.speedMenuClosePrevented = true; + }, []); const onPlayPauseButtonClick = React.useCallback(() => { if (paused) { if (typeof onPlayRequested === 'function') { @@ -125,7 +128,7 @@ const ControlBar = ({
- + ); +}; + +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 index c03b0cb58..836a9ac80 100644 --- a/src/routes/Player/SpeedMenu/SpeedMenu.js +++ b/src/routes/Player/SpeedMenu/SpeedMenu.js @@ -3,7 +3,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); -const { Multiselect } = require('stremio/common'); +const Option = require('./Option'); const styles = require('./styles'); const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25).reverse(); @@ -12,31 +12,29 @@ 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)); + const onOptionSelect = React.useCallback((value) => { + if (typeof onPlaybackSpeedChanged === 'function') { + onPlaybackSpeedChanged(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
- +
+ { + RATES.map((rate) => ( +
); }; diff --git a/src/routes/Player/SpeedMenu/styles.less b/src/routes/Player/SpeedMenu/styles.less index 39fc5677f..fbb47ed00 100644 --- a/src/routes/Player/SpeedMenu/styles.less +++ b/src/routes/Player/SpeedMenu/styles.less @@ -15,7 +15,13 @@ margin: 1rem; } - .select-input-container { - padding: 1rem 1.5rem; + .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 From a98ff5d8e855cc21d97356e9d166b34c160e8b4e Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 26 Nov 2022 14:03:16 +0100 Subject: [PATCH 09/10] fix(Player): close seed and videos menu on shortcuts --- src/routes/Player/Player.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 919537f7e..bfa5e4d7c 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -448,6 +448,7 @@ const Player = ({ urlParams, queryParams }) => { case 'KeyR': { closeInfoMenu(); closeSubtitlesMenu(); + closeVideosMenu(); if (player.playbackSpeed !== null) { toggleSpeedMenu(); } @@ -457,6 +458,7 @@ const Player = ({ urlParams, queryParams }) => { case 'KeyV': { closeInfoMenu(); closeSubtitlesMenu(); + closeSpeedMenu(); if (player.metaItem !== null && player.metaItem.type === 'Ready') { toggleVideosMenu(); } From 9ead12125186e59a1add88dc15aa073e1c8fe8e3 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 26 Nov 2022 14:04:16 +0100 Subject: [PATCH 10/10] fix(Player): use videoState instead of player for playbackSpeed --- src/routes/Player/Player.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index bfa5e4d7c..54e6d37ea 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -449,7 +449,7 @@ const Player = ({ urlParams, queryParams }) => { closeInfoMenu(); closeSubtitlesMenu(); closeVideosMenu(); - if (player.playbackSpeed !== null) { + if (videoState.playbackSpeed !== null) { toggleSpeedMenu(); } @@ -480,7 +480,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, 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();