mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
refactor(MultiselectMenu): use value only
This commit is contained in:
parent
718a64877c
commit
fd4c9e73c8
13 changed files with 63 additions and 117 deletions
|
|
@ -10,23 +10,25 @@ import styles from './Dropdown.less';
|
|||
|
||||
type Props = {
|
||||
options: MultiselectMenuOption[];
|
||||
selectedOption?: MultiselectMenuOption | null;
|
||||
value?: string | number | null;
|
||||
menuOpen: boolean | (() => void);
|
||||
level: number;
|
||||
setLevel: (level: number) => void;
|
||||
onSelect: (value: number) => void;
|
||||
onSelect: (value: string | number | null) => void;
|
||||
};
|
||||
|
||||
const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen }: Props) => {
|
||||
const Dropdown = ({ level, setLevel, options, onSelect, value, menuOpen }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const optionsRef = useRef(new Map());
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const handleSetOptionRef = useCallback((value: number) => (node: HTMLButtonElement | null) => {
|
||||
const selectedOption = options.find(opt => opt.value === value) || null;
|
||||
|
||||
const handleSetOptionRef = useCallback((optionValue: string | number) => (node: HTMLButtonElement | null) => {
|
||||
if (node) {
|
||||
optionsRef.current.set(value, node);
|
||||
optionsRef.current.set(optionValue, node);
|
||||
} else {
|
||||
optionsRef.current.delete(value);
|
||||
optionsRef.current.delete(optionValue);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
@ -67,7 +69,7 @@ const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen
|
|||
ref={handleSetOptionRef(option.value)}
|
||||
option={option}
|
||||
onSelect={onSelect}
|
||||
selectedOption={selectedOption}
|
||||
selectedValue={value}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,12 @@ import Icon from '@stremio/stremio-icons/react';
|
|||
|
||||
type Props = {
|
||||
option: MultiselectMenuOption;
|
||||
selectedOption?: MultiselectMenuOption | null;
|
||||
onSelect: (value: number) => void;
|
||||
selectedValue?: string | number | null;
|
||||
onSelect: (value: string | number | null) => void;
|
||||
};
|
||||
|
||||
const Option = forwardRef<HTMLButtonElement, Props>(({ option, selectedOption, onSelect }, ref) => {
|
||||
// consider using option.id === selectedOption?.id instead
|
||||
const selected = useMemo(() => option?.value === selectedOption?.value, [option, selectedOption]);
|
||||
const Option = forwardRef<HTMLButtonElement, Props>(({ option, selectedValue, onSelect }, ref) => {
|
||||
const selected = useMemo(() => option?.value === selectedValue, [option, selectedValue]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onSelect(option.value);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
}
|
||||
|
||||
.multiselect-button {
|
||||
color: var(--primary-foreground-color);
|
||||
padding: 0.75rem 1.5rem;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
|
@ -23,6 +22,13 @@
|
|||
gap: 0 0.5rem;
|
||||
border-radius: @border-radius;
|
||||
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1rem;
|
||||
color: var(--primary-foreground-color);
|
||||
|
|
|
|||
|
|
@ -13,17 +13,19 @@ type Props = {
|
|||
className?: string,
|
||||
title?: string | (() => string);
|
||||
options: MultiselectMenuOption[];
|
||||
selectedOption?: MultiselectMenuOption;
|
||||
onSelect: (value: number) => void;
|
||||
value?: string | number | null;
|
||||
onSelect: (value: string | number | null) => void;
|
||||
};
|
||||
|
||||
const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }: Props) => {
|
||||
const MultiselectMenu = ({ className, title, options, value, onSelect }: Props) => {
|
||||
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||
const multiselectMenuRef = useOutsideClick(() => closeMenu());
|
||||
const [level, setLevel] = React.useState<number>(0);
|
||||
|
||||
const onOptionSelect = (value: number) => {
|
||||
level ? setLevel(level + 1) : onSelect(value), closeMenu();
|
||||
const selectedOption = options.find(opt => opt.value === value);
|
||||
|
||||
const onOptionSelect = (selectedValue: string | number | null) => {
|
||||
level ? setLevel(level + 1) : onSelect(selectedValue), closeMenu();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -35,11 +37,13 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }
|
|||
aria-haspopup='listbox'
|
||||
aria-expanded={menuOpen}
|
||||
>
|
||||
<div className={styles['label']}>
|
||||
{
|
||||
typeof title === 'function'
|
||||
? title()
|
||||
: title ?? selectedOption?.label
|
||||
}
|
||||
</div>
|
||||
<Icon name={'caret-down'} className={classNames(styles['icon'], { [styles['open']]: menuOpen })} />
|
||||
</Button>
|
||||
{
|
||||
|
|
@ -50,7 +54,7 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }
|
|||
options={options}
|
||||
onSelect={onOptionSelect}
|
||||
menuOpen={menuOpen}
|
||||
selectedOption={selectedOption}
|
||||
value={value}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,7 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => {
|
|||
label: t.stringWithPrefix(name, 'ADDON_'),
|
||||
title: t.stringWithPrefix(name, 'ADDON_'),
|
||||
})),
|
||||
selectedOption: selectedCatalog
|
||||
? {
|
||||
label: t.stringWithPrefix(selectedCatalog.name, 'ADDON_'),
|
||||
value: selectedCatalog.deepLinks.addons,
|
||||
}
|
||||
: undefined,
|
||||
value: selectedCatalog ? selectedCatalog.deepLinks.addons : undefined,
|
||||
title: remoteAddons.selected !== null ?
|
||||
() => {
|
||||
const selectableCatalog = remoteAddons.selectable.catalogs
|
||||
|
|
@ -44,12 +39,7 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => {
|
|||
value: deepLinks.addons,
|
||||
label: t.stringWithPrefix(type, 'TYPE_')
|
||||
})),
|
||||
selectedOption: selectedType
|
||||
? {
|
||||
label: selectedType.type !== null ? t.stringWithPrefix(selectedType.type, 'TYPE_') : t.string('TYPE_ALL'),
|
||||
value: selectedType.deepLinks.addons
|
||||
}
|
||||
: undefined,
|
||||
value: selectedType ? selectedType.deepLinks.addons : undefined,
|
||||
title: () => {
|
||||
return installedAddons.selected !== null ?
|
||||
installedAddons.selected.request.type === null ?
|
||||
|
|
|
|||
|
|
@ -100,13 +100,13 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
<div className={styles['discover-content']}>
|
||||
<div className={styles['catalog-container']}>
|
||||
<div className={styles['selectable-inputs-container']}>
|
||||
{selectInputs.map(({ title, options, selectedOption, onSelect }, index) => (
|
||||
{selectInputs.map(({ title, options, value, onSelect }, index) => (
|
||||
<MultiselectMenu
|
||||
key={index}
|
||||
className={styles['select-input']}
|
||||
title={title}
|
||||
options={options}
|
||||
selectedOption={selectedOption}
|
||||
value={value}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -202,13 +202,13 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
{
|
||||
inputsModalOpen ?
|
||||
<ModalDialog title={'Catalog filters'} className={styles['selectable-inputs-modal']} onCloseRequest={closeInputsModal}>
|
||||
{selectInputs.map(({ title, options, selectedOption, onSelect }, index) => (
|
||||
{selectInputs.map(({ title, options, value, onSelect }, index) => (
|
||||
<MultiselectMenu
|
||||
key={index}
|
||||
className={styles['select-input']}
|
||||
title={title}
|
||||
options={options}
|
||||
selectedOption={selectedOption}
|
||||
value={value}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,8 @@ const mapSelectableInputs = (discover, t) => {
|
|||
value: deepLinks.discover,
|
||||
label: t.stringWithPrefix(type, 'TYPE_')
|
||||
})),
|
||||
selectedOption: selectedType
|
||||
? {
|
||||
label: t.stringWithPrefix(selectedType.type, 'TYPE_'),
|
||||
value: selectedType.deepLinks.discover,
|
||||
}
|
||||
value: selectedType
|
||||
? selectedType.deepLinks.discover
|
||||
: undefined,
|
||||
title: discover.selected !== null
|
||||
? () => t.stringWithPrefix(discover.selected.request.path.type, 'TYPE_')
|
||||
|
|
@ -32,11 +29,8 @@ const mapSelectableInputs = (discover, t) => {
|
|||
label: t.catalogTitle({ addon, id, name }),
|
||||
title: `${name} (${addon.manifest.name})`
|
||||
})),
|
||||
selectedOption: discover.selected?.request.path.id
|
||||
? {
|
||||
label: t.catalogTitle({ addon: selectedCatalog.addon, id: selectedCatalog.id, name: selectedCatalog.name }),
|
||||
value: selectedCatalog.deepLinks.discover
|
||||
}
|
||||
value: discover.selected?.request.path.id
|
||||
? selectedCatalog.deepLinks.discover
|
||||
: undefined,
|
||||
title: discover.selected !== null
|
||||
? () => {
|
||||
|
|
@ -61,13 +55,10 @@ const mapSelectableInputs = (discover, t) => {
|
|||
value
|
||||
})
|
||||
})),
|
||||
selectedOption: {
|
||||
label: typeof selectedExtra.value === 'string' ? t.stringWithPrefix(selectedExtra.value) : t.string('NONE'),
|
||||
value: JSON.stringify({
|
||||
href: selectedExtra.deepLinks.discover,
|
||||
value: selectedExtra.value,
|
||||
})
|
||||
},
|
||||
value: JSON.stringify({
|
||||
href: selectedExtra.deepLinks.discover,
|
||||
value: selectedExtra.value,
|
||||
}),
|
||||
title: options.some(({ selected, value }) => selected && value === null) ?
|
||||
() => t.stringWithPrefix(name, 'SELECT_')
|
||||
: t.stringWithPrefix(selectedExtra.value),
|
||||
|
|
|
|||
|
|
@ -64,10 +64,10 @@ const Library = ({ model, urlParams, queryParams }) => {
|
|||
}
|
||||
}, [profile.auth, library.selected]);
|
||||
React.useEffect(() => {
|
||||
if (!library.selected?.type && typeSelect.selectedOption) {
|
||||
window.location = typeSelect.selectedOption.value;
|
||||
if (!library.selected?.type && typeSelect.value) {
|
||||
window.location = typeSelect.value;
|
||||
}
|
||||
}, [typeSelect.selectedOption, library.selected]);
|
||||
}, [typeSelect.value, library.selected]);
|
||||
return (
|
||||
<MainNavBars className={styles['library-container']} route={model}>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@ const mapSelectableInputs = (library, t) => {
|
|||
value: deepLinks.library,
|
||||
label: type === null ? t.string('TYPE_ALL') : t.stringWithPrefix(type, 'TYPE_')
|
||||
})),
|
||||
selectedOption: {
|
||||
label: selectedType?.type === null ? t.string('TYPE_ALL') : t.stringWithPrefix(selectedType?.type, 'TYPE_'),
|
||||
value: selectedType?.deepLinks.library
|
||||
},
|
||||
value: selectedType?.deepLinks.library,
|
||||
onSelect: (value) => {
|
||||
window.location = value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,10 +89,7 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
|
|||
title: streamsByAddon[transportUrl].addon.manifest.name,
|
||||
}))
|
||||
],
|
||||
selectedOption: {
|
||||
label: selectedAddon === ALL_ADDONS_KEY ? t('ALL_ADDONS') : streamsByAddon[selectedAddon]?.addon.manifest.name,
|
||||
value: selectedAddon
|
||||
},
|
||||
value: selectedAddon,
|
||||
onSelect: onAddonSelected
|
||||
};
|
||||
}, [streamsByAddon, selectedAddon]);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
|
|||
}));
|
||||
}, [seasons]);
|
||||
const selectedSeason = React.useMemo(() => {
|
||||
return { label: String(season), value: String(season) };
|
||||
return String(season);
|
||||
}, [season]);
|
||||
const prevNextButtonOnClick = React.useCallback((event) => {
|
||||
if (typeof onSelect === 'function') {
|
||||
|
|
@ -64,7 +64,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
|
|||
className={styles['seasons-popup-label-container']}
|
||||
options={options}
|
||||
title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')}
|
||||
selectedOption={selectedSeason}
|
||||
value={selectedSeason}
|
||||
onSelect={seasonOnSelect}
|
||||
/>
|
||||
<Button className={classnames(styles['next-season-button'], { 'disabled': nextDisabled })} title={'Next season'} data-action={'next'} onClick={prevNextButtonOnClick}>
|
||||
|
|
|
|||
|
|
@ -15,10 +15,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
value: codes[0],
|
||||
label: name,
|
||||
})),
|
||||
selectedOption: {
|
||||
label: interfaceLanguages.find(({ codes }) => codes[0] === profile.settings.interfaceLanguage || codes[1] === profile.settings.interfaceLanguage)?.name,
|
||||
value: interfaceLanguages.find(({ codes }) => codes[1] === profile.settings.interfaceLanguage)?.codes?.[0] || profile.settings.interfaceLanguage
|
||||
},
|
||||
value: interfaceLanguages.find(({ codes }) => codes[1] === profile.settings.interfaceLanguage)?.codes?.[0] || profile.settings.interfaceLanguage,
|
||||
onSelect: (value) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
|
|
@ -73,10 +70,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
label: languageNames[code]
|
||||
}))
|
||||
],
|
||||
selectedOption: {
|
||||
label: languageNames[profile.settings.subtitlesLanguage],
|
||||
value: profile.settings.subtitlesLanguage
|
||||
},
|
||||
value: profile.settings.subtitlesLanguage,
|
||||
onSelect: (value) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
|
|
@ -95,10 +89,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
value: `${size}`,
|
||||
label: `${size}%`
|
||||
})),
|
||||
selectedOption: {
|
||||
label: `${profile.settings.subtitlesSize}%`,
|
||||
value: `${profile.settings.subtitlesSize}`
|
||||
},
|
||||
value: `${profile.settings.subtitlesSize}`,
|
||||
title: () => {
|
||||
return `${profile.settings.subtitlesSize}%`;
|
||||
},
|
||||
|
|
@ -165,10 +156,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
value: code,
|
||||
label: languageNames[code]
|
||||
})),
|
||||
selectedOption: {
|
||||
label: languageNames[profile.settings.audioLanguage],
|
||||
value: profile.settings.audioLanguage
|
||||
},
|
||||
value: profile.settings.audioLanguage,
|
||||
onSelect: (value) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
|
|
@ -218,10 +206,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
value: `${size}`,
|
||||
label: `${size / 1000} ${t('SECONDS')}`
|
||||
})),
|
||||
selectedOption: {
|
||||
label: `${profile.settings.seekTimeDuration / 1000} ${t('SECONDS')}`,
|
||||
value: `${profile.settings.seekTimeDuration}`
|
||||
},
|
||||
value: `${profile.settings.seekTimeDuration}`,
|
||||
title: () => {
|
||||
return `${profile.settings.seekTimeDuration / 1000} ${t('SECONDS')}`;
|
||||
},
|
||||
|
|
@ -243,10 +228,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
value: `${size}`,
|
||||
label: `${size / 1000} ${t('SECONDS')}`
|
||||
})),
|
||||
selectedOption: {
|
||||
label: `${profile.settings.seekShortTimeDuration / 1000} ${t('SECONDS')}`,
|
||||
value: `${profile.settings.seekShortTimeDuration}`,
|
||||
},
|
||||
value: `${profile.settings.seekShortTimeDuration}`,
|
||||
title: () => {
|
||||
return `${profile.settings.seekShortTimeDuration / 1000} ${t('SECONDS')}`;
|
||||
},
|
||||
|
|
@ -270,10 +252,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
value,
|
||||
label: t(label),
|
||||
})),
|
||||
selectedOption: {
|
||||
label: CONSTANTS.EXTERNAL_PLAYERS.find(({ value }) => value === profile.settings.playerType)?.label,
|
||||
value: profile.settings.playerType
|
||||
},
|
||||
value: profile.settings.playerType,
|
||||
title: () => {
|
||||
const selectedOption = CONSTANTS.EXTERNAL_PLAYERS.find(({ value }) => value === profile.settings.playerType);
|
||||
return selectedOption ? t(selectedOption.label, { defaultValue: selectedOption.label }) : profile.settings.playerType;
|
||||
|
|
@ -296,12 +275,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
value: `${duration}`,
|
||||
label: duration === 0 ? 'Disabled' : `${duration / 1000} ${t('SECONDS')}`
|
||||
})),
|
||||
selectedOption: {
|
||||
label: profile.settings.nextVideoNotificationDuration === 0
|
||||
? 'Disabled'
|
||||
: `${profile.settings.nextVideoNotificationDuration / 1000} ${t('SECONDS')}`,
|
||||
value: `${profile.settings.nextVideoNotificationDuration}`
|
||||
},
|
||||
value: `${profile.settings.nextVideoNotificationDuration}`,
|
||||
title: () => {
|
||||
return profile.settings.nextVideoNotificationDuration === 0 ?
|
||||
'Disabled'
|
||||
|
|
|
|||
|
|
@ -77,10 +77,7 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
|
|||
value: address,
|
||||
}))
|
||||
],
|
||||
selectedOption: {
|
||||
label: streamingServer.settings.content.remoteHttps || t('SETTINGS_DISABLED'),
|
||||
value: streamingServer.settings.content.remoteHttps
|
||||
},
|
||||
value: streamingServer.settings.content.remoteHttps,
|
||||
onSelect: (value) => {
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
|
|
@ -106,10 +103,7 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
|
|||
label: cacheSizeToString(size),
|
||||
value: JSON.stringify(size)
|
||||
})),
|
||||
selectedOption: {
|
||||
label: cacheSizeToString(streamingServer.settings.content.cacheSize),
|
||||
value: JSON.stringify(streamingServer.settings.content.cacheSize)
|
||||
},
|
||||
value: JSON.stringify(streamingServer.settings.content.cacheSize),
|
||||
title: () => {
|
||||
return cacheSizeToString(streamingServer.settings.content.cacheSize);
|
||||
},
|
||||
|
|
@ -158,12 +152,7 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
|
|||
:
|
||||
[]
|
||||
),
|
||||
selectedOption: {
|
||||
label: isCustomTorrentProfileSelected
|
||||
? 'custom'
|
||||
: Object.keys(TORRENT_PROFILES).find((profileName) => JSON.stringify(TORRENT_PROFILES[profileName]) === JSON.stringify(selectedTorrentProfile)),
|
||||
value: JSON.stringify(selectedTorrentProfile)
|
||||
},
|
||||
value: JSON.stringify(selectedTorrentProfile),
|
||||
onSelect: (value) => {
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
|
|
@ -194,10 +183,7 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
|
|||
value: name,
|
||||
}))
|
||||
],
|
||||
selectedOption: {
|
||||
label: streamingServer.settings.content.transcodeProfile || t('SETTINGS_DISABLED'),
|
||||
value: streamingServer.settings.content.transcodeProfile
|
||||
},
|
||||
value: streamingServer.settings.content.transcodeProfile ?? null,
|
||||
onSelect: (value) => {
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
|
|
|
|||
Loading…
Reference in a new issue