diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fceb851fc..b13674628 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,10 +3,12 @@ name: Build on: push: branches: + - development + tags-ignore: - '**' pull_request: branches: - - '**' + - development # Allow manual dispatch in GH workflow_dispatch: diff --git a/src/routes/MetaDetails/StreamsList/Stream/Stream.js b/src/routes/MetaDetails/StreamsList/Stream/Stream.js index 6587c72d0..23a1ce607 100644 --- a/src/routes/MetaDetails/StreamsList/Stream/Stream.js +++ b/src/routes/MetaDetails/StreamsList/Stream/Stream.js @@ -4,15 +4,57 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); -const { Button, Image, useProfile, platform, useToast } = require('stremio/common'); +const { Button, Image, useProfile, platform, useToast, Popup, useBinaryState } = require('stremio/common'); const { useServices } = require('stremio/services'); +const { useRouteFocused } = require('stremio-router'); const StreamPlaceholder = require('./StreamPlaceholder'); +const { t } = require('i18next'); const styles = require('./styles'); const Stream = ({ className, videoId, videoReleased, addonName, name, description, thumbnail, progress, deepLinks, ...props }) => { const profile = useProfile(); const toast = useToast(); const { core } = useServices(); + const routeFocused = useRouteFocused(); + + const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); + + React.useEffect(() => { + if (!routeFocused) { + closeMenu(); + } + }, [routeFocused]); + + const popupLabelOnMouseUp = React.useCallback((event) => { + if (!event.nativeEvent.togglePopupPrevented) { + if (event.nativeEvent.ctrlKey || event.nativeEvent.button === 2) { + event.preventDefault(); + toggleMenu(); + } + } + }, []); + const popupLabelOnContextMenu = React.useCallback((event) => { + if (!event.nativeEvent.togglePopupPrevented && !event.nativeEvent.ctrlKey) { + event.preventDefault(); + } + }, [toggleMenu]); + const popupLabelOnLongPress = React.useCallback((event) => { + if (event.nativeEvent.pointerType !== 'mouse' && !event.nativeEvent.togglePopupPrevented) { + toggleMenu(); + } + }, [toggleMenu]); + const popupMenuOnPointerDown = React.useCallback((event) => { + event.nativeEvent.togglePopupPrevented = true; + }, []); + const popupMenuOnContextMenu = React.useCallback((event) => { + event.nativeEvent.togglePopupPrevented = true; + }, []); + const popupMenuOnClick = React.useCallback((event) => { + event.nativeEvent.togglePopupPrevented = true; + }, []); + const popupMenuOnKeyDown = React.useCallback((event) => { + event.nativeEvent.buttonClickPrevented = true; + }, []); const href = React.useMemo(() => { return deepLinks ? @@ -74,41 +116,117 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio } }, [props.onClick, profile.settings, markVideoAsWatched]); + const streamLink = React.useMemo(() => { + return deepLinks?.externalPlayer?.download; + }, [deepLinks]); + + const copyStreamLink = React.useCallback((event) => { + event.preventDefault(); + if (streamLink && navigator?.clipboard) { + navigator.clipboard.writeText(deepLinks.externalPlayer.download) + .then(() => { + toast.show({ + type: 'success', + title: t('PLAYER_COPY_STREAM_SUCCESS'), + timeout: 4000 + }); + }) + .catch(() => { + toast.show({ + type: 'error', + title: t('PLAYER_COPY_STREAM_ERROR'), + timeout: 4000, + }); + }); + + } else { + toast.show({ + type: 'error', + title: t('PLAYER_COPY_STREAM_ERROR'), + timeout: 4000, + }); + } + closeMenu(); + }, [streamLink]); + const renderThumbnailFallback = React.useCallback(() => ( ), []); + const renderLabel = React.useMemo( + () => + function renderLabel({ className, thumbnail, progress, addonName, name, description, children, ...props }) { + return ( + + ); + }, + [onClick] + ); + + const renderMenu = React.useMemo( + () => + function renderMenu() { + return ( +
+ + {streamLink && } +
+ ); + }, [copyStreamLink, onClick] + ); + return ( - + ); }; diff --git a/src/routes/MetaDetails/StreamsList/Stream/styles.less b/src/routes/MetaDetails/StreamsList/Stream/styles.less index 22996622c..4e5151a88 100644 --- a/src/routes/MetaDetails/StreamsList/Stream/styles.less +++ b/src/routes/MetaDetails/StreamsList/Stream/styles.less @@ -3,6 +3,14 @@ @import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; @import (reference) '~stremio/common/screen-sizes.less'; +:import('~stremio/common/Popup/styles.less') { + context-menu-container: menu-container; + menu-direction-top-left: menu-direction-top-left; + menu-direction-bottom-left: menu-direction-bottom-left; + menu-direction-top-right: menu-direction-top-right; + menu-direction-bottom-right: menu-direction-bottom-right; +} + .stream-container { display: flex; flex-direction: row; @@ -103,6 +111,33 @@ color: var(--primary-foreground-color); background-color: var(--secondary-accent-color); } + + .context-menu-container { + max-width: calc(90% - 1.5rem); + z-index: 2; + + .context-menu-content { + --spatial-navigation-contain: contain; + + .context-menu-option-container { + display: flex; + flex-direction: row; + align-items: center; + padding: 1rem 1.5rem; + + &:hover, + &:focus { + background-color: var(--overlay-color); + } + + .context-menu-option-label { + font-size: 1rem; + font-weight: 500; + color:var(--primary-foreground-color); + } + } + } + } } @media only screen and (max-width: @small) { @@ -125,6 +160,27 @@ .addon-name { font-weight: 500; } + } + .context-menu-container { + &.menu-direction-top-left, + &.menu-direction-bottom-left { + right: 1.5rem; + } + + &.menu-direction-top-right, + &.menu-direction-bottom-right { + left: 1.5rem; + } + + &.menu-direction-top-left, + &.menu-direction-top-right { + bottom: 90%; + } + + &.menu-direction-bottom-left, + &.menu-direction-bottom-right { + top: 90%; + } } } }