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) => (
-
+ PRESETS.map((preset) => (
+
))
}
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';