mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-08-20 07:17:20 +00:00
Merge pull request #1291 from Aztup/feat/customPlaybackSpeed
Player: Redesign Speed Menu
This commit is contained in:
commit
ab2f9527c7
6 changed files with 130 additions and 106 deletions
|
|
@ -8,9 +8,10 @@ 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 stepValueRef = useLiveRef(stepValue !== null && !isNaN(stepValue) ? stepValue : null);
|
||||
const valueRef = useLiveRef(value !== null && !isNaN(value) ? Math.min(maximumValueRef.current, Math.max(minimumValueRef.current, value)) : 0);
|
||||
const bufferedRef = useLiveRef(buffered !== null && !isNaN(buffered) ? Math.min(maximumValueRef.current, Math.max(minimumValueRef.current, buffered)) : 0);
|
||||
const onSlideRef = useLiveRef(onSlide);
|
||||
|
|
@ -26,7 +27,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 normalizedValue = stepValueRef.current ? parseFloat((Math.round(value / stepValueRef.current) * stepValueRef.current).toFixed(2)) : value;
|
||||
return normalizedValue;
|
||||
}, []);
|
||||
const retainThumb = React.useCallback(() => {
|
||||
window.addEventListener('blur', onBlur);
|
||||
|
|
@ -158,6 +160,7 @@ Slider.propTypes = {
|
|||
buffered: PropTypes.number,
|
||||
minimumValue: PropTypes.number,
|
||||
maximumValue: PropTypes.number,
|
||||
stepValue: PropTypes.number,
|
||||
disabled: PropTypes.bool,
|
||||
onSlide: PropTypes.func,
|
||||
onComplete: PropTypes.func,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Button
|
||||
className={classnames(className, styles['option'], { 'selected': selected })}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className={styles['label']}>{ value }x</div>
|
||||
<div className={styles['icon']} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
OptionButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.number,
|
||||
selected: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
module.exports = OptionButton;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const Option = require('./Option');
|
||||
|
||||
module.exports = Option;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,38 +4,58 @@ const React = require('react');
|
|||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const { useTranslation } = require('react-i18next');
|
||||
const Option = require('./Option');
|
||||
const styles = require('./styles');
|
||||
const Button = require('stremio/components/Button').default;
|
||||
const Slider = require('stremio/components/Slider');
|
||||
|
||||
const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25).reverse();
|
||||
const styles = require('./styles');
|
||||
const RATES = [0.25, 0.5, 1, 1.5, 2, 2.5, 3, 4];
|
||||
|
||||
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 onSpeedButtonClick = React.useCallback((event) => {
|
||||
onSpeedChanged(Number(event.currentTarget.dataset.rate));
|
||||
}, [onSpeedChanged]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={classnames(className, styles['speed-menu-container'])} onMouseDown={onMouseDown}>
|
||||
<div className={styles['title']}>
|
||||
{ t('PLAYBACK_SPEED') }
|
||||
</div>
|
||||
<div className={styles['options-container']}>
|
||||
{
|
||||
RATES.map((rate) => (
|
||||
<Option
|
||||
className={styles['option']}
|
||||
key={rate}
|
||||
value={rate}
|
||||
selected={rate === playbackSpeed}
|
||||
onSelect={onOptionSelect}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
<div className={styles['main-container']}>
|
||||
<div className={styles['top-container']}>
|
||||
<div className={styles['speed-title']}>
|
||||
{playbackSpeed}x
|
||||
</div>
|
||||
|
||||
<Slider
|
||||
className={styles['slider']}
|
||||
value={playbackSpeed}
|
||||
minimumValue={0.1}
|
||||
maximumValue={4}
|
||||
stepValue={0.1}
|
||||
onSlide={onSpeedChanged}
|
||||
onComplete={onSpeedChanged}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles['options-container']}>
|
||||
{
|
||||
RATES.map((rate) => (
|
||||
<Button className={classnames(styles['speed-button'], {[styles['active']]: rate === playbackSpeed})} data-rate={rate} key={rate} onClick={onSpeedButtonClick}>
|
||||
{rate}x
|
||||
</Button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,24 +2,103 @@
|
|||
|
||||
@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;
|
||||
max-width: 100%;
|
||||
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 * 10);
|
||||
padding: 0 1rem 0.5rem;
|
||||
|
||||
.option {
|
||||
height: 3.2rem;
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
.options-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
padding: 0 1rem;
|
||||
|
||||
.speed-button {
|
||||
flex: 1 0 3rem;
|
||||
color: var(--primary-foreground-color);
|
||||
padding: 0.2rem 0.25rem;
|
||||
border-radius: var(--border-radius);
|
||||
text-align: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: none;
|
||||
min-height: 3rem;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
.speed-title {
|
||||
font-weight: 700;
|
||||
color: var(--primary-foreground-color);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
--track-size: 0.35rem;
|
||||
--thumb-size: 1rem;
|
||||
|
||||
flex: none;
|
||||
min-height: calc(var(--thumb-size) * 2);
|
||||
align-self: stretch;
|
||||
margin: 0.5rem var(--thumb-size) 0;
|
||||
position: relative;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .slider-thumb {
|
||||
transform: translateX(-50%) scale(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue