refactor: use menu provider logic for player menus

This commit is contained in:
Tim 2024-02-20 02:29:55 +01:00
parent c3361481b4
commit 63da56b4a0
24 changed files with 274 additions and 114 deletions

10
package-lock.json generated
View file

@ -48,6 +48,7 @@
"@babel/preset-react": "7.16.0", "@babel/preset-react": "7.16.0",
"@types/classnames": "^2.3.1", "@types/classnames": "^2.3.1",
"@types/react": "^18.2.9", "@types/react": "^18.2.9",
"@types/react-dom": "^18.2.19",
"babel-loader": "8.2.3", "babel-loader": "8.2.3",
"clean-webpack-plugin": "4.0.0", "clean-webpack-plugin": "4.0.0",
"copy-webpack-plugin": "9.0.1", "copy-webpack-plugin": "9.0.1",
@ -3265,6 +3266,15 @@
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
"node_modules/@types/react-dom": {
"version": "18.2.19",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz",
"integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.17.1", "version": "1.17.1",
"dev": true, "dev": true,

View file

@ -51,6 +51,7 @@
"@babel/preset-react": "7.16.0", "@babel/preset-react": "7.16.0",
"@types/classnames": "^2.3.1", "@types/classnames": "^2.3.1",
"@types/react": "^18.2.9", "@types/react": "^18.2.9",
"@types/react-dom": "^18.2.19",
"babel-loader": "8.2.3", "babel-loader": "8.2.3",
"clean-webpack-plugin": "4.0.0", "clean-webpack-plugin": "4.0.0",
"copy-webpack-plugin": "9.0.1", "copy-webpack-plugin": "9.0.1",

View file

@ -14,6 +14,7 @@ const ErrorDialog = require('./ErrorDialog');
const withProtectedRoutes = require('./withProtectedRoutes'); const withProtectedRoutes = require('./withProtectedRoutes');
const routerViewsConfig = require('./routerViewsConfig'); const routerViewsConfig = require('./routerViewsConfig');
const styles = require('./styles'); const styles = require('./styles');
const { MenuProvider } = require('stremio/common');
const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router)); const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router));
@ -163,14 +164,16 @@ const App = () => {
: :
<ToastProvider className={styles['toasts-container']}> <ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}> <TooltipProvider className={styles['tooltip-container']}>
<ServicesToaster /> <MenuProvider className={styles['menu-container']}>
<DeepLinkHandler /> <ServicesToaster />
<SearchParamsHandler /> <DeepLinkHandler />
<RouterWithProtectedRoutes <SearchParamsHandler />
className={styles['router']} <RouterWithProtectedRoutes
viewsConfig={routerViewsConfig} className={styles['router']}
onPathNotMatch={onPathNotMatch} viewsConfig={routerViewsConfig}
/> onPathNotMatch={onPathNotMatch}
/>
</MenuProvider>
</TooltipProvider> </TooltipProvider>
</ToastProvider> </ToastProvider>
: :

View file

