diff --git a/src/routes/Player/NextVideoPopup/NextVideoPopup.js b/src/routes/Player/NextVideoPopup/NextVideoPopup.js new file mode 100644 index 000000000..6198a5b2a --- /dev/null +++ b/src/routes/Player/NextVideoPopup/NextVideoPopup.js @@ -0,0 +1,114 @@ +const React = require('react'); +const PropTypes = require('prop-types'); +const classnames = require('classnames'); +const Icon = require('@stremio/stremio-icons/dom'); +const { Image, Button } = require('stremio/common'); +const styles = require('./styles'); + +const ICON_FOR_TYPE = new Map([ + ['movie', 'ic_movies'], + ['series', 'ic_series'], + ['channel', 'ic_channels'], + ['tv', 'ic_tv'], + ['book', 'ic_book'], + ['game', 'ic_games'], + ['music', 'ic_music'], + ['adult', 'ic_adult'], + ['radio', 'ic_radio'], + ['podcast', 'ic_podcast'], + ['other', 'ic_movies'], +]); + +const NextVideoPopup = ({ className, metaItem, nextVideo, onDismiss, onPlayNextVideoRequested }) => { + const watchNowButtonRef = React.useRef(null); + const [animationEnded, setAnimationEnded] = React.useState(false); + const videoName = React.useMemo(() => { + const title = nextVideo && nextVideo.title || metaItem && metaItem.title; + return nextVideo !== null && + typeof nextVideo.season === 'number' && + typeof nextVideo.episode === 'number' ? + `${title} (S${nextVideo.season}E${nextVideo.episode})` + : + title; + }, [metaItem, nextVideo]); + const onAnimationEnd = React.useCallback(() => { + setAnimationEnded(true); + }, []); + const renderPosterFallback = React.useCallback(() => { + return metaItem !== null && typeof metaItem.type === 'string' ? + + : + null; + }, [metaItem]); + const onDismissButtonClick = React.useCallback(() => { + if (typeof onDismiss === 'function') { + onDismiss(); + } + }, [onDismiss]); + const onWatchNowButtonClick = React.useCallback(() => { + if (typeof onPlayNextVideoRequested === 'function') { + onPlayNextVideoRequested(); + } + }, [onPlayNextVideoRequested]); + React.useLayoutEffect(() => { + if (animationEnded === true && watchNowButtonRef.current !== null) { + watchNowButtonRef.current.focus(); + } + }, [animationEnded]); + return ( +
+
+ {' +
+
+
+ { + typeof videoName === 'string' ? +
+ { videoName } +
+ : + null + } + { + nextVideo !== null && typeof nextVideo.overview === 'string' ? +
+ { nextVideo.overview } +
+ : + null + } +
+
+ + +
+
+
+ ); +}; + +NextVideoPopup.propTypes = { + className: PropTypes.string, + metaItem: PropTypes.object, + nextVideo: PropTypes.object, + onDismiss: PropTypes.func, + onPlayNextVideoRequested: PropTypes.func +}; + +module.exports = NextVideoPopup; diff --git a/src/routes/Player/NextVideoPopup/index.js b/src/routes/Player/NextVideoPopup/index.js new file mode 100644 index 000000000..9c10ee432 --- /dev/null +++ b/src/routes/Player/NextVideoPopup/index.js @@ -0,0 +1,3 @@ +const NextEpisodeModal = require('./NextVideoPopup'); + +module.exports = NextEpisodeModal; diff --git a/src/routes/Player/NextVideoPopup/styles.less b/src/routes/Player/NextVideoPopup/styles.less new file mode 100644 index 000000000..20bfe08db --- /dev/null +++ b/src/routes/Player/NextVideoPopup/styles.less @@ -0,0 +1,122 @@ +@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; + +.next-video-popup-container { + display: flex; + flex-direction: row; + height: 16rem; + width: 40rem; + animation: slide-fade-in 0.5s ease-in; + + @keyframes slide-fade-in { + 0% { + opacity: 0; + transform: translateX(calc(40rem + 2rem)); + } + + 100% { + opacity: 1; + transform: translateX(0); + } + } + + .poster-container { + flex: 1 1 40%; + display: flex; + justify-content: center; + align-items: center; + background-color: @color-background; + + .poster-image { + flex: none; + width: 100%; + height: 100%; + object-position: center; + object-fit: cover; + } + + .placeholder-icon { + flex: none; + width: 80%; + height: 50%; + fill: @color-background-light3-90; + } + } + + .info-container { + flex: 1 1 70%; + display: flex; + flex-direction: column; + + .details-container { + flex: auto; + padding: 1.5rem 1.5rem; + + .name { + flex: none; + align-self: stretch; + max-height: 2.4em; + font-weight: 600; + margin-bottom: 0.5rem; + color: @color-surface-light5-90; + } + + .description { + color: @color-surface-light5-50; + } + } + + .buttons-container { + display: flex; + flex-direction: row; + + .spacing { + flex: 0 0 50%; + } + + .button-container { + flex: 0 0 50%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + height: 3.5rem; + + &.play-button { + background-color: @color-accent3; + + .icon { + fill: @color-surface-light5-90; + } + + .label { + color: @color-surface-light5-90; + } + + &:hover, &:focus { + background-color: @color-accent3-light1; + } + } + + .icon { + flex: none; + width: 1.4rem; + height: 1.4rem; + margin-right: 1rem; + fill: @color-secondaryvariant1-90; + } + + .label { + flex: none; + max-height: 2.4em; + font-size: 1.1rem; + font-weight: 500; + color: @color-secondaryvariant1-90; + } + + &:hover, &:focus { + background-color: @color-background-light2; + } + } + } + } +} \ No newline at end of file diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index a8f7c2c3f..d11c17e16 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -10,6 +10,7 @@ const { HorizontalNavBar, Button, useFullscreen, useBinaryState, useToast, useSt const Icon = require('@stremio/stremio-icons/dom'); const BufferingLoader = require('./BufferingLoader'); const ControlBar = require('./ControlBar'); +const NextVideoPopup = require('./NextVideoPopup'); const InfoMenu = require('./InfoMenu'); const VideosMenu = require('./VideosMenu'); const SubtitlesMenu = require('./SubtitlesMenu'); @@ -40,6 +41,8 @@ const Player = ({ urlParams, queryParams }) => { const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false); const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false); const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false); + const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false); + const nextVideoPopupDismissed = React.useRef(false); const [error, setError] = React.useState(null); const [videoState, setVideoState] = React.useReducer( (videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }), @@ -100,16 +103,11 @@ const Player = ({ urlParams, queryParams }) => { ended(); pushToLibrary(); if (player.nextVideo !== null) { - window.location.replace( - typeof player.nextVideo.deepLinks.player === 'string' ? - player.nextVideo.deepLinks.player - : - player.nextVideo.deepLinks.metaDetailsStreams - ); + onPlayNextVideoRequested(); } else { window.history.back(); } - }, [player.libraryItem, player.nextVideo]); + }, [player.libraryItem, player.nextVideo, onPlayNextVideoRequested]); const onError = React.useCallback((error) => { console.error('Player', error); if (error.critical) { @@ -179,6 +177,20 @@ const Player = ({ urlParams, queryParams }) => { const onSubtitlesOffsetChanged = React.useCallback((offset) => { updateSettings({ subtitlesOffset: offset }); }, [updateSettings]); + const onDismissNextVideoPopup = React.useCallback(() => { + closeNextVideoPopup(); + nextVideoPopupDismissed.current = true; + }, []); + const onPlayNextVideoRequested = React.useCallback(() => { + if (player.nextVideo !== null) { + window.location.replace( + typeof player.nextVideo.deepLinks.player === 'string' ? + player.nextVideo.deepLinks.player + : + player.nextVideo.deepLinks.metaDetailsStreams + ); + } + }, [player.nextVideo]); const onVideoClick = React.useCallback(() => { if (videoState.paused !== null) { if (videoState.paused) { @@ -313,6 +325,17 @@ const Player = ({ urlParams, queryParams }) => { pausedChanged(videoState.paused); } }, [videoState.paused]); + React.useEffect(() => { + if (nextVideoPopupDismissed.current === false) { + if (videoState.time !== null && !isNaN(videoState.time) && videoState.duration !== null && !isNaN(videoState.duration)) { + if (videoState.time < videoState.duration && (videoState.duration - videoState.time) <= (35 * 1000)) { + openNextVideoPopup(); + } else { + closeNextVideoPopup(); + } + } + } + }, [videoState.time, videoState.duration]); React.useEffect(() => { if ((!Array.isArray(videoState.subtitlesTracks) || videoState.subtitlesTracks.length === 0) && (!Array.isArray(videoState.extraSubtitlesTracks) || videoState.extraSubtitlesTracks.length === 0) && @@ -439,6 +462,7 @@ const Player = ({ urlParams, queryParams }) => { closeSubtitlesMenu(); closeInfoMenu(); closeVideosMenu(); + onDismissNextVideoPopup(); break; } } @@ -458,7 +482,7 @@ const Player = ({ urlParams, queryParams }) => { }; }, []); return ( -
{ onMouseMove={onBarMouseMove} onMouseOver={onBarMouseMove} /> + { + !subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && nextVideoPopupOpen ? + + : + null + } { subtitlesMenuOpen ?