diff --git a/src/common/MultiselectMenu/Dropdown/Dropdown.less b/src/common/MultiselectMenu/Dropdown/Dropdown.less new file mode 100644 index 000000000..1e15419ef --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Dropdown.less @@ -0,0 +1,30 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +.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; + } + + .back-button { + display: flex; + align-items: center; + gap: 0 0.5rem; + padding: 0.75rem; + color: var(--primary-foreground-color); + + .back-button-icon { + width: 1.5rem; + } + } +} \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/Dropdown.tsx b/src/common/MultiselectMenu/Dropdown/Dropdown.tsx new file mode 100644 index 000000000..1c09de925 --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Dropdown.tsx @@ -0,0 +1,53 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React from 'react'; +import Button from 'stremio/common/Button'; +import { useTranslation } from 'react-i18next'; +import classNames from 'classnames'; +import Option from './Option'; +import Icon from '@stremio/stremio-icons/react'; +import styles from './Dropdown.less'; + +type Props = { + options: MultiselectMenuOption[]; + selectedOption?: MultiselectMenuOption | null; + menuOpen: boolean | (() => void); + level: number; + setLevel: (level: number) => void; + onSelect: (option: MultiselectMenuOption) => void; +}; + +const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen }: Props) => { + const { t } = useTranslation(); + const onBackButtonClick = () => { + setLevel(level - 1); + }; + + return ( +
+ { + level > 0 ? + + : null + } + { + options + .filter((option: MultiselectMenuOption) => !option.hidden) + .map((option: MultiselectMenuOption) => ( +
+ ); +}; + +export default Dropdown; \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/Option/Option.less b/src/common/MultiselectMenu/Dropdown/Option/Option.less new file mode 100644 index 000000000..067934dfc --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Option/Option.less @@ -0,0 +1,30 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +.option { + font-size: var(--font-size-normal); + color: var(--primary-foreground-color); + align-items: center; + display: flex; + flex-direction: row; + padding: 1rem; + + .label { + flex: 1; + 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; + } + + &:hover { + background-color: rgba(255, 255, 255, 0.15); + } +} \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/Option/Option.tsx b/src/common/MultiselectMenu/Dropdown/Option/Option.tsx new file mode 100644 index 000000000..7ade6dfa5 --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Option/Option.tsx @@ -0,0 +1,42 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React, { useMemo } from 'react'; +import classNames from 'classnames'; +import Button from 'stremio/common/Button'; +import styles from './Option.less'; +import Icon from '@stremio/stremio-icons/react'; + +type Props = { + option: MultiselectMenuOption; + selectedOption?: MultiselectMenuOption | null; + onSelect: (option: MultiselectMenuOption) => void; +}; + +const Option = ({ option, selectedOption, onSelect }: Props) => { + // consider using option.id === selectedOption?.id instead + const selected = useMemo(() => option?.value === selectedOption?.value, [option, selectedOption]); + + return ( + + ); +}; + +export default Option; \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/Option/index.ts b/src/common/MultiselectMenu/Dropdown/Option/index.ts new file mode 100644 index 000000000..6004f7754 --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Option/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Option from './Option'; + +export default Option; \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/index.ts b/src/common/MultiselectMenu/Dropdown/index.ts new file mode 100644 index 000000000..ce3622a25 --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Dropdown from './Dropdown'; + +export default Dropdown; \ No newline at end of file diff --git a/src/common/MultiselectMenu/MultiselectMenu.less b/src/common/MultiselectMenu/MultiselectMenu.less new file mode 100644 index 000000000..156e2035d --- /dev/null +++ b/src/common/MultiselectMenu/MultiselectMenu.less @@ -0,0 +1,34 @@ + +@border-radius: 2.75rem; + +.multiselect-menu { + background-color: var(--overlay-color); + position: relative; + min-width: 150px; + overflow: visible; + border-radius: @border-radius; + &.disabled { + pointer-events: none; + opacity: 0.3; + } + + .multiselect-button { + color: var(--primary-foreground-color); + background-color: rgba(255, 255, 255, 0.05); + padding: 0.75rem 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 0 0.5rem; + border-radius: @border-radius; + + .icon { + width: 1.5rem; + color: var(--primary-foreground-color); + + &.open { + transform: rotate(180deg); + } + } + } +} \ No newline at end of file diff --git a/src/common/MultiselectMenu/MultiselectMenu.tsx b/src/common/MultiselectMenu/MultiselectMenu.tsx new file mode 100644 index 000000000..5eb073eca --- /dev/null +++ b/src/common/MultiselectMenu/MultiselectMenu.tsx @@ -0,0 +1,56 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React from 'react'; +import Button from 'stremio/common/Button'; +import useBinaryState from 'stremio/common/useBinaryState'; +import Dropdown from './Dropdown'; +import classNames from 'classnames'; +import Icon from '@stremio/stremio-icons/react'; +import styles from './MultiselectMenu.less'; + +type Props = { + className?: string, + title?: string; + options: MultiselectMenuOption[]; + selectedOption?: MultiselectMenuOption; + onSelect: () => void; +}; + +const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }: Props) => { + const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); + const multiselectMenuRef = React.useRef(null); + const [level, setLevel] = React.useState(0); + + const onOptionSelect = (option: MultiselectMenuOption) => { + option.level ? setLevel(level + 1) : onSelect(), closeMenu(); + }; + + return ( +
+ + { + menuOpen ? + + : null + } +
+ ); +}; + +export default MultiselectMenu; \ No newline at end of file diff --git a/src/common/MultiselectMenu/index.ts b/src/common/MultiselectMenu/index.ts new file mode 100644 index 000000000..e526218cd --- /dev/null +++ b/src/common/MultiselectMenu/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import MultiselectMenu from './MultiselectMenu'; + +export default MultiselectMenu; \ No newline at end of file diff --git a/src/common/MultiselectMenu/types.d.ts b/src/common/MultiselectMenu/types.d.ts new file mode 100644 index 000000000..a1724ab9b --- /dev/null +++ b/src/common/MultiselectMenu/types.d.ts @@ -0,0 +1,9 @@ +type MultiselectMenuOption = { + id?: number; + label: string; + value: string; + destination?: string; + default?: boolean; + hidden?: boolean; + level?: MultiselectMenuOption[]; +}; \ No newline at end of file diff --git a/src/common/index.js b/src/common/index.js index 8046dab15..c582a13ca 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -15,6 +15,7 @@ const MetaPreview = require('./MetaPreview'); const MetaRow = require('./MetaRow'); const ModalDialog = require('./ModalDialog'); const Multiselect = require('./Multiselect'); +const { default: MultiselectMenu } = require('./MultiselectMenu'); const { HorizontalNavBar, VerticalNavBar } = require('./NavBar'); const PaginationInput = require('./PaginationInput'); const PlayIconCircleCentered = require('./PlayIconCircleCentered'); @@ -63,6 +64,7 @@ module.exports = { MetaRow, ModalDialog, Multiselect, + MultiselectMenu, HorizontalNavBar, VerticalNavBar, PaginationInput, diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js index d75080d01..6ecc2c815 100644 --- a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js +++ b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js @@ -5,9 +5,10 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const { t } = require('i18next'); const { default: Icon } = require('@stremio/stremio-icons/react'); -const { Button, Multiselect } = require('stremio/common'); +const { Button } = require('stremio/common'); const SeasonsBarPlaceholder = require('./SeasonsBarPlaceholder'); const styles = require('./styles'); +const { MultiselectMenu } = require('stremio/common'); const SeasonsBar = ({ className, seasons, season, onSelect }) => { const options = React.useMemo(() => { @@ -16,8 +17,8 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { label: season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL') })); }, [seasons]); - const selected = React.useMemo(() => { - return [String(season)]; + const selectedSeason = React.useMemo(() => { + return { label: String(season), value: String(season) }; }, [season]); const prevNextButtonOnClick = React.useCallback((event) => { if (typeof onSelect === 'function') { @@ -61,12 +62,11 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
Prev
- 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')} - direction={'bottom-left'} options={options} - selected={selected} + title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')} + selectedOption={selectedSeason} onSelect={seasonOnSelect} />