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,