@ -103,12 +103,12 @@ html {
height: 100%; height: 100%;
.toasts-container { .toasts-container {
z-index: 2;
position: absolute; position: absolute;
top: calc(1.2 * var(--horizontal-nav-bar-size)); top: calc(1.2 * var(--horizontal-nav-bar-size));
right: 0; right: 0;
bottom: calc(1.2 * var(--horizontal-nav-bar-size)); bottom: calc(1.2 * var(--horizontal-nav-bar-size));
left: auto; left: auto;
z-index: 1;
padding: 0 calc(0.5 * var(--horizontal-nav-bar-size)); padding: 0 calc(0.5 * var(--horizontal-nav-bar-size));
overflow-y: auto; overflow-y: auto;
scrollbar-width: none; scrollbar-width: none;
@ -120,6 +120,7 @@ html {
} }
.tooltip-container { .tooltip-container {
z-index: 1;
height: 2.5rem; height: 2.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
@ -137,6 +138,15 @@ html {
} }
} }
.menu-container {
z-index: 1;
position: absolute;
border-radius: var(--border-radius);
background-color: var(--modal-background-color);
box-shadow: 0 1.35rem 2.7rem rgba(0, 0, 0, 0.4), 0 1.1rem 0.85rem rgba(0, 0, 0, 0.2);
backdrop-filter: blur(15px);
}
.router { .router {
width: 100%; width: 100%;
height: 100%; height: 100%;

52
src/common/Menu/Menu.tsx Normal file
View file

@ -0,0 +1,52 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useEffect, useRef } from 'react';
import useMenu from './useMenu';
import useKeyboardEvent from '../useKeyboardEvent';
import styles from './styles.less';
type Props = {
children: JSX.Element,
className: string,
shortcut: string,
align: 'left' | 'right',
};
const createId = () => (Math.random() + 1).toString(36).substring(7);
const Menu = ({ children, className, shortcut, align }: Props) => {
const menu = useMenu();
const id = useRef(createId());
const element = useRef<HTMLDivElement>(null);
const onToggle = () => {
menu.toggle(id.current);
};
useEffect(() => {
const parent = element.current?.parentElement;
parent.addEventListener('click', onToggle);
menu.create({
id: id.current,
className,
parent,
align,
});
return () => {
parent.removeEventListener('click', onToggle);
menu.remove(id.current);
};
}, []);
useKeyboardEvent(shortcut, onToggle, !shortcut);
return <>
<div ref={element} className={styles['menu-placeholder']} />
{menu.active?.id === id.current && menu.render(children)}
</>;
};
export default Menu;

View file

@ -0,0 +1,17 @@
// Copyright (C) 2017-2024 Smart code 203358507
import { createContext } from 'react';
import { type Menu } from './types';
type MenuContext = {
active?: Menu,
create: (menu: Omit<Menu, 'open'>) => void,
remove: (id: string) => void,
render: (content: JSX.Element) => void,
toggle: (id: string) => void,
};
const MenuContext = createContext<MenuContext>({} as MenuContext);
export default MenuContext;

View file

@ -0,0 +1,82 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import classNames from 'classnames';
import useKeyboardEvent from '../useKeyboardEvent';
import MenuContext from './MenuContext';
import { type Menu } from './types';
type Props = {
children: JSX.Element,
className: string,
};
const MenuProvider = ({ children, className }: Props) => {
const container = useRef<HTMLDivElement>(null);
const [menus, setMenus] = useState<Menu[]>([]);
const [style, setStyle] = useState<React.CSSProperties>(null);
const active = useMemo(() => menus.find(({ open }) => open), [menus]);
const create = (menu: Omit<Menu, 'open'>) => {
setMenus((menus) => ([
...menus,
{
...menu,
open: false,
},
]));
};
const remove = (id: string) => {
setMenus((menus) => menus.filter((menu) => menu.id !== id));
};
const render = (content: JSX.Element) => {
return createPortal(content, container.current);
};
const toggle = (id: string) => {
setMenus((menus) => menus.map((menu) => ({
...menu,
open: menu.id === id && !menu.open,
})));
};
const closeAll = () => {
setMenus((menus) => menus.map((menu) => ({
...menu,
open: false,
})));
};
useLayoutEffect(() => {
setStyle(null);
if (container.current && active) {
const menu = container.current.getBoundingClientRect();
const parent = active.parent.getBoundingClientRect();
const y = (parent.top + menu.height) < window.innerHeight ? 'bottom' : 'top';
const x = (parent.left + menu.width) < window.innerWidth ? 'left' : 'right';
setStyle({
top: y === 'top' ? parent.top - menu.height : parent.bottom,
left: (active.align ?? x) === 'left' ? parent.left : parent.right - menu.width,
});
}
}, [active]);
useKeyboardEvent('Escape', closeAll);
return (
<MenuContext.Provider value={{ active, create, remove, render, toggle }}>
{children}
<div ref={container} className={classNames(className, active?.className)} style={style} />
</MenuContext.Provider>
);
};
export default MenuProvider;

9
src/common/Menu/index.ts Normal file
View file

@ -0,0 +1,9 @@
// Copyright (C) 2017-2024 Smart code 203358507
import Menu from './Menu';
import MenuProvider from './MenuProvider';
export {
Menu,
MenuProvider,
};

View file

@ -0,0 +1,11 @@
// Copyright (C) 2017-2023 Smart code 203358507
.menu-placeholder {
z-index: -1;
visibility: hidden;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}

7
src/common/Menu/types.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
export type Menu = {
id: string,
className: string,
parent: HTMLElement,
align: 'left' | 'right',
open: boolean,
};

View file

@ -0,0 +1,8 @@
// Copyright (C) 2017-2024 Smart code 203358507
import { useContext } from 'react';
import MenuContext from './MenuContext';
const useMenu = () => useContext(MenuContext);
export default useMenu;

View file

@ -48,6 +48,7 @@ const EventModal = require('./EventModal');
const { default: useOnClickOutside } = require('./useOnClickOutside'); const { default: useOnClickOutside } = require('./useOnClickOutside');
const { default: useKeyboardEvent } = require('./useKeyboardEvent'); const { default: useKeyboardEvent } = require('./useKeyboardEvent');
const { default: useMouseEvent } = require('./useMouseEvent'); const { default: useMouseEvent } = require('./useMouseEvent');
const { MenuProvider, Menu } = require('./Menu');
module.exports = { module.exports = {
AddonDetailsModal, AddonDetailsModal,
@ -102,4 +103,6 @@ module.exports = {
useOnClickOutside, useOnClickOutside,
useKeyboardEvent, useKeyboardEvent,
useMouseEvent, useMouseEvent,
MenuProvider,
Menu,
}; };

View file

@ -2,17 +2,17 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
const useOnClickOutside = (ref: React.MutableRefObject<HTMLElement>, handler: () => void) => { const useOnClickOutside = (ref: React.MutableRefObject<HTMLElement>, handler: () => void, ignore?: boolean) => {
useEffect(() => { useEffect(() => {
const onClickOutside = (event: MouseEvent) => { const onClickOutside = (event: MouseEvent) => {
const element = event.target as Node; const element = event.target as Node;
if (ref.current && !ref.current.contains(element)) { if (ref.current && !ref.current.contains(element)) {
handler(); !ignore && handler();
} }
}; };
document.addEventListener('click', onClickOutside); document.addEventListener('mouseup', onClickOutside);
return () => document.removeEventListener('click', onClickOutside); return () => document.removeEventListener('mouseup', onClickOutside);
}, [handler]); }, [handler]);
}; };

2
src/modules.d.ts vendored
View file

@ -1,2 +1,2 @@
declare module '*'; declare module '*.less';
declare module 'stremio/common'; declare module 'stremio/common';

View file

@ -1,9 +1,9 @@
// Copyright (C) 2017-2024 Smart code 203358507 // Copyright (C) 2017-2024 Smart code 203358507
import React, { useCallback, useEffect, useRef } from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from '@stremio/stremio-icons/react'; import Icon from '@stremio/stremio-icons/react';
import { Button, useBinaryState, useOnClickOutside, useKeyboardEvent } from 'stremio/common'; import { Button } from 'stremio/common';
import styles from './styles.less'; import styles from './styles.less';
type Props = { type Props = {
@ -12,53 +12,19 @@ type Props = {
disabled?: boolean, disabled?: boolean,
icon: string, icon: string,
title?: string, title?: string,
shortcut?: string,
onMenuChange?: (state: boolean) => void,
onClick?: () => void, onClick?: () => void,
}; };
const Control = ({ children, className, disabled, icon, title, shortcut, onMenuChange, onClick }: Props) => { const Control = ({ children, className, disabled, icon, title, onClick }: Props) => {
const ref = useRef(null);
const [menuOpen,, closeMenu, toggleMenu] = useBinaryState();
const onButtonClick = () => {
toggleMenu();
onClick && onClick();
};
const onMenuClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
};
const onClickOutside = useCallback(() => {
menuOpen && closeMenu();
}, [menuOpen]);
useEffect(() => {
onMenuChange && onMenuChange(menuOpen);
}, [menuOpen, onMenuChange]);
useOnClickOutside(ref, onClickOutside);
useKeyboardEvent('Escape', closeMenu);
shortcut && useKeyboardEvent(shortcut, toggleMenu);
return ( return (
<Button <Button
ref={ref}
className={classNames(className, styles['control-button'], { 'disabled': disabled })} className={classNames(className, styles['control-button'], { 'disabled': disabled })}
tabIndex={-1} tabIndex={-1}
title={title} title={title}
onClick={onButtonClick} onClick={onClick}
> >
<Icon className={styles['icon']} name={icon} /> <Icon className={styles['icon']} name={icon} />
{ {children}
children && menuOpen ?
<div className={styles['menu-container']} onClick={onMenuClick}>
{children}
</div>
:
null
}
</Button> </Button>
); );
}; };

View file

@ -22,15 +22,4 @@
height: 2.5rem; height: 2.5rem;
color: var(--primary-foreground-color); color: var(--primary-foreground-color);
} }
.menu-container {
position: absolute;
right: 0;
bottom: 8rem;
border-radius: var(--border-radius);
background-color: var(--modal-background-color);
box-shadow: 0 1.35rem 2.7rem rgba(0, 0, 0, 0.4), 0 1.1rem 0.85rem rgba(0, 0, 0, 0.2);
backdrop-filter: blur(15px);
overflow: hidden;
}
} }

