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..9f82106d1
--- /dev/null
+++ b/src/common/MultiselectMenu/Dropdown/Dropdown.tsx
@@ -0,0 +1,55 @@
+// 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: (value: number) => 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, index) => (
+
+ ))
+
+
+ }
+
+ );
+};
+
+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..a0ee1743f
--- /dev/null
+++ b/src/common/MultiselectMenu/Dropdown/Option/Option.less
@@ -0,0 +1,29 @@
+// 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;
+ 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..c7dbb4ebb
--- /dev/null
+++ b/src/common/MultiselectMenu/Dropdown/Option/Option.tsx
@@ -0,0 +1,46 @@
+// Copyright (C) 2017-2024 Smart code 203358507
+
+import React, { useCallback, 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: (value: number) => void;
+};
+
+const Option = ({ option, selectedOption, onSelect }: Props) => {
+ // consider using option.id === selectedOption?.id instead
+ const selected = useMemo(() => option?.value === selectedOption?.value, [option, selectedOption]);
+
+ const handleClick = useCallback(() => {
+ onSelect(option.value);
+ }, [onSelect, option.value]);
+
+ 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..3c7b81b59
--- /dev/null
+++ b/src/common/MultiselectMenu/MultiselectMenu.less
@@ -0,0 +1,39 @@
+// Copyright (C) 2017-2024 Smart code 203358507
+
+@border-radius: 2.75rem;
+
+.multiselect-menu {
+ position: relative;
+ min-width: 8.5rem;
+ overflow: visible;
+ border-radius: @border-radius;
+
+ &.disabled {
+ pointer-events: none;
+ opacity: 0.3;
+ }
+
+ .multiselect-button {
+ color: var(--primary-foreground-color);
+ padding: 0.75rem 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 0 0.5rem;
+ border-radius: @border-radius;
+
+ .icon {
+ width: 1rem;
+ color: var(--primary-foreground-color);
+ opacity: 0.6;
+
+ &.open {
+ transform: rotate(180deg);
+ }
+ }
+ }
+
+ &:hover {
+ background-color: var(--overlay-color);
+ }
+}
\ 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..2bd298752
--- /dev/null
+++ b/src/common/MultiselectMenu/MultiselectMenu.tsx
@@ -0,0 +1,57 @@
+// 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';
+import useOutsideClick from 'stremio/common/useOutsideClick';
+
+type Props = {
+ className?: string,
+ title?: string;
+ options: MultiselectMenuOption[];
+ selectedOption?: MultiselectMenuOption;
+ onSelect: (value: number) => void;
+};
+
+const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }: Props) => {
+ const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
+ const multiselectMenuRef = useOutsideClick(() => closeMenu());
+ const [level, setLevel] = React.useState(0);
+
+ const onOptionSelect = (value: number) => {
+ level ? setLevel(level + 1) : onSelect(value), 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..7ed039ddd
--- /dev/null
+++ b/src/common/MultiselectMenu/types.d.ts
@@ -0,0 +1,9 @@
+type MultiselectMenuOption = {
+ id?: number;
+ label: string;
+ value: number;
+ 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/common/useOutsideClick.ts b/src/common/useOutsideClick.ts
new file mode 100644
index 000000000..815132924
--- /dev/null
+++ b/src/common/useOutsideClick.ts
@@ -0,0 +1,27 @@
+// Copyright (C) 2017-2024 Smart code 203358507
+
+import { useEffect, useRef } from 'react';
+
+const useOutsideClick = (callback: () => void) => {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent | TouchEvent) => {
+ if (ref.current && !ref.current.contains(event.target as Node)) {
+ callback();
+ }
+ };
+
+ document.addEventListener('mouseup', handleClickOutside);
+ document.addEventListener('touchend', handleClickOutside);
+
+ return () => {
+ document.removeEventListener('mouseup', handleClickOutside);
+ document.removeEventListener('touchend', handleClickOutside);
+ };
+ }, [callback]);
+
+ return ref;
+};
+
+export default useOutsideClick;
\ No newline at end of file
diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js
index d75080d01..71c10ce92 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') {
@@ -35,8 +36,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
});
}
}, [season, seasons, onSelect]);
- const seasonOnSelect = React.useCallback((event) => {
- const value = parseFloat(event.value);
+ const seasonOnSelect = React.useCallback((value) => {
if (typeof onSelect === 'function') {
onSelect({
type: 'select',
@@ -61,12 +61,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}
/>