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