View file

@ -31,7 +31,6 @@ const ControlBar = ({
onSubtitlesOffsetChanged, onSubtitlesOffsetChanged,
onSubtitlesSizeChanged, onSubtitlesSizeChanged,
onExtraSubtitlesDelayChanged, onExtraSubtitlesDelayChanged,
onMenuChange,
...props ...props
}) => { }) => {
const { chromecast } = useServices(); const { chromecast } = useServices();
@ -73,14 +72,6 @@ const ControlBar = ({
return streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Ready' ? streamingServer.playbackDevices.content : []; return streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Ready' ? streamingServer.playbackDevices.content : [];
}, [streamingServer]); }, [streamingServer]);
const onMuteButtonClick = React.useCallback(() => {
muted ? onUnmuteRequested() : onMuteRequested();
}, [muted, onMuteRequested, onUnmuteRequested]);
const onChromecastButtonClick = React.useCallback(() => {
chromecast.transport.requestSession();
}, []);
const volumeIcon = React.useMemo(() => { const volumeIcon = React.useMemo(() => {
return (typeof muted === 'boolean' && muted) ? 'volume-mute' : return (typeof muted === 'boolean' && muted) ? 'volume-mute' :
(volume === null || isNaN(volume)) ? 'volume-off' : (volume === null || isNaN(volume)) ? 'volume-off' :
@ -89,6 +80,14 @@ const ControlBar = ({
'volume-high'; 'volume-high';
}, [muted, volume]); }, [muted, volume]);
const onMuteButtonClick = React.useCallback(() => {
muted ? onUnmuteRequested() : onMuteRequested();
}, [muted, onMuteRequested, onUnmuteRequested]);
const onChromecastButtonClick = React.useCallback(() => {
chromecast.transport.requestSession();
}, []);
React.useEffect(() => { React.useEffect(() => {
const onStateChanged = () => setChromecastServiceActive(chromecast.active); const onStateChanged = () => setChromecastServiceActive(chromecast.active);
chromecast.on('stateChanged', onStateChanged); chromecast.on('stateChanged', onStateChanged);
@ -140,16 +139,21 @@ const ControlBar = ({
onClick={toogleMobileMenu} onClick={toogleMobileMenu}
/> />
<div className={classnames(styles['controls-menu-container'], { 'open': mobileMenuOpen })}> <div className={classnames(styles['controls-menu-container'], { 'open': mobileMenuOpen })}>
<Control icon={'network'} disabled={!statistics || statistics.infoHash === null || !stream} shortcut={'KeyD'} onMenuChange={onMenuChange}> {
<StatisticsMenu {...statistics} /> statistics?.infoHash ?
</Control> <Control icon={'network'}>
<Control icon={'speed'} disabled={!playbackSpeed} shortcut={'KeyR'} onMenuChange={onMenuChange}> <StatisticsMenu {...statistics} />
</Control>
:
null
}
<Control icon={'speed'} disabled={!playbackSpeed}>
<SpeedMenu <SpeedMenu
playbackSpeed={playbackSpeed} playbackSpeed={playbackSpeed}
onChange={onPlaybackSpeedChangeRequested} onChange={onPlaybackSpeedChangeRequested}
/> />
</Control> </Control>
<Control icon={'about'} disabled={!metaItem || !stream} shortcut={'KeyI'} onMenuChange={onMenuChange}> <Control icon={'about'} disabled={!metaItem || !stream}>
<InfoMenu <InfoMenu
stream={stream} stream={stream}
addon={addon} addon={addon}
@ -157,7 +161,7 @@ const ControlBar = ({
/> />
</Control> </Control>
<Control icon={'cast'} disabled={!chromecastServiceActive} onClick={onChromecastButtonClick} /> <Control icon={'cast'} disabled={!chromecastServiceActive} onClick={onChromecastButtonClick} />
<Control icon={'subtitles'} disabled={tracks.length === 0} shortcut={'KeyS'} onMenuChange={onMenuChange}> <Control icon={'subtitles'} disabled={tracks.length === 0}>
<SubtitlesMenu <SubtitlesMenu
audioTracks={audioTracks} audioTracks={audioTracks}
selectedAudioTrackId={selectedAudioTrackId} selectedAudioTrackId={selectedAudioTrackId}
@ -182,7 +186,7 @@ const ControlBar = ({
</Control> </Control>
{ {
metaItem?.videos?.length > 0 ? metaItem?.videos?.length > 0 ?
<Control icon={'episodes'} shortcut={'KeyV'} onMenuChange={onMenuChange}> <Control icon={'episodes'}>
<VideosMenu <VideosMenu
metaItem={metaItem} metaItem={metaItem}
seriesInfo={seriesInfo} seriesInfo={seriesInfo}
@ -191,7 +195,7 @@ const ControlBar = ({
: :
null null
} }
<Control icon={'more-horizontal'} onMenuChange={onMenuChange}> <Control icon={'more-horizontal'}>
<OptionsMenu <OptionsMenu
stream={stream} stream={stream}
playbackDevices={playbackDevices} playbackDevices={playbackDevices}

View file

@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
// const Stream = require('stremio/routes/MetaDetails/StreamsList/Stream'); // const Stream = require('stremio/routes/MetaDetails/StreamsList/Stream');
// const AddonDetails = require('stremio/common/AddonDetailsModal/AddonDetails'); // const AddonDetails = require('stremio/common/AddonDetailsModal/AddonDetails');
const { MetaPreview, CONSTANTS } = require('stremio/common'); const { MetaPreview, Menu, CONSTANTS } = require('stremio/common');
const styles = require('./styles'); const styles = require('./styles');
const InfoMenu = ({ ...props }) => { const InfoMenu = ({ ...props }) => {
@ -18,11 +18,8 @@ const InfoMenu = ({ ...props }) => {
: :
null; null;
}, [props.metaItem]); }, [props.metaItem]);
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.infoMenuClosePrevented = true;
}, []);
return ( return (
<div className={classnames(styles['info-menu-container'])} onMouseDown={onMouseDown}> <Menu className={classnames(styles['info-menu-container'])} shortcut={'KeyI'}>
{ {
metaItem !== null ? metaItem !== null ?
<MetaPreview <MetaPreview
@ -63,7 +60,7 @@ const InfoMenu = ({ ...props }) => {
: :
null null
} */} } */}
</div> </Menu>
); );
}; };

View file

@ -4,7 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const { useTranslation } = require('react-i18next'); const { useTranslation } = require('react-i18next');
const { useToast } = require('stremio/common'); const { Menu, useToast } = require('stremio/common');
const { useServices } = require('stremio/services'); const { useServices } = require('stremio/services');
const Option = require('./Option'); const Option = require('./Option');
const styles = require('./styles'); const styles = require('./styles');
@ -65,11 +65,8 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
}); });
} }
}, [streamingUrl]); }, [streamingUrl]);
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.optionsMenuClosePrevented = true;
}, []);
return ( return (
<div className={classnames(className, styles['options-menu-container'])} onMouseDown={onMouseDown}> <Menu className={classnames(className, styles['options-menu-container'])}>
{ {
streamingUrl || downloadUrl ? streamingUrl || downloadUrl ?
<Option <Option
@ -104,7 +101,7 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
/> />
)) ))
} }
</div> </Menu>
); );
}; };

