mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
Merge pull request #965 from Stremio/feat/player-subtitles-settings-hold-click
Player: Support holding click for subtitles settings
This commit is contained in:
commit
85fea50c15
11 changed files with 200 additions and 131 deletions
|
|
@ -15,6 +15,7 @@ const routesRegexp = require('./routesRegexp');
|
|||
const useAnimationFrame = require('./useAnimationFrame');
|
||||
const useBinaryState = require('./useBinaryState');
|
||||
const { default: useFullscreen } = require('./useFullscreen');
|
||||
const { default: useInterval } = require('./useInterval');
|
||||
const useLiveRef = require('./useLiveRef');
|
||||
const useModelState = require('./useModelState');
|
||||
const useNotifications = require('./useNotifications');
|
||||
|
|
@ -23,6 +24,7 @@ const useProfile = require('./useProfile');
|
|||
const { default: useSettings } = require('./useSettings');
|
||||
const { default: useShell } = require('./useShell');
|
||||
const useStreamingServer = require('./useStreamingServer');
|
||||
const { default: useTimeout } = require('./useTimeout');
|
||||
const useTorrent = require('./useTorrent');
|
||||
const useTranslate = require('./useTranslate');
|
||||
const { default: useOrientation } = require('./useOrientation');
|
||||
|
|
@ -49,6 +51,7 @@ module.exports = {
|
|||
useAnimationFrame,
|
||||
useBinaryState,
|
||||
useFullscreen,
|
||||
useInterval,
|
||||
useLiveRef,
|
||||
useModelState,
|
||||
useNotifications,
|
||||
|
|
@ -57,6 +60,7 @@ module.exports = {
|
|||
useSettings,
|
||||
useShell,
|
||||
useStreamingServer,
|
||||
useTimeout,
|
||||
useTorrent,
|
||||
useTranslate,
|
||||
useOrientation,
|
||||
|
|
|
|||
26
src/common/useInterval.ts
Normal file
26
src/common/useInterval.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
|
||||
const useInterval = (duration: number) => {
|
||||
const interval = useRef<NodeJS.Timer | null>(null);
|
||||
|
||||
const start = (callback: () => void) => {
|
||||
cancel();
|
||||
interval.current = setInterval(callback, duration);
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
interval.current && clearInterval(interval.current);
|
||||
interval.current = null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => cancel();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
start,
|
||||
cancel,
|
||||
};
|
||||
};
|
||||
|
||||
export default useInterval;
|
||||
26
src/common/useTimeout.ts
Normal file
26
src/common/useTimeout.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
|
||||
const useTimeout = (duration: number) => {
|
||||
const timeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const start = (callback: () => void) => {
|
||||
cancel();
|
||||
timeout.current = setTimeout(callback, duration);
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
timeout.current && clearTimeout(timeout.current);
|
||||
timeout.current = null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => cancel();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
start,
|
||||
cancel,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTimeout;
|
||||
|
|
@ -16,6 +16,8 @@ type Props = {
|
|||
children: React.ReactNode,
|
||||
onKeyDown?: (event: React.KeyboardEvent) => void,
|
||||
onMouseDown?: (event: React.MouseEvent) => void,
|
||||
onMouseUp?: (event: React.MouseEvent) => void,
|
||||
onMouseLeave?: (event: React.MouseEvent) => void,
|
||||
onLongPress?: () => void,
|
||||
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void,
|
||||
onDoubleClick?: () => void,
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useTranslation } = require('react-i18next');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||
const { Button } = require('stremio/components');
|
||||
const styles = require('./styles');
|
||||
|
||||
const DiscreteSelectInput = ({ className, value, label, disabled, dataset, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const buttonOnClick = React.useCallback((event) => {
|
||||
if (typeof onChange === 'function') {
|
||||
onChange({
|
||||
type: 'change',
|
||||
value: event.currentTarget.dataset.type,
|
||||
dataset: dataset,
|
||||
reactEvent: event,
|
||||
nativeEvent: event.nativeEvent
|
||||
});
|
||||
}
|
||||
}, [dataset, onChange]);
|
||||
return (
|
||||
<div className={classnames(className, styles['discrete-input-container'], { 'disabled': disabled })}>
|
||||
<div className={styles['header']}>{label}</div>
|
||||
<div className={styles['input-container']} title={disabled ? t('DISABLED_LABEL', { label }) : null}>
|
||||
<Button className={classnames(styles['button-container'], { 'disabled': disabled })} data-type={'decrement'} onClick={buttonOnClick}>
|
||||
<Icon className={styles['icon']} name={'remove'} />
|
||||
</Button>
|
||||
<div className={styles['option-label']} title={value}>{value}</div>
|
||||
<Button className={classnames(styles['button-container'], { 'disabled': disabled })} data-type={'increment'} onClick={buttonOnClick}>
|
||||
<Icon className={styles['icon']} name={'add'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DiscreteSelectInput.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
dataset: PropTypes.object,
|
||||
onChange: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = DiscreteSelectInput;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const DiscreteSelectInput = require('./DiscreteSelectInput');
|
||||
|
||||
module.exports = DiscreteSelectInput;
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
|
||||
.discrete-input-container {
|
||||
.stepper {
|
||||
&:global(.disabled) {
|
||||
.header {
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.input-container {
|
||||
.content {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,14 +15,14 @@
|
|||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-radius: 3.5rem;
|
||||
background: var(--overlay-color);
|
||||
|
||||
.button-container {
|
||||
.button {
|
||||
flex: none;
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
|
|
@ -42,7 +38,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.option-label {
|
||||
.value {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
98
src/routes/Player/SubtitlesMenu/Stepper/Stepper.tsx
Normal file
98
src/routes/Player/SubtitlesMenu/Stepper/Stepper.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '@stremio/stremio-icons/react';
|
||||
import { Button } from 'stremio/components';
|
||||
import { useInterval, useTimeout } from 'stremio/common';
|
||||
import styles from './Stepper.less';
|
||||
|
||||
const clamp = (value: number, min?: number, max?: number) => {
|
||||
const minClamped = typeof min === 'number' ? Math.max(value, min) : value;
|
||||
const maxClamped = typeof max === 'number' ? Math.min(minClamped, max) : minClamped;
|
||||
return maxClamped;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
className: string,
|
||||
label: string,
|
||||
value: number,
|
||||
unit?: string,
|
||||
step: number,
|
||||
min?: number,
|
||||
max?: number,
|
||||
disabled?: boolean,
|
||||
onChange: (value: number) => void,
|
||||
};
|
||||
|
||||
const Stepper = ({ className, label, value, unit, step, min, max, disabled, onChange }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const localValue = useRef(value);
|
||||
|
||||
const interval = useInterval(100);
|
||||
const timeout = useTimeout(250);
|
||||
|
||||
const cancel = () => {
|
||||
interval.cancel();
|
||||
timeout.cancel();
|
||||
};
|
||||
|
||||
const updateValue = useCallback((delta: number) => {
|
||||
onChange(clamp(localValue.current + delta, min, max));
|
||||
}, [onChange]);
|
||||
|
||||
const onDecrementMouseDown = useCallback(() => {
|
||||
cancel();
|
||||
timeout.start(() => interval.start(() => updateValue(-step)));
|
||||
}, [updateValue]);
|
||||
|
||||
const onDecrementMouseUp = useCallback(() => {
|
||||
cancel();
|
||||
updateValue(-step);
|
||||
}, [updateValue]);
|
||||
|
||||
const onIncrementMouseDown = useCallback(() => {
|
||||
cancel();
|
||||
timeout.start(() => interval.start(() => updateValue(step)));
|
||||
}, [updateValue]);
|
||||
|
||||
const onIncrementMouseUp = useCallback(() => {
|
||||
cancel();
|
||||
updateValue(step);
|
||||
}, [updateValue]);
|
||||
|
||||
useEffect(() => {
|
||||
localValue.current = value;
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles['stepper'], className)}>
|
||||
<div className={styles['header']}>
|
||||
{ t(label) }
|
||||
</div>
|
||||
<div className={styles['content']}>
|
||||
<Button
|
||||
className={classNames(styles['button'], { 'disabled': disabled })}
|
||||
onMouseDown={onDecrementMouseDown}
|
||||
onMouseUp={onDecrementMouseUp}
|
||||
onMouseLeave={cancel}
|
||||
>
|
||||
<Icon className={styles['icon']} name={'remove'} />
|
||||
</Button>
|
||||
<div className={styles['value']}>
|
||||
{ disabled ? '--' : `${value}${unit}` }
|
||||
</div>
|
||||
<Button
|
||||
className={classNames(styles['button'], { 'disabled': disabled })}
|
||||
onMouseDown={onIncrementMouseDown}
|
||||
onMouseUp={onIncrementMouseUp}
|
||||
onMouseLeave={cancel}
|
||||
>
|
||||
<Icon className={styles['icon']} name={'add'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Stepper;
|
||||
2
src/routes/Player/SubtitlesMenu/Stepper/index.ts
Normal file
2
src/routes/Player/SubtitlesMenu/Stepper/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import Stepper from './Stepper';
|
||||
export default Stepper;
|
||||
|
|
@ -3,11 +3,12 @@
|
|||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const { CONSTANTS, comparatorWithPriorities, languages } = require('stremio/common');
|
||||
const { comparatorWithPriorities, languages } = require('stremio/common');
|
||||
const { SUBTITLES_SIZES } = require('stremio/common/CONSTANTS');
|
||||
const { Button } = require('stremio/components');
|
||||
const DiscreteSelectInput = require('./DiscreteSelectInput');
|
||||
const styles = require('./styles');
|
||||
const { t } = require('i18next');
|
||||
const { default: Stepper } = require('./Stepper');
|
||||
|
||||
const ORIGIN_PRIORITIES = {
|
||||
'LOCAL': 3,
|
||||
|
|
@ -98,51 +99,41 @@ const SubtitlesMenu = React.memo((props) => {
|
|||
}
|
||||
}
|
||||
}, [props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]);
|
||||
const onSubtitlesDelayChanged = React.useCallback((event) => {
|
||||
const delta = event.value === 'increment' ? 250 : -250;
|
||||
const onSubtitlesDelayChanged = React.useCallback((value) => {
|
||||
if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
|
||||
if (props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay)) {
|
||||
const extraDelay = props.extraSubtitlesDelay + delta;
|
||||
if (typeof props.onExtraSubtitlesDelayChanged === 'function') {
|
||||
props.onExtraSubtitlesDelayChanged(extraDelay);
|
||||
props.onExtraSubtitlesDelayChanged(value * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [props.selectedExtraSubtitlesTrackId, props.extraSubtitlesDelay, props.onExtraSubtitlesDelayChanged]);
|
||||
const onSubtitlesSizeChanged = React.useCallback((event) => {
|
||||
const delta = event.value === 'increment' ? 1 : -1;
|
||||
const onSubtitlesSizeChanged = React.useCallback((value) => {
|
||||
if (typeof props.selectedSubtitlesTrackId === 'string') {
|
||||
if (props.subtitlesSize !== null && !isNaN(props.subtitlesSize)) {
|
||||
const sizeIndex = CONSTANTS.SUBTITLES_SIZES.indexOf(props.subtitlesSize);
|
||||
const size = CONSTANTS.SUBTITLES_SIZES[Math.max(0, Math.min(CONSTANTS.SUBTITLES_SIZES.length - 1, sizeIndex + delta))];
|
||||
if (typeof props.onSubtitlesSizeChanged === 'function') {
|
||||
props.onSubtitlesSizeChanged(size);
|
||||
props.onSubtitlesSizeChanged(value);
|
||||
}
|
||||
}
|
||||
} else if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
|
||||
if (props.extraSubtitlesSize !== null && !isNaN(props.extraSubtitlesSize)) {
|
||||
const extraSizeIndex = CONSTANTS.SUBTITLES_SIZES.indexOf(props.extraSubtitlesSize);
|
||||
const extraSize = CONSTANTS.SUBTITLES_SIZES[Math.max(0, Math.min(CONSTANTS.SUBTITLES_SIZES.length - 1, extraSizeIndex + delta))];
|
||||
if (typeof props.onExtraSubtitlesSizeChanged === 'function') {
|
||||
props.onExtraSubtitlesSizeChanged(extraSize);
|
||||
props.onExtraSubtitlesSizeChanged(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [props.selectedSubtitlesTrackId, props.selectedExtraSubtitlesTrackId, props.subtitlesSize, props.extraSubtitlesSize, props.onSubtitlesSizeChanged, props.onExtraSubtitlesSizeChanged]);
|
||||
const onSubtitlesOffsetChanged = React.useCallback((event) => {
|
||||
const delta = event.value === 'increment' ? 1 : -1;
|
||||
const onSubtitlesOffsetChanged = React.useCallback((value) => {
|
||||
if (typeof props.selectedSubtitlesTrackId === 'string') {
|
||||
if (props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset)) {
|
||||
const offset = Math.max(0, Math.min(100, Math.floor(props.subtitlesOffset + delta)));
|
||||
if (typeof props.onSubtitlesOffsetChanged === 'function') {
|
||||
props.onSubtitlesOffsetChanged(offset);
|
||||
props.onSubtitlesOffsetChanged(value);
|
||||
}
|
||||
}
|
||||
} else if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
|
||||
if (props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset)) {
|
||||
const offset = Math.max(0, Math.min(100, Math.floor(props.extraSubtitlesOffset + delta)));
|
||||
if (typeof props.onExtraSubtitlesOffsetChanged === 'function') {
|
||||
props.onExtraSubtitlesOffsetChanged(offset);
|
||||
props.onExtraSubtitlesOffsetChanged(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -215,57 +206,35 @@ const SubtitlesMenu = React.memo((props) => {
|
|||
<div className={styles['subtitles-settings-container']}>
|
||||
<div className={styles['settings-header']}>{t('PLAYER_SUBTITLES_SETTINGS')}</div>
|
||||
<div className={styles['settings-list']}>
|
||||
<DiscreteSelectInput
|
||||
className={styles['discrete-input']}
|
||||
label={t('DELAY')}
|
||||
value={typeof props.selectedExtraSubtitlesTrackId === 'string' && props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay) ? `${(props.extraSubtitlesDelay / 1000).toFixed(2)}s` : '--'}
|
||||
disabled={typeof props.selectedExtraSubtitlesTrackId !== 'string' || props.extraSubtitlesDelay === null || isNaN(props.extraSubtitlesDelay)}
|
||||
<Stepper
|
||||
className={styles['stepper']}
|
||||
label={'DELAY'}
|
||||
value={props.extraSubtitlesDelay / 1000}
|
||||
unit={'s'}
|
||||
step={0.25}
|
||||
disabled={props.extraSubtitlesDelay === null}
|
||||
onChange={onSubtitlesDelayChanged}
|
||||
/>
|
||||
<DiscreteSelectInput
|
||||
className={styles['discrete-input']}
|
||||
label={t('SIZE')}
|
||||
value={
|
||||
typeof props.selectedSubtitlesTrackId === 'string' ?
|
||||
props.subtitlesSize !== null && !isNaN(props.subtitlesSize) ? `${props.subtitlesSize}%` : '--'
|
||||
:
|
||||
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
||||
props.extraSubtitlesSize !== null && !isNaN(props.extraSubtitlesSize) ? `${props.extraSubtitlesSize}%` : '--'
|
||||
:
|
||||
'--'
|
||||
}
|
||||
disabled={
|
||||
typeof props.selectedSubtitlesTrackId === 'string' ?
|
||||
props.subtitlesSize === null || isNaN(props.subtitlesSize)
|
||||
:
|
||||
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
||||
props.extraSubtitlesSize === null || isNaN(props.extraSubtitlesSize)
|
||||
:
|
||||
true
|
||||
}
|
||||
<Stepper
|
||||
className={styles['stepper']}
|
||||
label={'SIZE'}
|
||||
value={props.selectedSubtitlesTrackId ? props.subtitlesSize : props.selectedExtraSubtitlesTrackId ? props.extraSubtitlesSize : null}
|
||||
unit={'%'}
|
||||
step={25}
|
||||
min={SUBTITLES_SIZES[0]}
|
||||
max={SUBTITLES_SIZES[SUBTITLES_SIZES.length - 1]}
|
||||
disabled={(props.selectedSubtitlesTrackId && props.subtitlesSize === null) || (props.selectedExtraSubtitlesTrackId && props.extraSubtitlesSize === null)}
|
||||
onChange={onSubtitlesSizeChanged}
|
||||
/>
|
||||
<DiscreteSelectInput
|
||||
className={styles['discrete-input']}
|
||||
label={t('PLAYER_SUBTITLES_VERTICAL_POSIITON')}
|
||||
value={
|
||||
typeof props.selectedSubtitlesTrackId === 'string' ?
|
||||
props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset) ? `${props.subtitlesOffset}%` : '--'
|
||||
:
|
||||
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
||||
props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset) ? `${props.extraSubtitlesOffset}%` : '--'
|
||||
:
|
||||
'--'
|
||||
}
|
||||
disabled={
|
||||
typeof props.selectedSubtitlesTrackId === 'string' ?
|
||||
props.subtitlesOffset === null || isNaN(props.subtitlesOffset)
|
||||
:
|
||||
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
||||
props.extraSubtitlesOffset === null || isNaN(props.extraSubtitlesOffset)
|
||||
:
|
||||
true
|
||||
}
|
||||
<Stepper
|
||||
className={styles['stepper']}
|
||||
label={'PLAYER_SUBTITLES_VERTICAL_POSIITON'}
|
||||
value={props.selectedSubtitlesTrackId ? props.subtitlesOffset : props.selectedExtraSubtitlesTrackId ? props.extraSubtitlesOffset : null}
|
||||
unit={'%'}
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
disabled={(props.selectedSubtitlesTrackId && props.subtitlesOffset === null) || (props.selectedExtraSubtitlesTrackId && props.extraSubtitlesOffset === null)}
|
||||
onChange={onSubtitlesOffsetChanged}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.discrete-input {
|
||||
.stepper {
|
||||
padding: 0 1.5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue