mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
feat(Player): implement next video popup
This commit is contained in:
parent
9b2f23cac6
commit
3bd2738001
4 changed files with 283 additions and 8 deletions
114
src/routes/Player/NextVideoPopup/NextVideoPopup.js
Normal file
114
src/routes/Player/NextVideoPopup/NextVideoPopup.js
Normal file
|
|
@ -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' ?
|
||||
<Icon
|
||||
className={styles['placeholder-icon']}
|
||||
icon={ICON_FOR_TYPE.has(metaItem.type) ? ICON_FOR_TYPE.get(metaItem.type) : ICON_FOR_TYPE.get('other')}
|
||||
/>
|
||||
:
|
||||
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 (
|
||||
<div className={classnames(className, styles['next-video-popup-container'])} onAnimationEnd={onAnimationEnd}>
|
||||
<div className={styles['poster-container']}>
|
||||
<Image
|
||||
className={styles['poster-image']}
|
||||
src={nextVideo?.thumbnail}
|
||||
alt={' '}
|
||||
fallbackSrc={metaItem?.poster}
|
||||
renderFallback={renderPosterFallback}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['info-container']}>
|
||||
<div className={styles['details-container']}>
|
||||
{
|
||||
typeof videoName === 'string' ?
|
||||
<div className={styles['name']}>
|
||||
{ videoName }
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
nextVideo !== null && typeof nextVideo.overview === 'string' ?
|
||||
<div className={styles['description']}>
|
||||
{ nextVideo.overview }
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
<div className={styles['buttons-container']}>
|
||||
<Button className={styles['button-container']} onClick={onDismissButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_x'} />
|
||||
<div className={styles['label']}>Dismiss</div>
|
||||
</Button>
|
||||
<Button ref={watchNowButtonRef} className={classnames(styles['button-container'], styles['play-button'])} onClick={onWatchNowButtonClick}>
|
||||
<Icon className={styles['icon']} icon={'ic_play'} />
|
||||
<div className={styles['label']}>Watch Now</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NextVideoPopup.propTypes = {
|
||||
className: PropTypes.string,
|
||||
metaItem: PropTypes.object,
|
||||
nextVideo: PropTypes.object,
|
||||
onDismiss: PropTypes.func,
|
||||
onPlayNextVideoRequested: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = NextVideoPopup;
|
||||
3
src/routes/Player/NextVideoPopup/index.js
Normal file
3
src/routes/Player/NextVideoPopup/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
const NextEpisodeModal = require('./NextVideoPopup');
|
||||
|
||||
module.exports = NextEpisodeModal;
|
||||
122
src/routes/Player/NextVideoPopup/styles.less
Normal file
122
src/routes/Player/NextVideoPopup/styles.less
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen })}
|
||||
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !nextVideoPopupOpen })}
|
||||
onMouseDown={onContainerMouseDown}
|
||||
onMouseMove={onContainerMouseMove}
|
||||
onMouseOver={onContainerMouseMove}
|
||||
|
|
@ -539,6 +563,18 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
onMouseMove={onBarMouseMove}
|
||||
onMouseOver={onBarMouseMove}
|
||||
/>
|
||||
{
|
||||
!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && nextVideoPopupOpen ?
|
||||
<NextVideoPopup
|
||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||
metaItem={player.metaItem !== null && player.metaItem.type === 'Ready' ? player.metaItem.content : null}
|
||||
nextVideo={player.nextVideo}
|
||||
onDismiss={onDismissNextVideoPopup}
|
||||
onPlayNextVideoRequested={onPlayNextVideoRequested}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
subtitlesMenuOpen ?
|
||||
<SubtitlesMenu
|
||||
|
|
|
|||
Loading…
Reference in a new issue