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) => ( -
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) => ( - )) diff --git a/src/routes/Player/SpeedMenu/styles.less b/src/routes/Player/SpeedMenu/styles.less index 7002972b59..243ae7cf5f 100644 --- a/src/routes/Player/SpeedMenu/styles.less +++ b/src/routes/Player/SpeedMenu/styles.less @@ -9,7 +9,7 @@ .speed-menu-container { width: 30rem; - height: 10rem; + max-width: 100%; display: flex; flex-direction: column; @@ -26,6 +26,7 @@ display: flex; flex-direction: column; flex: 1; + padding-bottom: 1rem; .options-container { display: flex; @@ -36,19 +37,27 @@ color: var(--primary-foreground-color); padding: 0.2rem 0.75rem; border-radius: var(--border-radius); - transition: background-color 150ms ease; + transition: background-color 150ms ease, color 150ms ease; &:hover, &:focus { background-color: var(--overlay-color); } + + &.active { + background-color: var(--overlay-color); + color: @color-secondaryvariant1-light4; + font-weight: 700; + } } } .top-container { display: flex; flex-direction: column; - flex: 0.75; + flex: none; + min-height: 3rem; gap: 0.5rem; + padding: 0.5rem 0; .speed-title { font-weight: 700; @@ -61,9 +70,11 @@ --track-size: 0.35rem; --thumb-size: 1rem; - flex: 0.5; + flex: none; + min-height: calc(var(--thumb-size) * 2); align-self: stretch; - margin: 0 var(--thumb-size); + margin: 0.5rem var(--thumb-size) 0; + position: relative; .slider-thumb { background-color: @color-secondaryvariant1-light4;