diff --git a/package-lock.json b/package-lock.json
index 586ff67cf..ef6320fc5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,7 @@
"@babel/runtime": "7.16.0",
"@sentry/browser": "6.13.3",
"@stremio/stremio-colors": "5.0.1",
- "@stremio/stremio-core-web": "0.44.6",
+ "@stremio/stremio-core-web": "0.44.7",
"@stremio/stremio-icons": "4.0.0",
"@stremio/stremio-video": "0.0.24",
"a-color-picker": "1.2.1",
@@ -2699,9 +2699,9 @@
"integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA=="
},
"node_modules/@stremio/stremio-core-web": {
- "version": "0.44.6",
- "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.6.tgz",
- "integrity": "sha512-Mxc6oRKgTuXU80JEacJIe4TphccZUJkyHTMUZnUx9sotVetGX+EJsyvr+HLKNMDGJHx5xcwGT/BUikdyQR/Lpw==",
+ "version": "0.44.7",
+ "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.7.tgz",
+ "integrity": "sha512-hkeYLfL1On4TMBHn87Onrp93aeRuTh4YXMKdDR1Vz5YikPOiPEq/JRoLLmmSSsFEdifs6Egu+A0qiggTttepOA==",
"dependencies": {
"@babel/runtime": "7.16.0"
}
@@ -16716,9 +16716,9 @@
"integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA=="
},
"@stremio/stremio-core-web": {
- "version": "0.44.6",
- "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.6.tgz",
- "integrity": "sha512-Mxc6oRKgTuXU80JEacJIe4TphccZUJkyHTMUZnUx9sotVetGX+EJsyvr+HLKNMDGJHx5xcwGT/BUikdyQR/Lpw==",
+ "version": "0.44.7",
+ "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.7.tgz",
+ "integrity": "sha512-hkeYLfL1On4TMBHn87Onrp93aeRuTh4YXMKdDR1Vz5YikPOiPEq/JRoLLmmSSsFEdifs6Egu+A0qiggTttepOA==",
"requires": {
"@babel/runtime": "7.16.0"
}
diff --git a/package.json b/package.json
index f93083d7b..2242f5f80 100755
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
"@babel/runtime": "7.16.0",
"@sentry/browser": "6.13.3",
"@stremio/stremio-colors": "5.0.1",
- "@stremio/stremio-core-web": "0.44.6",
+ "@stremio/stremio-core-web": "0.44.7",
"@stremio/stremio-icons": "4.0.0",
"@stremio/stremio-video": "0.0.24",
"a-color-picker": "1.2.1",
@@ -24,7 +24,6 @@
"classnames": "2.3.1",
"eventemitter3": "4.0.7",
"filter-invalid-dom-props": "2.1.0",
- "langs": "^2.0.0",
"hat": "0.0.3",
"langs": "^2.0.0",
"lodash.debounce": "4.0.8",
diff --git a/src/common/Button/styles.less b/src/common/Button/styles.less
index 734368666..c34d67ad3 100644
--- a/src/common/Button/styles.less
+++ b/src/common/Button/styles.less
@@ -14,5 +14,6 @@
&:global(.disabled) {
pointer-events: none;
+ opacity: 0.25;
}
}
\ No newline at end of file
diff --git a/src/common/CONSTANTS.js b/src/common/CONSTANTS.js
index e755790bb..af7105214 100644
--- a/src/common/CONSTANTS.js
+++ b/src/common/CONSTANTS.js
@@ -4,6 +4,7 @@ const CHROMECAST_RECEIVER_APP_ID = '1634F54B';
const SUBTITLES_SIZES = [75, 100, 125, 150, 175, 200, 250];
const SUBTITLES_FONTS = ['Roboto', 'Arial', 'Halvetica', 'Times New Roman', 'Verdana', 'Courier', 'Lucida Console', 'sans-serif', 'serif', 'monospace'];
const SEEK_TIME_DURATIONS = [5000, 10000, 15000, 20000, 25000, 30000];
+const NEXT_VIDEO_POPUP_DURATIONS = [0, 5000, 10000, 15000, 20000, 25000, 30000, 35000, 40000, 45000, 50000];
const CATALOG_PREVIEW_SIZE = 10;
const CATALOG_PAGE_SIZE = 100;
const NONE_EXTRA_VALUE = 'None';
@@ -25,12 +26,26 @@ const TYPE_PRIORITIES = {
adult: 1,
other: -Infinity
};
+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'],
+]);
module.exports = {
CHROMECAST_RECEIVER_APP_ID,
SUBTITLES_SIZES,
SUBTITLES_FONTS,
SEEK_TIME_DURATIONS,
+ NEXT_VIDEO_POPUP_DURATIONS,
CATALOG_PREVIEW_SIZE,
CATALOG_PAGE_SIZE,
NONE_EXTRA_VALUE,
@@ -39,5 +54,6 @@ module.exports = {
IMDB_LINK_CATEGORY,
SHARE_LINK_CATEGORY,
WRITERS_LINK_CATEGORY,
- TYPE_PRIORITIES
+ TYPE_PRIORITIES,
+ ICON_FOR_TYPE
};
diff --git a/src/common/MetaItem/MetaItem.js b/src/common/MetaItem/MetaItem.js
index 5175b2ed6..0196cbf65 100644
--- a/src/common/MetaItem/MetaItem.js
+++ b/src/common/MetaItem/MetaItem.js
@@ -10,22 +10,9 @@ const Image = require('stremio/common/Image');
const Multiselect = require('stremio/common/Multiselect');
const PlayIconCircleCentered = require('stremio/common/PlayIconCircleCentered');
const useBinaryState = require('stremio/common/useBinaryState');
+const { ICON_FOR_TYPE } = require('stremio/common/CONSTANTS');
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 MetaItem = React.memo(({ className, type, name, poster, posterShape, playIcon, progress, options, deepLinks, dataset, optionOnSelect, ...props }) => {
const [menuOpen, onMenuOpen, onMenuClose] = useBinaryState(false);
const href = React.useMemo(() => {
diff --git a/src/routes/Player/NextVideoPopup/NextVideoPopup.js b/src/routes/Player/NextVideoPopup/NextVideoPopup.js
new file mode 100644
index 000000000..2b5d1972d
--- /dev/null
+++ b/src/routes/Player/NextVideoPopup/NextVideoPopup.js
@@ -0,0 +1,102 @@
+// Copyright (C) 2017-2022 Smart code 203358507
+
+const React = require('react');
+const PropTypes = require('prop-types');
+const classnames = require('classnames');
+const Icon = require('@stremio/stremio-icons/dom');
+const { Image, Button, CONSTANTS } = require('stremio/common');
+const styles = require('./styles');
+
+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..b79c4bfdd
--- /dev/null
+++ b/src/routes/Player/NextVideoPopup/index.js
@@ -0,0 +1,5 @@
+// Copyright (C) 2017-2022 Smart code 203358507
+
+const NextVideoPopup = require('./NextVideoPopup');
+
+module.exports = NextVideoPopup;
diff --git a/src/routes/Player/NextVideoPopup/styles.less b/src/routes/Player/NextVideoPopup/styles.less
new file mode 100644
index 000000000..81a566a24
--- /dev/null
+++ b/src/routes/Player/NextVideoPopup/styles.less
@@ -0,0 +1,124 @@
+// Copyright (C) 2017-2022 Smart code 203358507
+
+@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 1c29fcef0..dacf654b7 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -11,6 +11,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 OptionsMenu = require('./OptionsMenu');
const VideosMenu = require('./VideosMenu');
@@ -45,6 +46,8 @@ const Player = ({ urlParams, queryParams }) => {
const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
+ const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
+ const nextVideoPopupDismissed = React.useRef(false);
const defaultSubtitlesSelected = React.useRef(false);
const defaultAudioTrackSelected = React.useRef(false);
const [error, setError] = React.useState(null);
@@ -108,16 +111,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.nextVideo, onPlayNextVideoRequested]);
const onError = React.useCallback((error) => {
console.error('Player', error);
if (error.critical) {
@@ -190,6 +188,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) {
@@ -333,6 +345,15 @@ const Player = ({ urlParams, queryParams }) => {
pausedChanged(videoState.paused);
}
}, [videoState.paused]);
+ React.useEffect(() => {
+ if (!!settings.bingeWatching && player.nextVideo !== null && !nextVideoPopupDismissed.current) {
+ if (videoState.time !== null && videoState.duration !== null && videoState.time < videoState.duration && (videoState.duration - videoState.time) <= settings.nextVideoNotificationDuration) {
+ openNextVideoPopup();
+ } else {
+ closeNextVideoPopup();
+ }
+ }
+ }, [player.nextVideo, videoState.time, videoState.duration]);
React.useEffect(() => {
if (!defaultSubtitlesSelected.current) {
const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
@@ -363,6 +384,7 @@ const Player = ({ urlParams, queryParams }) => {
React.useEffect(() => {
defaultSubtitlesSelected.current = false;
defaultAudioTrackSelected.current = false;
+ nextVideoPopupDismissed.current = false;
}, [videoState.stream]);
React.useEffect(() => {
if ((!Array.isArray(videoState.subtitlesTracks) || videoState.subtitlesTracks.length === 0) &&
@@ -514,6 +536,7 @@ const Player = ({ urlParams, queryParams }) => {
closeInfoMenu();
closeSpeedMenu();
closeVideosMenu();
+ onDismissNextVideoPopup();
break;
}
}
@@ -533,7 +556,7 @@ const Player = ({ urlParams, queryParams }) => {
};
}, []);
return (
- {
onMouseMove={onBarMouseMove}
onMouseOver={onBarMouseMove}
/>
+ {
+ nextVideoPopupOpen ?
+
+ :
+ null
+ }
{
subtitlesMenuOpen ?
{
subtitlesOutlineColorInput,
audioLanguageSelect,
seekTimeDurationSelect,
+ nextVideoPopupDurationSelect,
bingeWatchingCheckbox,
playInBackgroundCheckbox,
playInExternalPlayerCheckbox,
@@ -338,6 +339,16 @@ const Settings = () => {
{...bingeWatchingCheckbox}
/>
+
+
+
Next video popup duration
+
+
+
Play in background
diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js
index f7bcb9885..e861f15ed 100644
--- a/src/routes/Settings/useProfileSettingsInputs.js
+++ b/src/routes/Settings/useProfileSettingsInputs.js
@@ -153,6 +153,31 @@ const useProfileSettingsInputs = (profile) => {
});
}
}), [profile.settings]);
+ const nextVideoPopupDurationSelect = React.useMemo(() => ({
+ options: CONSTANTS.NEXT_VIDEO_POPUP_DURATIONS.map((duration) => ({
+ value: `${duration}`,
+ label: duration === 0 ? 'Disabled' : `${duration / 1000} seconds`
+ })),
+ selected: [`${profile.settings.nextVideoNotificationDuration}`],
+ renderLabelText: () => {
+ return profile.settings.nextVideoNotificationDuration === 0 ?
+ 'Disabled'
+ :
+ `${profile.settings.nextVideoNotificationDuration / 1000} seconds`;
+ },
+ onSelect: (event) => {
+ core.transport.dispatch({
+ action: 'Ctx',
+ args: {
+ action: 'UpdateSettings',
+ args: {
+ ...profile.settings,
+ nextVideoNotificationDuration: parseInt(event.value, 10)
+ }
+ }
+ });
+ }
+ }), [profile.settings]);
const bingeWatchingCheckbox = React.useMemo(() => ({
checked: profile.settings.bingeWatching,
onClick: () => {
@@ -237,6 +262,7 @@ const useProfileSettingsInputs = (profile) => {
subtitlesOutlineColorInput,
audioLanguageSelect,
seekTimeDurationSelect,
+ nextVideoPopupDurationSelect,
bingeWatchingCheckbox,
playInBackgroundCheckbox,
playInExternalPlayerCheckbox,