View file

@ -4,6 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const { useTranslation } = require('react-i18next'); const { useTranslation } = require('react-i18next');
const { Menu } = require('stremio/common');
const Option = require('./Option'); const Option = require('./Option');
const styles = require('./styles'); const styles = require('./styles');
@ -12,10 +13,6 @@ const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25).reverse();
const SpeedMenu = ({ className, playbackSpeed, onChange }) => { const SpeedMenu = ({ className, playbackSpeed, onChange }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.speedMenuClosePrevented = true;
}, []);
const onOptionSelect = React.useCallback((value) => { const onOptionSelect = React.useCallback((value) => {
if (typeof onChange === 'function') { if (typeof onChange === 'function') {
onChange(value); onChange(value);
@ -23,7 +20,7 @@ const SpeedMenu = ({ className, playbackSpeed, onChange }) => {
}, [onChange]); }, [onChange]);
return ( return (
<div className={classnames(className, styles['speed-menu-container'])} onMouseDown={onMouseDown}> <Menu className={classnames(className, styles['speed-menu-container'])} shortcut={'KeyR'} align={'right'}>
<div className={styles['title']}> <div className={styles['title']}>
{ t('PLAYBACK_SPEED') } { t('PLAYBACK_SPEED') }
</div> </div>
@ -40,7 +37,7 @@ const SpeedMenu = ({ className, playbackSpeed, onChange }) => {
)) ))
} }
</div> </div>
</div> </Menu>
); );
}; };

