From f5d2943783e6e2e4037ab33993495c716ac9ec57 Mon Sep 17 00:00:00 2001 From: AK <144495202+AKnassa@users.noreply.github.com> Date: Sat, 13 Jun 2026 23:23:28 -0400 Subject: [PATCH 1/3] feat(Player): redesign playback speed menu Replace the vertical rate list with a live readout, a slider between minus/plus steppers, and preset chips for quick selection. Reuses the shared Slider and Button; removes the old Option chip. --- src/routes/Player/SpeedMenu/Option/Option.js | 33 ---- src/routes/Player/SpeedMenu/Option/index.js | 5 - .../Player/SpeedMenu/Option/styles.less | 40 ----- src/routes/Player/SpeedMenu/SpeedMenu.js | 94 ++++++++-- src/routes/Player/SpeedMenu/styles.less | 160 ++++++++++++++++-- 5 files changed, 229 insertions(+), 103 deletions(-) delete mode 100644 src/routes/Player/SpeedMenu/Option/Option.js delete mode 100644 src/routes/Player/SpeedMenu/Option/index.js delete mode 100644 src/routes/Player/SpeedMenu/Option/styles.less diff --git a/src/routes/Player/SpeedMenu/Option/Option.js b/src/routes/Player/SpeedMenu/Option/Option.js deleted file mode 100644 index 1bcde69709..0000000000 --- a/src/routes/Player/SpeedMenu/Option/Option.js +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const React = require('react'); -const PropTypes = require('prop-types'); -const classnames = require('classnames'); -const { Button } = require('stremio/components'); -const styles = require('./styles'); - -const OptionButton = ({ className, value, selected, onSelect }) => { - 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 deleted file mode 100644 index 368faa94f5..0000000000 --- a/src/routes/Player/SpeedMenu/Option/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 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 deleted file mode 100644 index 77b3519090..0000000000 --- a/src/routes/Player/SpeedMenu/Option/styles.less +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2017-2023 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; - margin-bottom: 0.5rem; - border-radius: var(--border-radius); - - &:global(.selected) { - background-color: var(--overlay-color); - - .icon { - display: block; - } - } - - &:hover, &:focus { - background-color: var(--overlay-color); - } - - .label { - flex: 1; - font-weight: 400; - color: var(--primary-foreground-color); - } - - .icon { - flex: none; - display: none; - width: 0.5rem; - height: 0.5rem; - border-radius: 100%; - margin-left: 1rem; - background-color: var(--secondary-accent-color); - } -} \ No newline at end of file diff --git a/src/routes/Player/SpeedMenu/SpeedMenu.js b/src/routes/Player/SpeedMenu/SpeedMenu.js index 501020e362..d13fa14a53 100644 --- a/src/routes/Player/SpeedMenu/SpeedMenu.js +++ b/src/routes/Player/SpeedMenu/SpeedMenu.js @@ -1,39 +1,105 @@ -// Copyright (C) 2017-2023 Smart code 203358507 +// Copyright (C) 2017-2024 Smart code 203358507 const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { useTranslation } = require('react-i18next'); -const Option = require('./Option'); +const { default: Icon } = require('@stremio/stremio-icons/react'); +const { Button, Slider } = require('stremio/components'); const styles = require('./styles'); -const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25).reverse(); +const MIN_SPEED = 0.25; +const MAX_SPEED = 2; +const SPEED_STEP = 0.25; +const PRESETS = [0.5, 1, 1.25, 1.5, 2]; + +const snap = (value) => { + const clamped = Math.min(MAX_SPEED, Math.max(MIN_SPEED, value)); + return Math.round(clamped / SPEED_STEP) * SPEED_STEP; +}; + +const formatPreset = (value) => value % 1 === 0 ? value.toFixed(1) : String(value); const SpeedMenu = React.memo(React.forwardRef(({ className, playbackSpeed, onPlaybackSpeedChanged }, ref) => { const { t } = useTranslation(); + const speed = typeof playbackSpeed === 'number' && !isNaN(playbackSpeed) ? snap(playbackSpeed) : 1; + const [sliding, setSliding] = React.useState(null); + const displaySpeed = sliding !== null ? snap(sliding) : speed; + const canDecrease = displaySpeed > MIN_SPEED; + const canIncrease = displaySpeed < MAX_SPEED; const onMouseDown = React.useCallback((event) => { event.nativeEvent.speedMenuClosePrevented = true; }, []); - const onOptionSelect = React.useCallback((value) => { + const setSpeed = React.useCallback((value) => { if (typeof onPlaybackSpeedChanged === 'function') { - onPlaybackSpeedChanged(value); + onPlaybackSpeedChanged(snap(value)); } }, [onPlaybackSpeedChanged]); + const onDecrease = React.useCallback(() => { + if (canDecrease) { + setSpeed(displaySpeed - SPEED_STEP); + } + }, [canDecrease, displaySpeed, setSpeed]); + const onIncrease = React.useCallback(() => { + if (canIncrease) { + setSpeed(displaySpeed + SPEED_STEP); + } + }, [canIncrease, displaySpeed, setSpeed]); + const onSlide = React.useCallback((value) => { + setSliding(value); + setSpeed(value); + }, [setSpeed]); + const onComplete = React.useCallback((value) => { + setSliding(null); + setSpeed(value); + }, [setSpeed]); return (
{ t('PLAYBACK_SPEED') }
-
+
+
{ displaySpeed.toFixed(2) }x
+
+ { displaySpeed === 1 ? t('PLAYBACK_SPEED_NORMAL', 'Normal') : ' ' } +
+
+
+ + + +
+
{ - RATES.map((rate) => ( -
diff --git a/src/routes/Player/SpeedMenu/styles.less b/src/routes/Player/SpeedMenu/styles.less index c2a694629d..511bd7dd33 100644 --- a/src/routes/Player/SpeedMenu/styles.less +++ b/src/routes/Player/SpeedMenu/styles.less @@ -1,25 +1,163 @@ // Copyright (C) 2017-2024 Smart code 203358507 @import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; +@import (reference) '~stremio/common/screen-sizes.less'; + +:import('~stremio/components/Slider/styles.less') { + slider-track: track; + slider-track-after: track-after; + slider-thumb: thumb; +} .speed-menu-container { - width: 14rem; + display: flex; + flex-direction: column; + width: 22rem; + max-width: 100%; + padding: 1.5rem 1.5rem 1.75rem; + gap: 1.25rem; .title { flex: none; - align-self: stretch; font-weight: 700; color: var(--primary-foreground-color); - padding: 1.5rem 2rem; } - .options-container { - flex: 0 1 auto; - max-height: calc(3.2rem * 10); - padding: 0 1rem 0.5rem; - - .option { - height: 3.2rem; + .readout { + flex: none; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.1rem; + + .value { + font-weight: 700; + font-size: 2rem; + line-height: 1.1; + color: var(--primary-foreground-color); + font-variant-numeric: tabular-nums; + } + + .descriptor { + min-height: 1.2rem; + font-size: 0.9rem; + color: var(--primary-foreground-color); + opacity: 0.65; + } + } + + .stepper { + flex: none; + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; + + --track-size: 0.4rem; + --thumb-size: 1.2rem; + + .step-button { + flex: none; + display: flex; + align-items: center; + justify-content: center; + width: 2.6rem; + height: 2.6rem; + border-radius: 100%; + background-color: var(--overlay-color); + transition: filter 150ms ease, transform 150ms ease; + + .icon { + width: 1.1rem; + height: 1.1rem; + color: var(--primary-foreground-color); + } + + &:hover, &:focus { + filter: brightness(1.4); + outline: none; + } + + &:focus { + box-shadow: 0 0 0 0.2rem var(--primary-foreground-color); + } + + &:active { + transform: scale(0.92); + } + + &:global(.disabled) { + pointer-events: none; + opacity: 0.35; + } + } + + .slider { + flex: 1; + height: var(--thumb-size); + + .slider-track { + background-color: var(--overlay-color); + opacity: 1; + } + + .slider-track-after { + background-color: var(--primary-foreground-color); + } + + .slider-thumb { + background-color: var(--primary-foreground-color); + transition: transform 150ms ease; + } + + &:hover .slider-thumb { + transform: translateX(-50%) scale(1.2); + } + } + } + + .presets { + flex: none; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 0.5rem; + + .preset { + flex: 1 1 auto; + display: flex; + align-items: center; + justify-content: center; + min-width: 3rem; + height: 2.4rem; + padding: 0 0.75rem; + border-radius: 1.2rem; + font-weight: 600; + color: var(--primary-foreground-color); + background-color: var(--overlay-color); + font-variant-numeric: tabular-nums; + transition: filter 150ms ease, background-color 150ms ease, color 150ms ease; + + &:hover, &:focus { + filter: brightness(1.4); + outline: none; + } + + &:focus { + box-shadow: 0 0 0 0.2rem var(--primary-foreground-color); + } + + &:global(.selected) { + color: var(--modal-background-color); + background-color: var(--primary-foreground-color); + filter: none; + } } } -} \ No newline at end of file +} + +@media (orientation: portrait) and (max-width: @minimum) { + .speed-menu-container { + width: 18rem; + } +} From d5a3fa3342825dd256439f6d2ce66f51d4e5285b Mon Sep 17 00:00:00 2001 From: AK <144495202+AKnassa@users.noreply.github.com> Date: Sat, 13 Jun 2026 23:57:11 -0400 Subject: [PATCH 2/3] fix(Player): make speed menu responsive Fluid width and a single-row chip grid for phones, a compact 3-row grid in short landscape so chips no longer clip, larger touch targets on coarse pointers, and a prefers-reduced-motion guard. --- src/routes/Player/SpeedMenu/styles.less | 99 ++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/src/routes/Player/SpeedMenu/styles.less b/src/routes/Player/SpeedMenu/styles.less index 511bd7dd33..9d9bde5d11 100644 --- a/src/routes/Player/SpeedMenu/styles.less +++ b/src/routes/Player/SpeedMenu/styles.less @@ -12,7 +12,7 @@ .speed-menu-container { display: flex; flex-direction: column; - width: 22rem; + width: min(22rem, calc(100vw - 5rem)); max-width: 100%; padding: 1.5rem 1.5rem 1.75rem; gap: 1.25rem; @@ -118,19 +118,17 @@ .presets { flex: none; - display: flex; - flex-direction: row; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(5, 1fr); gap: 0.5rem; .preset { - flex: 1 1 auto; display: flex; align-items: center; justify-content: center; - min-width: 3rem; + min-width: 0; height: 2.4rem; - padding: 0 0.75rem; + padding: 0 0.5rem; border-radius: 1.2rem; font-weight: 600; color: var(--primary-foreground-color); @@ -156,8 +154,91 @@ } } -@media (orientation: portrait) and (max-width: @minimum) { +@media (pointer: coarse) { + .speed-menu-container { + .stepper { + .step-button { + width: 2.9rem; + height: 2.9rem; + } + + .slider { + height: 2.75rem; + } + } + + .presets .preset { + height: 2.9rem; + } + } +} + +@media (prefers-reduced-motion: reduce) { + .speed-menu-container { + .stepper .step-button, + .stepper .slider .slider-thumb, + .presets .preset { + transition: none; + } + + .stepper .step-button:active { + transform: none; + } + + .stepper .slider:hover .slider-thumb { + transform: translateX(-50%); + } + } +} + +@media (orientation: landscape) and (max-height: 600px) { .speed-menu-container { - width: 18rem; + display: grid; + grid-template-columns: 1fr auto; + grid-template-areas: + "title readout" + "stepper stepper" + "presets presets"; + align-items: center; + column-gap: 1rem; + row-gap: 0.6rem; + padding: 0.85rem 1.1rem 1rem; + + .title { + grid-area: title; + } + + .readout { + grid-area: readout; + flex-direction: row; + align-items: baseline; + justify-content: flex-end; + gap: 0.4rem; + + .value { + font-size: 1.5rem; + } + + .descriptor { + min-height: 0; + } + } + + .stepper { + grid-area: stepper; + + .step-button { + width: 2.75rem; + height: 2.75rem; + } + } + + .presets { + grid-area: presets; + + .preset { + height: 2.75rem; + } + } } } From 9c3d2a76d4fb8379fe5ca064047295cd6fec456a Mon Sep 17 00:00:00 2001 From: AK <144495202+AKnassa@users.noreply.github.com> Date: Sun, 14 Jun 2026 01:55:34 -0400 Subject: [PATCH 3/3] chore(SpeedMenu): bump copyright year to 2026 --- src/routes/Player/SpeedMenu/SpeedMenu.js | 2 +- src/routes/Player/SpeedMenu/styles.less | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/Player/SpeedMenu/SpeedMenu.js b/src/routes/Player/SpeedMenu/SpeedMenu.js index d13fa14a53..4293434b54 100644 --- a/src/routes/Player/SpeedMenu/SpeedMenu.js +++ b/src/routes/Player/SpeedMenu/SpeedMenu.js @@ -1,4 +1,4 @@ -// Copyright (C) 2017-2024 Smart code 203358507 +// Copyright (C) 2017-2026 Smart code 203358507 const React = require('react'); const PropTypes = require('prop-types'); diff --git a/src/routes/Player/SpeedMenu/styles.less b/src/routes/Player/SpeedMenu/styles.less index 9d9bde5d11..c60a17b803 100644 --- a/src/routes/Player/SpeedMenu/styles.less +++ b/src/routes/Player/SpeedMenu/styles.less @@ -1,4 +1,4 @@ -// Copyright (C) 2017-2024 Smart code 203358507 +// Copyright (C) 2017-2026 Smart code 203358507 @import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; @import (reference) '~stremio/common/screen-sizes.less';