mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
refactor(EpisodePicker): improve styles and typings
This commit is contained in:
parent
6ca94a2124
commit
3f60df9073
4 changed files with 119 additions and 104 deletions
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (C) 2017-2025 Smart code 203358507
|
||||
|
||||
import React, { ChangeEvent, forwardRef, useCallback, useState } from 'react';
|
||||
import React, { ChangeEvent, forwardRef, useCallback, useEffect, useState } from 'react';
|
||||
import { type KeyboardEvent, type InputHTMLAttributes } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import styles from './styles.less';
|
||||
|
|
@ -18,10 +18,11 @@ type Props = InputHTMLAttributes<HTMLInputElement> & {
|
|||
max?: number;
|
||||
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onSubmit?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onUpdate?: (value: number) => void;
|
||||
};
|
||||
|
||||
const NumberInput = forwardRef<HTMLInputElement, Props>(({ defaultValue, ...props }, ref) => {
|
||||
const [value, setValue] = useState(defaultValue || 0);
|
||||
const NumberInput = forwardRef<HTMLInputElement, Props>(({ defaultValue, showButtons, onUpdate, ...props }, ref) => {
|
||||
const [value, setValue] = useState<number>(defaultValue || 0);
|
||||
const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
|
||||
props.onKeyDown && props.onKeyDown(event);
|
||||
|
||||
|
|
@ -32,33 +33,63 @@ const NumberInput = forwardRef<HTMLInputElement, Props>(({ defaultValue, ...prop
|
|||
|
||||
const handleIncrease = () => {
|
||||
const { max } = props;
|
||||
if (typeof max !== 'undefined') {
|
||||
return setValue((prevVal) =>
|
||||
prevVal + 1 > max ? max : prevVal + 1
|
||||
);
|
||||
if (max !== undefined) {
|
||||
return setValue((prevVal) => {
|
||||
const value = prevVal || 0;
|
||||
return value + 1 > max ? max : value + 1;
|
||||
});
|
||||
}
|
||||
setValue((prevVal) => prevVal + 1);
|
||||
setValue((prevVal) => {
|
||||
const value = prevVal || 0;
|
||||
return value + 1;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDecrease = () => {
|
||||
const { min } = props;
|
||||
if (typeof min !== 'undefined') {
|
||||
return setValue((prevVal) =>
|
||||
prevVal - 1 < min ? min : prevVal - 1
|
||||
);
|
||||
if (min !== undefined) {
|
||||
return setValue((prevVal) => {
|
||||
const value = prevVal || 0;
|
||||
return value - 1 < min ? min : value - 1;
|
||||
});
|
||||
}
|
||||
setValue((prevVal) => prevVal - 1);
|
||||
setValue((prevVal) => {
|
||||
const value = prevVal || 0;
|
||||
return value - 1;
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const min = props.min || 0;
|
||||
let newValue = event.target.valueAsNumber;
|
||||
if (newValue && newValue < min) {
|
||||
newValue = min;
|
||||
}
|
||||
if (props.max !== undefined && newValue && newValue > props.max) {
|
||||
newValue = props.max;
|
||||
}
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof onUpdate === 'function') {
|
||||
onUpdate(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className={classnames(props.containerClassName, styles['number-input'])}>
|
||||
{props.showButtons ? <Button
|
||||
className={styles['btn']}
|
||||
onClick={handleDecrease}
|
||||
disabled={props.disabled || (props.min !== undefined ? value <= props.min : false)}>
|
||||
<Icon className={styles['icon']} name={'remove'} />
|
||||
</Button> : null}
|
||||
<div className={classnames(styles['number-display'], props.showButtons ? styles['with-btns'] : '')}>
|
||||
{
|
||||
showButtons ?
|
||||
<Button
|
||||
className={styles['button']}
|
||||
onClick={handleDecrease}
|
||||
disabled={props.disabled || (props.min !== undefined ? value <= props.min : false)}>
|
||||
<Icon className={styles['icon']} name={'remove'} />
|
||||
</Button>
|
||||
: null
|
||||
}
|
||||
<div className={classnames(styles['number-display'], showButtons ? styles['buttons-container'] : '')}>
|
||||
{props.label && <div className={styles['label']}>{props.label}</div>}
|
||||
<input
|
||||
ref={ref}
|
||||
|
|
@ -67,19 +98,18 @@ const NumberInput = forwardRef<HTMLInputElement, Props>(({ defaultValue, ...prop
|
|||
value={value}
|
||||
{...props}
|
||||
className={classnames(props.className, styles['value'], { 'disabled': props.disabled })}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = parseInt(event.target.value);
|
||||
if (props.min !== undefined && newValue < props.min) return props.min;
|
||||
if (props.max !== undefined && newValue > props.max) return props.max;
|
||||
setValue(newValue);
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
{props.showButtons ? <Button
|
||||
className={styles['btn']} onClick={handleIncrease} disabled={props.disabled || (props.max !== undefined ? value >= props.max : false)}>
|
||||
<Icon className={styles['icon']} name={'add'} />
|
||||
</Button> : null}
|
||||
{
|
||||
showButtons ?
|
||||
<Button
|
||||
className={styles['button']} onClick={handleIncrease} disabled={props.disabled || (props.max !== undefined ? value >= props.max : false)}>
|
||||
<Icon className={styles['icon']} name={'add'} />
|
||||
</Button>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,73 +3,63 @@
|
|||
.number-input {
|
||||
user-select: text;
|
||||
display: flex;
|
||||
max-width: 12rem;
|
||||
max-width: 14rem;
|
||||
height: 3.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary-foreground-color);
|
||||
background: var(--overlay-color);
|
||||
border-radius: 100rem;
|
||||
border-radius: 3.5rem;
|
||||
|
||||
.btn {
|
||||
width: 2.875rem;
|
||||
height: 2.875rem;
|
||||
background: var(--overlay-color);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: var(--primary-foreground-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
z-index: 1;
|
||||
|
||||
svg {
|
||||
width: 1.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
.number-display {
|
||||
height: 2.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 0 1rem;
|
||||
|
||||
&::-moz-focus-inner {
|
||||
.button {
|
||||
flex: none;
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
padding: 1rem;
|
||||
background: var(--overlay-color);
|
||||
border: none;
|
||||
border-radius: 100%;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.with-btns {
|
||||
padding: 0 1.9375rem;
|
||||
margin-left: -1.4375rem;
|
||||
margin-right: -1.4375rem;
|
||||
max-width: 9.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Label */
|
||||
.number-display .label {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 400;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Value */
|
||||
.number-display .value {
|
||||
font-size: 1.3125rem;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
color: var(--primary-foreground-color);
|
||||
text-align: center;
|
||||
.number-display {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 0 1rem;
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: textfield;
|
||||
margin: 0;
|
||||
&::-moz-focus-inner {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
color: var(--primary-foreground-color);
|
||||
text-align: center;
|
||||
appearance: none;
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
flex: 0 1 auto;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
max-height: 3.6em;
|
||||
max-height: 3.5rem;
|
||||
text-align: center;
|
||||
color: var(--primary-foreground-color);
|
||||
margin-bottom: 0;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (C) 2017-2025 Smart code 203358507
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, NumberInput } from 'stremio/components';
|
||||
import styles from './EpisodePicker.less';
|
||||
|
|
@ -16,33 +16,28 @@ export const EpisodePicker = ({ className, onSubmit }: Props) => {
|
|||
const videoId = decodeURIComponent(splitPath[splitPath.length - 1]);
|
||||
const [, pathSeason, pathEpisode] = videoId ? videoId.split(':') : [];
|
||||
const [season, setSeason] = React.useState(() => {
|
||||
const initialSeason = isNaN(parseInt(pathSeason)) ? 1 : parseInt(pathSeason);
|
||||
const initialSeason = isNaN(parseInt(pathSeason)) ? 0 : parseInt(pathSeason);
|
||||
return initialSeason;
|
||||
});
|
||||
const [episode, setEpisode] = React.useState(() => {
|
||||
const initialEpisode = isNaN(parseInt(pathEpisode)) ? 1 : parseInt(pathEpisode);
|
||||
return initialEpisode;
|
||||
});
|
||||
const seasonRef = useRef<HTMLInputElement>(null);
|
||||
const episodeRef = useRef<HTMLInputElement>(null);
|
||||
const handleSeasonChange = (value: number) => setSeason(!isNaN(value) ? value : 1);
|
||||
|
||||
const handleSeasonChange = (value?: number) => setSeason(value !== undefined ? value : 1);
|
||||
|
||||
const handleEpisodeChange = (value?: number) => setEpisode(value !== undefined ? value : 1);
|
||||
const handleEpisodeChange = (value: number) => setEpisode(!isNaN(value) ? value : 1);
|
||||
|
||||
const handleSubmit = React.useCallback(() => {
|
||||
const season = parseInt(seasonRef.current?.value || '1');
|
||||
const episode = parseInt(episodeRef.current?.value || '1');
|
||||
if (typeof onSubmit === 'function' && !isNaN(season) && !isNaN(episode)) {
|
||||
onSubmit(season, episode);
|
||||
}
|
||||
}, [onSubmit, seasonRef, episodeRef]);
|
||||
}, [onSubmit, season, episode]);
|
||||
|
||||
const disabled = React.useMemo(() => season === parseInt(pathSeason) && episode === parseInt(pathEpisode), [pathSeason, pathEpisode, season, episode]);
|
||||
|
||||
return <div className={className}>
|
||||
<NumberInput ref={seasonRef} min={0} label={t('SEASON')} defaultValue={season} onUpdate={handleSeasonChange} showButtons />
|
||||
<NumberInput ref={episodeRef} min={1} label={t('EPISODE')} defaultValue={episode} onUpdate={handleEpisodeChange} showButtons />
|
||||
<NumberInput min={0} label={t('SEASON')} placeholder={t('SPECIAL')} defaultValue={season} onUpdate={handleSeasonChange} showButtons />
|
||||
<NumberInput min={1} label={t('EPISODE')} defaultValue={episode} onUpdate={handleEpisodeChange} showButtons />
|
||||
<Button className={styles['button-container']} onClick={handleSubmit} disabled={disabled}>
|
||||
<div className={styles['label']}>{t('SIDEBAR_SHOW_STREAMS')}</div>
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Reference in a new issue