View file

@ -3,11 +3,12 @@
const React = require('react'); const React = require('react');
const classNames = require('classnames'); const classNames = require('classnames');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const { Menu } = require('stremio/common');
const styles = require('./styles.less'); const styles = require('./styles.less');
const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => { const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
return ( return (
<div className={classNames(className, styles['statistics-menu-container'])}> <Menu className={classNames(className, styles['statistics-menu-container'])} shortcut={'KeyD'}>
<div className={styles['title']}> <div className={styles['title']}>
Statistics Statistics
</div> </div>
@ -45,7 +46,7 @@ const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
{ infoHash } { infoHash }
</div> </div>
</div> </div>
</div> </Menu>
); );
}; };

View file

@ -3,7 +3,7 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const { Button, CONSTANTS, comparatorWithPriorities, languageNames } = require('stremio/common'); const { Button, Menu, CONSTANTS, comparatorWithPriorities, languageNames } = require('stremio/common');
const DiscreteSelectInput = require('./DiscreteSelectInput'); const DiscreteSelectInput = require('./DiscreteSelectInput');
const styles = require('./styles'); const styles = require('./styles');
const { t } = require('i18next'); const { t } = require('i18next');
@ -58,9 +58,6 @@ const SubtitlesMenu = React.memo((props) => {
.filter(({ lang }) => lang === selectedSubtitlesLanguage) .filter(({ lang }) => lang === selectedSubtitlesLanguage)
.sort((t1, t2) => comparatorWithPriorities(ORIGIN_PRIORITIES)(t1.origin, t2.origin)); .sort((t1, t2) => comparatorWithPriorities(ORIGIN_PRIORITIES)(t1.origin, t2.origin));
}, [props.subtitlesTracks, props.extraSubtitlesTracks, selectedSubtitlesLanguage]); }, [props.subtitlesTracks, props.extraSubtitlesTracks, selectedSubtitlesLanguage]);
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.subtitlesMenuClosePrevented = true;
}, []);
const subtitlesLanguageOnClick = React.useCallback((event) => { const subtitlesLanguageOnClick = React.useCallback((event) => {
const track = (Array.isArray(props.subtitlesTracks) ? props.subtitlesTracks : []) const track = (Array.isArray(props.subtitlesTracks) ? props.subtitlesTracks : [])
.concat(Array.isArray(props.extraSubtitlesTracks) ? props.extraSubtitlesTracks : []) .concat(Array.isArray(props.extraSubtitlesTracks) ? props.extraSubtitlesTracks : [])
@ -150,7 +147,7 @@ const SubtitlesMenu = React.memo((props) => {
} }
}, [props.onAudioTrackSelected]); }, [props.onAudioTrackSelected]);
return ( return (
<div className={classnames(props.className, styles['subtitles-menu-container'])} onMouseDown={onMouseDown}> <Menu className={classnames(props.className, styles['subtitles-menu-container'])} shortcut={'KeyS'}>
{ {
Array.isArray(props.audioTracks) && props.audioTracks.length > 1 ? Array.isArray(props.audioTracks) && props.audioTracks.length > 1 ?
<div className={styles['languages-container']}> <div className={styles['languages-container']}>
@ -278,7 +275,7 @@ const SubtitlesMenu = React.memo((props) => {
onChange={onSubtitlesOffsetChanged} onChange={onSubtitlesOffsetChanged}
/> />
</div> </div>
</div> </Menu>
); );
}); });

