From f4ab95e0c6580d6d494e2871c7c9e33fadc94efa Mon Sep 17 00:00:00 2001
From: Aztup <38fight.gaming06@gmail.com>
Date: Tue, 26 May 2026 19:46:10 +0200
Subject: [PATCH 1/5] feat(player): add custom speed input
---
src/routes/Player/SpeedMenu/SpeedMenu.js | 26 ++++++++++++++++++++
src/routes/Player/SpeedMenu/styles.less | 31 +++++++++++++++++++++++-
2 files changed, 56 insertions(+), 1 deletion(-)
diff --git a/src/routes/Player/SpeedMenu/SpeedMenu.js b/src/routes/Player/SpeedMenu/SpeedMenu.js
index 501020e362..15ece6fa61 100644
--- a/src/routes/Player/SpeedMenu/SpeedMenu.js
+++ b/src/routes/Player/SpeedMenu/SpeedMenu.js
@@ -5,6 +5,9 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const Option = require('./Option');
+const TextInput = require('stremio/components/TextInput').default;
+const Button = require('stremio/components/Button').default;
+
const styles = require('./styles');
const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25).reverse();
@@ -19,6 +22,18 @@ const SpeedMenu = React.memo(React.forwardRef(({ className, playbackSpeed, onPla
onPlaybackSpeedChanged(value);
}
}, [onPlaybackSpeedChanged]);
+ const [customSpeedInput, setCustomSpeedInput] = React.useState();
+ const applyCustomSpeed = React.useCallback((value) => {
+ if (typeof onPlaybackSpeedChanged === 'function') {
+ let num = parseFloat(customSpeedInput.value)
+
+ if (num && !Number.isNaN(num) && isFinite(num) && num >= 0) {
+ onPlaybackSpeedChanged(num);
+ } else {
+ customSpeedInput.value = playbackSpeed
+ }
+ }
+ }, [onPlaybackSpeedChanged, playbackSpeed, customSpeedInput]);
return (
@@ -36,6 +51,17 @@ const SpeedMenu = React.memo(React.forwardRef(({ className, playbackSpeed, onPla
/>
))
}
+
+
+
+
);
diff --git a/src/routes/Player/SpeedMenu/styles.less b/src/routes/Player/SpeedMenu/styles.less
index c2a694629d..be70888d8c 100644
--- a/src/routes/Player/SpeedMenu/styles.less
+++ b/src/routes/Player/SpeedMenu/styles.less
@@ -15,11 +15,40 @@
.options-container {
flex: 0 1 auto;
- max-height: calc(3.2rem * 10);
+ max-height: calc(3.2rem * 11);
padding: 0 1rem 0.5rem;
.option {
height: 3.2rem;
}
}
+
+ .custom-speed-container {
+ display: flex;
+ gap: 0.5rem;
+
+ .custom-speed-input {
+ padding: 1.5rem 2rem;
+ color: var(--primary-foreground-color);
+ flex: 1;
+ padding: 0.5rem;
+ border: 1px solid var(--overlay-color);
+ border-radius: var(--border-radius);
+ }
+
+ .custom-speed-button {
+ &:hover,
+ &:focus {
+ background-color: var(--overlay-color);
+ }
+
+ border-radius: var(--border-radius);
+ padding: 0.5rem;
+ font-size: 1rem;
+ font-weight: 300;
+ color: var(--primary-foreground-color);
+ text-transform: capitalize;
+ }
+
+ }
}
\ No newline at end of file
From 183215cd46bf74af748b57a901d7ca73f41b2a85 Mon Sep 17 00:00:00 2001
From: Aztup <38fight.gaming06@gmail.com>
Date: Tue, 26 May 2026 19:46:41 +0200
Subject: [PATCH 2/5] fix: lint
---
src/routes/Player/SpeedMenu/SpeedMenu.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/routes/Player/SpeedMenu/SpeedMenu.js b/src/routes/Player/SpeedMenu/SpeedMenu.js
index 15ece6fa61..f9157547e1 100644
--- a/src/routes/Player/SpeedMenu/SpeedMenu.js
+++ b/src/routes/Player/SpeedMenu/SpeedMenu.js
@@ -23,14 +23,14 @@ const SpeedMenu = React.memo(React.forwardRef(({ className, playbackSpeed, onPla
}
}, [onPlaybackSpeedChanged]);
const [customSpeedInput, setCustomSpeedInput] = React.useState();
- const applyCustomSpeed = React.useCallback((value) => {
+ const applyCustomSpeed = React.useCallback(() => {
if (typeof onPlaybackSpeedChanged === 'function') {
- let num = parseFloat(customSpeedInput.value)
+ let num = parseFloat(customSpeedInput.value);
if (num && !Number.isNaN(num) && isFinite(num) && num >= 0) {
onPlaybackSpeedChanged(num);
} else {
- customSpeedInput.value = playbackSpeed
+ customSpeedInput.value = playbackSpeed;
}
}
}, [onPlaybackSpeedChanged, playbackSpeed, customSpeedInput]);
From 2c17b2ca0c786cd54d36ced07e18f2476c0728c7 Mon Sep 17 00:00:00 2001
From: Aztup <38fight.gaming06@gmail.com>
Date: Fri, 29 May 2026 16:07:04 +0200
Subject: [PATCH 3/5] feat(slider): Add step prop
---
src/components/Slider/Slider.js | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/components/Slider/Slider.js b/src/components/Slider/Slider.js
index 5ad113798c..1801970e6f 100644
--- a/src/components/Slider/Slider.js
+++ b/src/components/Slider/Slider.js
@@ -8,7 +8,7 @@ const useAnimationFrame = require('stremio/common/useAnimationFrame');
const useLiveRef = require('stremio/common/useLiveRef');
const styles = require('./styles');
-const Slider = ({ className, value, buffered, minimumValue, maximumValue, disabled, onSlide, onComplete, audioBoost }) => {
+const Slider = ({ className, value, buffered, minimumValue, maximumValue, disabled, onSlide, onComplete, audioBoost, stepValue }) => {
const minimumValueRef = useLiveRef(minimumValue !== null && !isNaN(minimumValue) ? minimumValue : 0);
const maximumValueRef = useLiveRef(maximumValue !== null && !isNaN(maximumValue) ? maximumValue : 100);
const valueRef = useLiveRef(value !== null && !isNaN(value) ? Math.min(maximumValueRef.current, Math.max(minimumValueRef.current, value)) : 0);
@@ -26,7 +26,8 @@ const Slider = ({ className, value, buffered, minimumValue, maximumValue, disabl
const { x: sliderX, width: sliderWidth } = sliderContainerRef.current.getBoundingClientRect();
const thumbStart = Math.min(Math.max(mouseX - sliderX, 0), sliderWidth);
const value = (thumbStart / sliderWidth) * (maximumValueRef.current - minimumValueRef.current) + minimumValueRef.current;
- return value;
+ const noramlizedValue = stepValue ? parseFloat((Math.round(value / stepValue) * stepValue).toFixed(2)) : value;
+ return noramlizedValue;
}, []);
const retainThumb = React.useCallback(() => {
window.addEventListener('blur', onBlur);
@@ -158,6 +159,7 @@ Slider.propTypes = {
buffered: PropTypes.number,
minimumValue: PropTypes.number,
maximumValue: PropTypes.number,
+ stepValue: PropTypes.number,
disabled: PropTypes.bool,
onSlide: PropTypes.func,
onComplete: PropTypes.func,
From 8fb6186cd46c3d46441630adf972baf9bd5d9b63 Mon Sep 17 00:00:00 2001
From: Aztup <38fight.gaming06@gmail.com>
Date: Fri, 29 May 2026 16:07:49 +0200
Subject: [PATCH 4/5] feat(SpeedMenu): revamp to slider with presets
---
src/routes/Player/SpeedMenu/SpeedMenu.js | 67 +++++++---------
src/routes/Player/SpeedMenu/styles.less | 99 ++++++++++++++++--------
2 files changed, 96 insertions(+), 70 deletions(-)
diff --git a/src/routes/Player/SpeedMenu/SpeedMenu.js b/src/routes/Player/SpeedMenu/SpeedMenu.js
index f9157547e1..90b29047e9 100644
--- a/src/routes/Player/SpeedMenu/SpeedMenu.js
+++ b/src/routes/Player/SpeedMenu/SpeedMenu.js
@@ -4,63 +4,54 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
-const Option = require('./Option');
-const TextInput = require('stremio/components/TextInput').default;
const Button = require('stremio/components/Button').default;
+const Slider = require('stremio/components/Slider');
const styles = require('./styles');
-
-const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25).reverse();
+const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25);
const SpeedMenu = React.memo(React.forwardRef(({ className, playbackSpeed, onPlaybackSpeedChanged }, ref) => {
const { t } = useTranslation();
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.speedMenuClosePrevented = true;
}, []);
- const onOptionSelect = React.useCallback((value) => {
+ const onSpeedChanged = React.useCallback((value) => {
if (typeof onPlaybackSpeedChanged === 'function') {
onPlaybackSpeedChanged(value);
}
- }, [onPlaybackSpeedChanged]);
- const [customSpeedInput, setCustomSpeedInput] = React.useState();
- const applyCustomSpeed = React.useCallback(() => {
- if (typeof onPlaybackSpeedChanged === 'function') {
- let num = parseFloat(customSpeedInput.value);
+ }, [onPlaybackSpeedChanged, playbackSpeed]);
- if (num && !Number.isNaN(num) && isFinite(num) && num >= 0) {
- onPlaybackSpeedChanged(num);
- } else {
- customSpeedInput.value = playbackSpeed;
- }
- }
- }, [onPlaybackSpeedChanged, playbackSpeed, customSpeedInput]);
return (
{ t('PLAYBACK_SPEED') }
-
- {
- RATES.map((rate) => (
-
- ))
- }
-
-
+
+
+ {playbackSpeed}x
+
+
+
-
+
+
+
+ {
+ RATES.map((rate) => (
+
+ ))
+ }
diff --git a/src/routes/Player/SpeedMenu/styles.less b/src/routes/Player/SpeedMenu/styles.less
index be70888d8c..7002972b59 100644
--- a/src/routes/Player/SpeedMenu/styles.less
+++ b/src/routes/Player/SpeedMenu/styles.less
@@ -2,53 +2,88 @@
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
+:import('~stremio/components/Slider/styles.less') {
+ slider-track-after: track-after;
+ slider-thumb: thumb;
+}
+
.speed-menu-container {
- width: 14rem;
+ width: 30rem;
+ height: 10rem;
+ display: flex;
+ flex-direction: column;
.title {
flex: none;
align-self: stretch;
font-weight: 700;
color: var(--primary-foreground-color);
- padding: 1.5rem 2rem;
+ padding: 1rem;
+ text-align: center;
}
- .options-container {
- flex: 0 1 auto;
- max-height: calc(3.2rem * 11);
- padding: 0 1rem 0.5rem;
-
- .option {
- height: 3.2rem;
+ .main-container {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+
+ .options-container {
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+
+ .speed-button {
+ color: var(--primary-foreground-color);
+ padding: 0.2rem 0.75rem;
+ border-radius: var(--border-radius);
+ transition: background-color 150ms ease;
+
+ &:hover, &:focus {
+ background-color: var(--overlay-color);
+ }
+ }
}
- }
- .custom-speed-container {
- display: flex;
- gap: 0.5rem;
-
- .custom-speed-input {
- padding: 1.5rem 2rem;
- color: var(--primary-foreground-color);
- flex: 1;
- padding: 0.5rem;
- border: 1px solid var(--overlay-color);
- border-radius: var(--border-radius);
+ .top-container {
+ display: flex;
+ flex-direction: column;
+ flex: 0.75;
+ gap: 0.5rem;
+
+ .speed-title {
+ font-weight: 700;
+ color: var(--primary-foreground-color);
+ text-align: center;
+ }
}
- .custom-speed-button {
- &:hover,
- &:focus {
- background-color: var(--overlay-color);
+ .slider {
+ --track-size: 0.35rem;
+ --thumb-size: 1rem;
+
+ flex: 0.5;
+ align-self: stretch;
+ margin: 0 var(--thumb-size);
+
+ .slider-thumb {
+ background-color: @color-secondaryvariant1-light4;
+ transition: transform 150ms ease;
+
+ &:after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ border-radius: 100%;
+ box-shadow: 0 0 0 0.25rem white inset;
+ }
}
- border-radius: var(--border-radius);
- padding: 0.5rem;
- font-size: 1rem;
- font-weight: 300;
- color: var(--primary-foreground-color);
- text-transform: capitalize;
+ &:hover .slider-thumb {
+ transform: translateX(-50%) scale(1.2);
+ }
}
-
}
}
\ No newline at end of file
From 3c73dac20ddb796921549f55aab4f30e6fb4e785 Mon Sep 17 00:00:00 2001
From: Aztup <38fight.gaming06@gmail.com>
Date: Sun, 12 Jul 2026 14:21:22 +0200
Subject: [PATCH 5/5] feat(SpeedMenu): add active class to button
feat(SpeedMenu): fix mobile overflow
---
src/routes/Player/SpeedMenu/SpeedMenu.js | 2 +-
src/routes/Player/SpeedMenu/styles.less | 21 ++++++++++++++++-----
2 files changed, 17 insertions(+), 6 deletions(-)
diff --git a/src/routes/Player/SpeedMenu/SpeedMenu.js b/src/routes/Player/SpeedMenu/SpeedMenu.js
index 90b29047e9..b1d927205e 100644
--- a/src/routes/Player/SpeedMenu/SpeedMenu.js
+++ b/src/routes/Player/SpeedMenu/SpeedMenu.js
@@ -47,7 +47,7 @@ const SpeedMenu = React.memo(React.forwardRef(({ className, playbackSpeed, onPla
{
RATES.map((rate) => (
-