mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-20 10:42:12 +00:00
refactor: Create dedicated LanguageMultiselect component for Player settings
- Create new LanguageMultiselect component separate from MetaItem's Multiselect - Add dynamic tooltip showing selected languages line-by-line on hover - Preserve original Multiselect component for MetaItem usage
This commit is contained in:
parent
3bf3dbc3b7
commit
f23510a475
7 changed files with 507 additions and 195 deletions
201
src/components/LanguageMultiselect/LanguageMultiselect.js
Normal file
201
src/components/LanguageMultiselect/LanguageMultiselect.js
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
// 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 ModalDialog = require('stremio/components/ModalDialog');
|
||||
const useBinaryState = require('stremio/common/useBinaryState');
|
||||
const { default: useOutsideClick } = require('stremio/common/useOutsideClick');
|
||||
const styles = require('./styles');
|
||||
const menuStyles = require('../MultiselectMenu/MultiselectMenu.less');
|
||||
|
||||
const LanguageMultiselect = ({ className, mode, direction, title, disabled = false, dataset = undefined, options, renderLabelContent = undefined, renderLabelText = undefined, onOpen = undefined, onClose = undefined, onSelect, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||
|
||||
const multiselectMenuRef = useOutsideClick(() => {
|
||||
if (menuOpen) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
const filteredOptions = React.useMemo(() => {
|
||||
return Array.isArray(options) ?
|
||||
options.filter((option) => {
|
||||
return option && (typeof option.value === 'string' || option.value === null);
|
||||
})
|
||||
:
|
||||
[];
|
||||
}, [options]);
|
||||
|
||||
const selected = React.useMemo(() => {
|
||||
return Array.isArray(props.selected) ?
|
||||
props.selected.filter((value) => {
|
||||
return typeof value === 'string' || value === null;
|
||||
})
|
||||
:
|
||||
[];
|
||||
}, [props.selected]);
|
||||
|
||||
const optionOnClick = React.useCallback((event) => {
|
||||
if (typeof onSelect === 'function') {
|
||||
onSelect({
|
||||
type: 'select',
|
||||
value: event.currentTarget.dataset.value,
|
||||
reactEvent: event,
|
||||
nativeEvent: event.nativeEvent,
|
||||
dataset: dataset
|
||||
});
|
||||
}
|
||||
}, [dataset, onSelect]);
|
||||
|
||||
const mountedRef = React.useRef(false);
|
||||
React.useLayoutEffect(() => {
|
||||
if (mountedRef.current) {
|
||||
if (menuOpen) {
|
||||
if (typeof onOpen === 'function') {
|
||||
onOpen({
|
||||
type: 'open',
|
||||
dataset: dataset
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (typeof onClose === 'function') {
|
||||
onClose({
|
||||
type: 'close',
|
||||
dataset: dataset
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mountedRef.current = true;
|
||||
}, [menuOpen]);
|
||||
|
||||
const renderLabelContentNode = () => {
|
||||
if (typeof renderLabelContent === 'function') {
|
||||
return renderLabelContent();
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={classnames(menuStyles['label'], styles['multiselect-label-fix'])}>
|
||||
{
|
||||
typeof renderLabelText === 'function' ?
|
||||
renderLabelText()
|
||||
:
|
||||
selected.length > 0 ?
|
||||
(() => {
|
||||
const MAX_ITEMS = 2;
|
||||
const items = selected.slice(0, MAX_ITEMS).map((value) => {
|
||||
const option = filteredOptions.find((option) => option.value === value);
|
||||
return option && typeof option.label === 'string' ?
|
||||
option.label
|
||||
:
|
||||
value;
|
||||
});
|
||||
return selected.length > MAX_ITEMS ? items.join(', ') + ', ...' : items.join(', ');
|
||||
})()
|
||||
:
|
||||
title
|
||||
}
|
||||
</div>
|
||||
<Icon className={classnames(menuStyles['icon'], { [menuStyles['open']]: menuOpen })} name={'caret-down'} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMenu = () => (
|
||||
<div className={classnames(styles['dropdown'], { [styles['open']]: menuOpen })}>
|
||||
{
|
||||
filteredOptions.length > 0 ?
|
||||
filteredOptions.map(({ label, title, value }) => {
|
||||
const isSelected = selected.includes(value);
|
||||
return (
|
||||
<Button
|
||||
key={value}
|
||||
className={classnames(styles['option'], { [styles['selected']]: isSelected })}
|
||||
title={typeof title === 'string' ? title : typeof label === 'string' ? label : value}
|
||||
data-value={value}
|
||||
onClick={optionOnClick}
|
||||
>
|
||||
<div className={styles['label']}>{typeof label === 'string' ? label : value}</div>
|
||||
{isSelected && <div className={styles['icon']} />}
|
||||
</Button>
|
||||
);
|
||||
})
|
||||
:
|
||||
<div className={styles['no-options-container']}>
|
||||
<div className={styles['label']}>{t('NO_OPTIONS')}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (mode === 'modal') {
|
||||
return (
|
||||
<div className={classnames(menuStyles['multiselect-menu'], className)}>
|
||||
<Button
|
||||
className={classnames(menuStyles['multiselect-button'], styles['multiselect-button-fix'], { [menuStyles['open']]: menuOpen })}
|
||||
disabled={disabled}
|
||||
onClick={toggleMenu}
|
||||
title={title}
|
||||
>
|
||||
{renderLabelContentNode()}
|
||||
</Button>
|
||||
{menuOpen ?
|
||||
<ModalDialog className={styles['modal-container']} title={title} onCloseRequest={closeMenu}>
|
||||
{renderMenu()}
|
||||
</ModalDialog>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(menuStyles['multiselect-menu'], { [menuStyles['active']]: menuOpen, [menuStyles['disabled']]: disabled }, className)}
|
||||
ref={multiselectMenuRef}
|
||||
>
|
||||
<Button
|
||||
className={classnames(menuStyles['multiselect-button'], styles['multiselect-button-fix'], { [menuStyles['open']]: menuOpen })}
|
||||
disabled={disabled}
|
||||
onClick={toggleMenu}
|
||||
tabIndex={0}
|
||||
aria-haspopup='listbox'
|
||||
aria-expanded={menuOpen}
|
||||
title={title}
|
||||
>
|
||||
{renderLabelContentNode()}
|
||||
</Button>
|
||||
{renderMenu()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LanguageMultiselect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
mode: PropTypes.oneOf(['popup', 'modal']),
|
||||
direction: PropTypes.any,
|
||||
title: PropTypes.string,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
value: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
label: PropTypes.string
|
||||
})),
|
||||
selected: PropTypes.arrayOf(PropTypes.string),
|
||||
disabled: PropTypes.bool,
|
||||
dataset: PropTypes.object,
|
||||
renderLabelContent: PropTypes.func,
|
||||
renderLabelText: PropTypes.func,
|
||||
onOpen: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
onSelect: PropTypes.func,
|
||||
onClick: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = LanguageMultiselect;
|
||||
5
src/components/LanguageMultiselect/index.js
Normal file
5
src/components/LanguageMultiselect/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const LanguageMultiselect = require('./LanguageMultiselect');
|
||||
|
||||
module.exports = LanguageMultiselect;
|
||||
98
src/components/LanguageMultiselect/styles.less
Normal file
98
src/components/LanguageMultiselect/styles.less
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
@item-height: 3rem;
|
||||
@parent-height: 12rem;
|
||||
|
||||
.dropdown {
|
||||
background: var(--modal-background-color);
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
box-shadow: var(--outer-glow);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
|
||||
&.open {
|
||||
display: block;
|
||||
max-height: calc(@item-height * 7);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-label-fix {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.multiselect-button-fix {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.option {
|
||||
height: @item-height;
|
||||
font-size: var(--font-size-normal);
|
||||
color: var(--primary-foreground-color);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 100%;
|
||||
margin-left: 1rem;
|
||||
background-color: var(--secondary-accent-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.no-options-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
background-color: @color-background;
|
||||
|
||||
.label {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
flex-basis: auto;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: @color-surface-light5-90;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (max-width: @xsmall) {
|
||||
.dropdown {
|
||||
&.open {
|
||||
max-height: calc(100dvh - var(--horizontal-nav-bar-size) - @parent-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,22 +6,14 @@ const PropTypes = require('prop-types');
|
|||
const classnames = require('classnames');
|
||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||
const { Button } = require('stremio/components');
|
||||
const Popup = require('stremio/components/Popup');
|
||||
const ModalDialog = require('stremio/components/ModalDialog');
|
||||
const useBinaryState = require('stremio/common/useBinaryState');
|
||||
const { default: useOutsideClick } = require('stremio/common/useOutsideClick');
|
||||
const styles = require('./styles');
|
||||
const menuStyles = require('../MultiselectMenu/MultiselectMenu.less');
|
||||
|
||||
const Multiselect = ({ className, mode, direction, title, disabled = false, dataset = undefined, options, renderLabelContent = undefined, renderLabelText = undefined, onOpen = undefined, onClose = undefined, onSelect, ...props }) => {
|
||||
const Multiselect = ({ className, mode, direction, title, disabled, dataset, options, renderLabelContent, renderLabelText, onOpen, onClose, onSelect, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||
|
||||
const multiselectMenuRef = useOutsideClick(() => {
|
||||
if (menuOpen) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
const filteredOptions = React.useMemo(() => {
|
||||
return Array.isArray(options) ?
|
||||
options.filter((option) => {
|
||||
|
|
@ -30,7 +22,6 @@ const Multiselect = ({ className, mode, direction, title, disabled = false, data
|
|||
:
|
||||
[];
|
||||
}, [options]);
|
||||
|
||||
const selected = React.useMemo(() => {
|
||||
return Array.isArray(props.selected) ?
|
||||
props.selected.filter((value) => {
|
||||
|
|
@ -39,7 +30,21 @@ const Multiselect = ({ className, mode, direction, title, disabled = false, data
|
|||
:
|
||||
[];
|
||||
}, [props.selected]);
|
||||
const labelOnClick = React.useCallback((event) => {
|
||||
if (typeof props.onClick === 'function') {
|
||||
props.onClick(event);
|
||||
}
|
||||
|
||||
if (!event.nativeEvent.toggleMenuPrevented) {
|
||||
toggleMenu();
|
||||
}
|
||||
}, [props.onClick, toggleMenu]);
|
||||
const menuOnClick = React.useCallback((event) => {
|
||||
event.nativeEvent.toggleMenuPrevented = true;
|
||||
}, []);
|
||||
const menuOnKeyDown = React.useCallback((event) => {
|
||||
event.nativeEvent.buttonClickPrevented = true;
|
||||
}, []);
|
||||
const optionOnClick = React.useCallback((event) => {
|
||||
if (typeof onSelect === 'function') {
|
||||
onSelect({
|
||||
|
|
@ -50,8 +55,11 @@ const Multiselect = ({ className, mode, direction, title, disabled = false, data
|
|||
dataset: dataset
|
||||
});
|
||||
}
|
||||
}, [dataset, onSelect]);
|
||||
|
||||
if (!event.nativeEvent.closeMenuPrevented) {
|
||||
closeMenu();
|
||||
}
|
||||
}, [dataset, onSelect]);
|
||||
const mountedRef = React.useRef(false);
|
||||
React.useLayoutEffect(() => {
|
||||
if (mountedRef.current) {
|
||||
|
|
@ -74,107 +82,79 @@ const Multiselect = ({ className, mode, direction, title, disabled = false, data
|
|||
|
||||
mountedRef.current = true;
|
||||
}, [menuOpen]);
|
||||
|
||||
const renderLabelContentNode = () => {
|
||||
if (typeof renderLabelContent === 'function') {
|
||||
return renderLabelContent();
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={classnames(menuStyles['label'], styles['multiselect-label-fix'])}>
|
||||
{
|
||||
typeof renderLabelText === 'function' ?
|
||||
renderLabelText()
|
||||
:
|
||||
selected.length > 0 ?
|
||||
(() => {
|
||||
const MAX_ITEMS = 2;
|
||||
const items = selected.slice(0, MAX_ITEMS).map((value) => {
|
||||
const option = filteredOptions.find((option) => option.value === value);
|
||||
return option && typeof option.label === 'string' ?
|
||||
option.label
|
||||
:
|
||||
value;
|
||||
});
|
||||
return selected.length > MAX_ITEMS ? items.join(', ') + ', ...' : items.join(', ');
|
||||
})()
|
||||
:
|
||||
title
|
||||
}
|
||||
</div>
|
||||
<Icon className={classnames(menuStyles['icon'], { [menuStyles['open']]: menuOpen })} name={'caret-down'} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMenu = () => (
|
||||
<div className={classnames(styles['dropdown'], { [styles['open']]: menuOpen })}>
|
||||
const renderLabel = React.useCallback(({ children, className, ...props }) => (
|
||||
<Button {...props} className={classnames(className, styles['label-container'], { 'active': menuOpen })} title={title} disabled={disabled} onClick={labelOnClick}>
|
||||
{
|
||||
typeof renderLabelContent === 'function' ?
|
||||
renderLabelContent()
|
||||
:
|
||||
<React.Fragment>
|
||||
<div className={styles['label']}>
|
||||
{
|
||||
typeof renderLabelText === 'function' ?
|
||||
renderLabelText()
|
||||
:
|
||||
selected.length > 0 ?
|
||||
selected.map((value) => {
|
||||
const option = filteredOptions.find((option) => option.value === value);
|
||||
return option && typeof option.label === 'string' ?
|
||||
option.label
|
||||
:
|
||||
value;
|
||||
}).join(', ')
|
||||
:
|
||||
title
|
||||
}
|
||||
</div>
|
||||
<Icon className={styles['icon']} name={'caret-down'} />
|
||||
</React.Fragment>
|
||||
}
|
||||
{children}
|
||||
</Button>
|
||||
), [menuOpen, title, disabled, filteredOptions, selected, labelOnClick, renderLabelContent, renderLabelText]);
|
||||
const renderMenu = React.useCallback(() => (
|
||||
<div className={styles['menu-container']} onKeyDown={menuOnKeyDown} onClick={menuOnClick}>
|
||||
{
|
||||
filteredOptions.length > 0 ?
|
||||
filteredOptions.map(({ label, title, value }) => {
|
||||
const isSelected = selected.includes(value);
|
||||
return (
|
||||
<Button
|
||||
key={value}
|
||||
className={classnames(styles['option'], { [styles['selected']]: isSelected })}
|
||||
title={typeof title === 'string' ? title : typeof label === 'string' ? label : value}
|
||||
data-value={value}
|
||||
onClick={optionOnClick}
|
||||
>
|
||||
<div className={styles['label']}>{typeof label === 'string' ? label : value}</div>
|
||||
{isSelected && <div className={styles['icon']} />}
|
||||
</Button>
|
||||
);
|
||||
})
|
||||
filteredOptions.map(({ label, title, value }) => (
|
||||
<Button key={value} className={classnames(styles['option-container'], { 'selected': selected.includes(value) })} title={typeof title === 'string' ? title : typeof label === 'string' ? label : value} data-value={value} onClick={optionOnClick}>
|
||||
<div className={styles['label']}>{typeof label === 'string' ? label : value}</div>
|
||||
<div className={styles['icon']} />
|
||||
</Button>
|
||||
))
|
||||
:
|
||||
<div className={styles['no-options-container']}>
|
||||
<div className={styles['label']}>{t('NO_OPTIONS')}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (mode === 'modal') {
|
||||
return (
|
||||
<div className={classnames(menuStyles['multiselect-menu'], className)}>
|
||||
<Button
|
||||
className={classnames(menuStyles['multiselect-button'], styles['multiselect-button-fix'], { [menuStyles['open']]: menuOpen })}
|
||||
disabled={disabled}
|
||||
onClick={toggleMenu}
|
||||
title={title}
|
||||
>
|
||||
{renderLabelContentNode()}
|
||||
</Button>
|
||||
{menuOpen ?
|
||||
<ModalDialog className={styles['modal-container']} title={title} onCloseRequest={closeMenu}>
|
||||
{renderMenu()}
|
||||
</ModalDialog>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(menuStyles['multiselect-menu'], { [menuStyles['active']]: menuOpen, [menuStyles['disabled']]: disabled }, className)}
|
||||
ref={multiselectMenuRef}
|
||||
>
|
||||
<Button
|
||||
className={classnames(menuStyles['multiselect-button'], styles['multiselect-button-fix'], { [menuStyles['open']]: menuOpen })}
|
||||
disabled={disabled}
|
||||
onClick={toggleMenu}
|
||||
tabIndex={0}
|
||||
aria-haspopup='listbox'
|
||||
aria-expanded={menuOpen}
|
||||
title={title}
|
||||
>
|
||||
{renderLabelContentNode()}
|
||||
</Button>
|
||||
{renderMenu()}
|
||||
</div>
|
||||
);
|
||||
), [filteredOptions, selected, menuOnKeyDown, menuOnClick, optionOnClick]);
|
||||
const renderPopupLabel = React.useMemo(() => (labelProps) => {
|
||||
return renderLabel({
|
||||
...labelProps,
|
||||
...props,
|
||||
className: classnames(className, labelProps.className)
|
||||
});
|
||||
}, [props, className, renderLabel]);
|
||||
return mode === 'modal' ?
|
||||
renderLabel({
|
||||
...props,
|
||||
className,
|
||||
children: menuOpen ?
|
||||
<ModalDialog className={styles['modal-container']} title={title} onCloseRequest={closeMenu} onKeyDown={menuOnKeyDown} onClick={menuOnClick}>
|
||||
{renderMenu()}
|
||||
</ModalDialog>
|
||||
:
|
||||
null
|
||||
})
|
||||
:
|
||||
<Popup
|
||||
open={menuOpen}
|
||||
direction={direction}
|
||||
onCloseRequest={closeMenu}
|
||||
renderLabel={renderPopupLabel}
|
||||
renderMenu={renderMenu}
|
||||
/>;
|
||||
};
|
||||
|
||||
Multiselect.propTypes = {
|
||||
|
|
|
|||
|
|
@ -3,95 +3,113 @@
|
|||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
@item-height: 3rem;
|
||||
@parent-height: 12rem;
|
||||
|
||||
.dropdown {
|
||||
background: var(--modal-background-color);
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
box-shadow: var(--outer-glow);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
|
||||
&.open {
|
||||
display: block;
|
||||
max-height: calc(@item-height * 7);
|
||||
overflow: auto;
|
||||
}
|
||||
:import('~stremio/components/Popup/styles.less') {
|
||||
popup-menu-container: menu-container;
|
||||
}
|
||||
|
||||
.multiselect-label-fix {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@parent-height: 10rem;
|
||||
|
||||
.multiselect-button-fix {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.option {
|
||||
height: @item-height;
|
||||
font-size: var(--font-size-normal);
|
||||
color: var(--primary-foreground-color);
|
||||
align-items: center;
|
||||
.label-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
height: 2.75rem;
|
||||
padding: 0 1.5rem;
|
||||
border-radius: 2.75rem;
|
||||
background-color: var(--overlay-color);
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
color: var(--primary-foreground-color);
|
||||
&:global(.active) {
|
||||
.icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
>.label {
|
||||
flex: 1;
|
||||
max-height: 2.4em;
|
||||
font-weight: 500;
|
||||
color: var(--primary-foreground-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 100%;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-left: 1rem;
|
||||
background-color: var(--secondary-accent-color);
|
||||
color: var(--primary-foreground-color);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
.popup-menu-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.no-options-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
background-color: @color-background;
|
||||
.modal-container, .popup-menu-container {
|
||||
.menu-container {
|
||||
max-height: calc(3rem * 7);
|
||||
|
||||
.label {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
flex-basis: auto;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: @color-surface-light5-90;
|
||||
.option-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
|
||||
&:global(.selected) {
|
||||
.icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
max-height: 4.8em;
|
||||
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);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.no-options-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
background-color: @color-background;
|
||||
|
||||
.label {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
flex-basis: auto;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: @color-surface-light5-90;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (max-width: @xsmall) {
|
||||
.dropdown {
|
||||
&.open {
|
||||
.modal-container, .popup-menu-container {
|
||||
.menu-container {
|
||||
max-height: calc(100dvh - var(--horizontal-nav-bar-size) - @parent-height);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
src/modules.d.ts
vendored
1
src/modules.d.ts
vendored
|
|
@ -6,3 +6,4 @@ declare module '*.less' {
|
|||
declare module 'stremio-router';
|
||||
declare module 'stremio/components/NavBar';
|
||||
declare module 'stremio/components/ModalDialog';
|
||||
declare module 'stremio/components/LanguageMultiselect';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { forwardRef, useMemo } from 'react';
|
||||
import { ColorInput, MultiselectMenu, Toggle, Multiselect, Input } from 'stremio/components';
|
||||
import { ColorInput, MultiselectMenu, Toggle, Input } from 'stremio/components';
|
||||
import LanguageMultiselect from 'stremio/components/LanguageMultiselect';
|
||||
import { useServices } from 'stremio/services';
|
||||
import { Category, Option, Section } from '../components';
|
||||
import usePlayerOptions from './usePlayerOptions';
|
||||
|
|
@ -228,25 +229,33 @@ const Player = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
|
|||
}), [interactiveSettings.targetLang, sortedTargetLanguages, setInteractiveSettings]);
|
||||
|
||||
// Multi-select for LLM target languages - matches Multiselect component API
|
||||
const multiTargetLangsSelect = useMemo(() => ({
|
||||
mode: 'popup' as const,
|
||||
direction: 'bottom-left' as const,
|
||||
title: 'Target Languages',
|
||||
options: sortedTargetLanguages,
|
||||
selected: interactiveSettings.targetLangs,
|
||||
onSelect: (event: { type: string; value: string; nativeEvent?: { closeMenuPrevented?: boolean } }) => {
|
||||
// Prevent menu from closing on selection
|
||||
if (event.nativeEvent) {
|
||||
event.nativeEvent.closeMenuPrevented = true;
|
||||
}
|
||||
const value = event.value;
|
||||
const current = interactiveSettings.targetLangs;
|
||||
const updated = current.includes(value)
|
||||
? current.filter((l) => l !== value)
|
||||
: [...current, value];
|
||||
setInteractiveSettings({ targetLangs: updated.length > 0 ? updated : [] });
|
||||
},
|
||||
}), [interactiveSettings.targetLangs, sortedTargetLanguages, setInteractiveSettings]);
|
||||
const multiTargetLangsSelect = useMemo(() => {
|
||||
// Generate tooltip with selected languages line by line
|
||||
const selectedLanguageNames = interactiveSettings.targetLangs
|
||||
.map((code) => languageNames[code as keyof typeof languageNames] || code)
|
||||
.join('\n');
|
||||
const tooltipTitle = selectedLanguageNames || 'Target Languages';
|
||||
|
||||
return {
|
||||
mode: 'popup' as const,
|
||||
direction: 'bottom-left' as const,
|
||||
title: tooltipTitle,
|
||||
options: sortedTargetLanguages,
|
||||
selected: interactiveSettings.targetLangs,
|
||||
onSelect: (event: { type: string; value: string; nativeEvent?: { closeMenuPrevented?: boolean } }) => {
|
||||
// Prevent menu from closing on selection
|
||||
if (event.nativeEvent) {
|
||||
event.nativeEvent.closeMenuPrevented = true;
|
||||
}
|
||||
const value = event.value;
|
||||
const current = interactiveSettings.targetLangs;
|
||||
const updated = current.includes(value)
|
||||
? current.filter((l) => l !== value)
|
||||
: [...current, value];
|
||||
setInteractiveSettings({ targetLangs: updated.length > 0 ? updated : [] });
|
||||
},
|
||||
};
|
||||
}, [interactiveSettings.targetLangs, sortedTargetLanguages, setInteractiveSettings]);
|
||||
|
||||
const isLLMProvider = LLM_PROVIDERS.includes(interactiveSettings.provider);
|
||||
const showApiKeyInput = ['GEMINI', 'OPENAI', 'CLAUDE', 'OPENROUTER', 'CUSTOM'].includes(interactiveSettings.provider);
|
||||
|
|
@ -322,7 +331,7 @@ const Player = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
|
|||
)}
|
||||
{isLLMProvider && (
|
||||
<Option label={'Target Languages (Multi)'}>
|
||||
<Multiselect
|
||||
<LanguageMultiselect
|
||||
className={'multiselect'}
|
||||
{...multiTargetLangsSelect}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in a new issue