View file

@ -3,13 +3,11 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const { Menu } = require('stremio/common');
const Video = require('../../../../MetaDetails/VideosList/Video'); const Video = require('../../../../MetaDetails/VideosList/Video');
const styles = require('./styles'); const styles = require('./styles');
const VideosMenu = ({ className, metaItem, seriesInfo }) => { const VideosMenu = ({ className, metaItem, seriesInfo }) => {
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.videosMenuClosePrevented = true;
}, []);
const videos = React.useMemo(() => { const videos = React.useMemo(() => {
return seriesInfo && typeof seriesInfo.season === 'number' && Array.isArray(metaItem.videos) ? return seriesInfo && typeof seriesInfo.season === 'number' && Array.isArray(metaItem.videos) ?
metaItem.videos.filter(({ season }) => season === seriesInfo.season) metaItem.videos.filter(({ season }) => season === seriesInfo.season)
@ -17,7 +15,7 @@ const VideosMenu = ({ className, metaItem, seriesInfo }) => {
metaItem.videos; metaItem.videos;
}, [metaItem, seriesInfo]); }, [metaItem, seriesInfo]);
return ( return (
<div className={classnames(className, styles['videos-menu-container'])} onMouseDown={onMouseDown}> <Menu className={classnames(className, styles['videos-menu-container'])} shortcut={'KeyV'}>
{ {
videos.map((video, index) => ( videos.map((video, index) => (
<Video <Video
@ -35,7 +33,7 @@ const VideosMenu = ({ className, metaItem, seriesInfo }) => {
/> />
)) ))
} }
</div> </Menu>
); );
}; };

