diff --git a/src/components/ContextMenu/ContextMenu.less b/src/components/ContextMenu/ContextMenu.less new file mode 100644 index 000000000..e38b7bad2 --- /dev/null +++ b/src/components/ContextMenu/ContextMenu.less @@ -0,0 +1,17 @@ +@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; + +.context-menu-container { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + + .context-menu { + position: fixed; + border-radius: var(--border-radius); + background-color: var(--modal-background-color); + box-shadow: 0 1.35rem 2.7rem @color-background-dark5-40, + 0 1.1rem 0.85rem @color-background-dark5-20; + } +} \ No newline at end of file diff --git a/src/components/ContextMenu/ContextMenu.tsx b/src/components/ContextMenu/ContextMenu.tsx new file mode 100644 index 000000000..28b1a94fc --- /dev/null +++ b/src/components/ContextMenu/ContextMenu.tsx @@ -0,0 +1,89 @@ +import React, { RefObject, useCallback, useEffect, useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; +import styles from './ContextMenu.less'; + +type Props = { + children: React.ReactNode, + on: RefObject[], + autoClose: boolean, +}; + +const ContextMenu = ({ children, on, autoClose }: Props) => { + const [active, setActive] = useState(false); + const [position, setPosition] = useState([0, 0]); + const [containerSize, setContainerSize] = useState([0, 0]); + + const ref = useCallback((element: HTMLDivElement) => { + element && setContainerSize([element.offsetWidth, element.offsetHeight]); + }, []); + + const style = useMemo(() => { + const [viewportWidth, viewportHeight] = [window.innerWidth, window.innerHeight]; + const [containerWidth, containerHeight] = containerSize; + const [x, y] = position; + + const left = (x + containerWidth) > viewportWidth ? x - containerWidth : x; + const top = (y + containerHeight) > viewportHeight ? y - containerHeight : y; + + return { top, left }; + }, [position, containerSize]); + + const close = () => { + setPosition([0, 0]); + setActive(false); + }; + + const onContextMenu = (event: MouseEvent) => { + event.preventDefault(); + const { clientX, clientY } = event; + + setPosition([clientX, clientY]); + setActive(true); + }; + + const onClickOutside = () => { + close(); + }; + + const onClick = useCallback(() => { + autoClose && close(); + }, [autoClose]); + + const onMouseDown = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + + const onTouchStart = (event: React.TouchEvent) => { + event.stopPropagation(); + }; + + useEffect(() => { + const containers = on.map((ref) => ref.current).filter((element) => !!element); + containers.forEach((container) => container.addEventListener('contextmenu', onContextMenu)); + + return () => { + containers.forEach((container) => container.removeEventListener('contextmenu', onContextMenu)); + }; + }, [on]); + + return active && createPortal(( +
+
+ {children} +
+
+ ), document.body); +}; + +export default ContextMenu; diff --git a/src/components/ContextMenu/index.ts b/src/components/ContextMenu/index.ts new file mode 100644 index 000000000..10ae5e5d7 --- /dev/null +++ b/src/components/ContextMenu/index.ts @@ -0,0 +1,2 @@ +import ContextMenu from './ContextMenu'; +export default ContextMenu; diff --git a/src/components/index.ts b/src/components/index.ts index f65d66f81..db5210685 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,6 +3,7 @@ import BottomSheet from './BottomSheet'; import Button from './Button'; import Chips from './Chips'; import ColorInput from './ColorInput'; +import ContextMenu from './ContextMenu'; import ContinueWatchingItem from './ContinueWatchingItem'; import DelayedRenderer from './DelayedRenderer'; import EventModal from './EventModal'; @@ -33,6 +34,7 @@ export { Button, Chips, ColorInput, + ContextMenu, ContinueWatchingItem, DelayedRenderer, EventModal, diff --git a/src/routes/Player/BufferingLoader/BufferingLoader.js b/src/routes/Player/BufferingLoader/BufferingLoader.js index 042aaea38..3d5664ae4 100644 --- a/src/routes/Player/BufferingLoader/BufferingLoader.js +++ b/src/routes/Player/BufferingLoader/BufferingLoader.js @@ -6,9 +6,9 @@ const classnames = require('classnames'); const { Image } = require('stremio/components'); const styles = require('./styles'); -const BufferingLoader = ({ className, logo, onContextMenu }) => { +const BufferingLoader = React.forwardRef(({ className, logo }, ref) => { return ( -
+
{ />
); -}; +}); BufferingLoader.propTypes = { className: PropTypes.string, logo: PropTypes.string, - onContextMenu: PropTypes.func }; module.exports = BufferingLoader; diff --git a/src/routes/Player/Error/Error.js b/src/routes/Player/Error/Error.js index c9a7c3bef..61db0e45f 100644 --- a/src/routes/Player/Error/Error.js +++ b/src/routes/Player/Error/Error.js @@ -8,7 +8,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react'); const { Button } = require('stremio/components'); const styles = require('./styles'); -const Error = ({ className, code, message, stream }) => { +const Error = React.forwardRef(({ className, code, message, stream }, ref) => { const { t } = useTranslation(); const [playlist, fileName] = React.useMemo(() => { @@ -19,7 +19,7 @@ const Error = ({ className, code, message, stream }) => { }, [stream]); return ( -
+
{message}
{ code === 2 ? @@ -44,7 +44,7 @@ const Error = ({ className, code, message, stream }) => { }
); -}; +}); Error.propTypes = { className: PropTypes.string, diff --git a/src/routes/Player/OptionsMenu/OptionsMenu.js b/src/routes/Player/OptionsMenu/OptionsMenu.js index a279a711a..0065d9b55 100644 --- a/src/routes/Player/OptionsMenu/OptionsMenu.js +++ b/src/routes/Player/OptionsMenu/OptionsMenu.js @@ -5,12 +5,11 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const { useTranslation } = require('react-i18next'); const { usePlatform, useToast } = require('stremio/common'); -const { default: useOutsideClick } = require('stremio/common/useOutsideClick'); const { useServices } = require('stremio/services'); const Option = require('./Option'); const styles = require('./styles'); -const OptionsMenu = ({ menuRef, className, stream, playbackDevices, style, onOutsideClick }) => { +const OptionsMenu = ({ className, stream, playbackDevices, style }) => { const { t } = useTranslation(); const { core } = useServices(); const platform = usePlatform(); @@ -71,12 +70,8 @@ const OptionsMenu = ({ menuRef, className, stream, playbackDevices, style, onOut event.nativeEvent.optionsMenuClosePrevented = true; }, []); - useOutsideClick(menuRef, () => { - if (typeof onOutsideClick === 'function') onOutsideClick(); - }); - return ( -
+
{ streamingUrl || downloadUrl ?