View file

@ -8,6 +8,7 @@ const langs = require('langs');
const { useTranslation } = require('react-i18next'); const { useTranslation } = require('react-i18next');
const { useServices } = require('stremio/services'); const { useServices } = require('stremio/services');
const { HorizontalNavBar, useFullscreen, useBinaryState, useToast, useStreamingServer, useKeyboardEvent, useMouseEvent, withCoreSuspender } = require('stremio/common'); const { HorizontalNavBar, useFullscreen, useBinaryState, useToast, useStreamingServer, useKeyboardEvent, useMouseEvent, withCoreSuspender } = require('stremio/common');
const { default: useMenu } = require('stremio/common/Menu/useMenu');
const BufferingLoader = require('./BufferingLoader'); const BufferingLoader = require('./BufferingLoader');
const VolumeChangeIndicator = require('./VolumeChangeIndicator'); const VolumeChangeIndicator = require('./VolumeChangeIndicator');
const Error = require('./Error'); const Error = require('./Error');
@ -26,6 +27,7 @@ const Player = ({ urlParams, queryParams }) => {
return queryParams.has('forceTranscoding'); return queryParams.has('forceTranscoding');
}, [queryParams]); }, [queryParams]);
const menu = useMenu();
const [player, videoParamsChanged, timeChanged, pausedChanged, ended, nextVideo] = usePlayer(urlParams); const [player, videoParamsChanged, timeChanged, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
const [settings, updateSettings] = useSettings(); const [settings, updateSettings] = useSettings();
const streamingServer = useStreamingServer(); const streamingServer = useStreamingServer();
@ -40,7 +42,7 @@ const Player = ({ urlParams, queryParams }) => {
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []); const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
const [,,, toggleFullscreen] = useFullscreen(); const [,,, toggleFullscreen] = useFullscreen();
const [areMenusOpen, setMenusState] = React.useState(false); const areMenusOpen = React.useMemo(() => menu.active, [menu.active]);
const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false); const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
const ignoreShortcuts = React.useMemo(() => { const ignoreShortcuts = React.useMemo(() => {
@ -519,7 +521,6 @@ const Player = ({ urlParams, queryParams }) => {
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged} onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onSubtitlesSizeChanged={onSubtitlesSizeChanged} onSubtitlesSizeChanged={onSubtitlesSizeChanged}
onExtraSubtitlesDelayChanged={onExtraSubtitlesDelayChanged} onExtraSubtitlesDelayChanged={onExtraSubtitlesDelayChanged}
onMenuChange={setMenusState}
onMouseMove={onBarMouseMove} onMouseMove={onBarMouseMove}
onMouseOver={onBarMouseMove} onMouseOver={onBarMouseMove}
/> />