diff --git a/src/common/CONSTANTS.js b/src/common/CONSTANTS.js index 359c5a3fb..ad5454bcf 100644 --- a/src/common/CONSTANTS.js +++ b/src/common/CONSTANTS.js @@ -4,6 +4,8 @@ const CATALOG_PREVIEW_SIZE = 10; const CATALOG_PAGE_SIZE = 100; const NONE_EXTRA_VALUE = 'None'; const SKIP_EXTRA_NAME = 'skip'; +const IMDB_LINK_CATEGORY = 'imdb'; +const SHARE_LINK_CATEGORY = 'share'; const TYPE_PRIORITIES = { movie: 10, series: 9, @@ -25,5 +27,7 @@ module.exports = { CATALOG_PAGE_SIZE, NONE_EXTRA_VALUE, SKIP_EXTRA_NAME, + IMDB_LINK_CATEGORY, + SHARE_LINK_CATEGORY, TYPE_PRIORITIES }; diff --git a/src/common/MetaPreview/MetaPreview.js b/src/common/MetaPreview/MetaPreview.js index 557f76331..a93cc168d 100644 --- a/src/common/MetaPreview/MetaPreview.js +++ b/src/common/MetaPreview/MetaPreview.js @@ -8,6 +8,7 @@ const Button = require('stremio/common/Button'); const Image = require('stremio/common/Image'); const ModalDialog = require('stremio/common/ModalDialog'); const SharePrompt = require('stremio/common/SharePrompt'); +const CONSTANTS = require('stremio/common/CONSTANTS'); const routesRegexp = require('stremio/common/routesRegexp'); const useBinaryState = require('stremio/common/useBinaryState'); const ActionButton = require('./ActionButton'); @@ -15,8 +16,6 @@ const MetaLinks = require('./MetaLinks'); const MetaPreviewPlaceholder = require('./MetaPreviewPlaceholder'); const styles = require('./styles'); -const IMDB_LINK_CATEGORY = 'imdb'; -const SHARE_LINK_CATEGORY = 'share'; const ALLOWED_LINK_REDIRECTS = [ routesRegexp.search.regexp, routesRegexp.discover.regexp, @@ -30,12 +29,12 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele links .filter((link) => link && typeof link.category === 'string' && typeof link.url === 'string') .reduce((linksGroups, { category, name, url }) => { - if (category === IMDB_LINK_CATEGORY) { + if (category === CONSTANTS.IMDB_LINK_CATEGORY) { linksGroups[category] = { label: name, href: `https://www.stremio.com/warning#${encodeURIComponent(`https://www.imdb.com/title/${encodeURIComponent(url)}`)}` }; - } else if (category === SHARE_LINK_CATEGORY) { + } else if (category === CONSTANTS.SHARE_LINK_CATEGORY) { linksGroups[category] = { label: name, href: url @@ -96,7 +95,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele null } { - (typeof releaseInfo === 'string' && releaseInfo.length > 0) || (released instanceof Date && !isNaN(released.getTime())) || (typeof runtime === 'string' && runtime.length > 0) || typeof linksGroups[IMDB_LINK_CATEGORY] === 'object' ? + (typeof releaseInfo === 'string' && releaseInfo.length > 0) || (released instanceof Date && !isNaN(released.getTime())) || (typeof runtime === 'string' && runtime.length > 0) || typeof linksGroups[CONSTANTS.IMDB_LINK_CATEGORY] === 'object' ?
{ typeof runtime === 'string' && runtime.length > 0 ? @@ -114,16 +113,16 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele null } { - typeof linksGroups[IMDB_LINK_CATEGORY] === 'object' ? + typeof linksGroups[CONSTANTS.IMDB_LINK_CATEGORY] === 'object' ? : null @@ -149,8 +148,8 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele { Object.keys(linksGroups) .filter((category) => { - return category !== IMDB_LINK_CATEGORY && - category !== SHARE_LINK_CATEGORY; + return category !== CONSTANTS.IMDB_LINK_CATEGORY && + category !== CONSTANTS.SHARE_LINK_CATEGORY; }) .map((category, index) => ( : diff --git a/src/common/Slider/styles.less b/src/common/Slider/styles.less index 3f46cba42..30d796423 100644 --- a/src/common/Slider/styles.less +++ b/src/common/Slider/styles.less @@ -9,9 +9,6 @@ html.active-slider-within { } .slider-container { - --track-size: 0.5rem; - --thumb-size: 1.5rem; - position: relative; z-index: 0; overflow: visible; diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js index cbc37920f..7033efa89 100644 --- a/src/routes/Player/ControlBar/ControlBar.js +++ b/src/routes/Player/ControlBar/ControlBar.js @@ -3,11 +3,7 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const Icon = require('stremio-icons/dom'); const { Button } = require('stremio/common'); -const MetaPreviewButton = require('./MetaPreviewButton'); -const MuteButton = require('./MuteButton'); -const PlayPauseButton = require('./PlayPauseButton'); const SeekBar = require('./SeekBar'); -const SubtitlesButton = require('./SubtitlesButton'); const VolumeSlider = require('./VolumeSlider'); const styles = require('./styles'); @@ -19,17 +15,55 @@ const ControlBar = ({ volume, muted, subtitlesTracks, - metaResource, + info, onPlayRequested, onPauseRequested, onMuteRequested, onUnmuteRequested, onVolumeChangeRequested, onSeekRequested, - onToggleSubtitlesPicker, - onToggleMetaPreview, + onToggleSubtitlesMenu, + onToggleInfoMenu, ...props }) => { + const onSubtitlesButtonMouseDown = React.useCallback((event) => { + event.nativeEvent.subtitlesMenuClosePrevented = true; + }, []); + const onInfoButtonMouseDown = React.useCallback((event) => { + event.nativeEvent.infoMenuClosePrevented = true; + }, []); + const onPlayPauseButtonClick = React.useCallback(() => { + if (paused) { + if (typeof onPlayRequested === 'function') { + onPlayRequested(); + } + } else { + if (typeof onPauseRequested === 'function') { + onPauseRequested(); + } + } + }, [paused, onPlayRequested, onPauseRequested]); + const onMuteButtonClick = React.useCallback(() => { + if (muted) { + if (typeof onUnmuteRequested === 'function') { + onUnmuteRequested(); + } + } else { + if (typeof onMuteRequested === 'function') { + onMuteRequested(); + } + } + }, [muted, onMuteRequested, onUnmuteRequested]); + const onSubtitlesButtonClick = React.useCallback(() => { + if (typeof onToggleSubtitlesMenu === 'function') { + onToggleSubtitlesMenu(); + } + }, [onToggleSubtitlesMenu]); + const onInfoButtonClick = React.useCallback(() => { + if (typeof onToggleInfoMenu === 'function') { + onToggleInfoMenu(); + } + }, [onToggleInfoMenu]); return (
- - + +
- - - + +
@@ -84,21 +116,21 @@ const ControlBar = ({ ControlBar.propTypes = { className: PropTypes.string, - paused: PropTypes.any, - time: PropTypes.any, - duration: PropTypes.any, - volume: PropTypes.any, - muted: PropTypes.any, - subtitlesTracks: PropTypes.any, - metaResource: PropTypes.any, - onPlayRequested: PropTypes.any, - onPauseRequested: PropTypes.any, - onMuteRequested: PropTypes.any, - onUnmuteRequested: PropTypes.any, - onVolumeChangeRequested: PropTypes.any, - onSeekRequested: PropTypes.any, - onToggleSubtitlesPicker: PropTypes.any, - onToggleMetaPreview: PropTypes.any + paused: PropTypes.bool, + time: PropTypes.number, + duration: PropTypes.number, + volume: PropTypes.number, + muted: PropTypes.bool, + subtitlesTracks: PropTypes.array, + info: PropTypes.object, + onPlayRequested: PropTypes.func, + onPauseRequested: PropTypes.func, + onMuteRequested: PropTypes.func, + onUnmuteRequested: PropTypes.func, + onVolumeChangeRequested: PropTypes.func, + onSeekRequested: PropTypes.func, + onToggleSubtitlesMenu: PropTypes.func, + onToggleInfoMenu: PropTypes.func }; module.exports = ControlBar; diff --git a/src/routes/Player/ControlBar/MetaPreviewButton/MetaPreviewButton.js b/src/routes/Player/ControlBar/MetaPreviewButton/MetaPreviewButton.js deleted file mode 100644 index 6bb2ec221..000000000 --- a/src/routes/Player/ControlBar/MetaPreviewButton/MetaPreviewButton.js +++ /dev/null @@ -1,29 +0,0 @@ -const React = require('react'); -const PropTypes = require('prop-types'); -const classnames = require('classnames'); -const Icon = require('stremio-icons/dom'); -const { Button } = require('stremio/common'); - -const MetaPreviewButton = ({ className, metaResource, onToggleMetaPreview }) => { - const onMouseDown = React.useCallback((event) => { - event.nativeEvent.metaPreviewClosePrevented = true; - }, []); - const onClick = React.useCallback(() => { - if (typeof onToggleMetaPreview === 'function') { - onToggleMetaPreview(); - } - }, [onToggleMetaPreview]); - return ( - - ); -}; - -MetaPreviewButton.propTypes = { - className: PropTypes.string, - metaResource: PropTypes.object, - onToggleMetaPreview: PropTypes.func -}; - -module.exports = MetaPreviewButton; diff --git a/src/routes/Player/ControlBar/MetaPreviewButton/index.js b/src/routes/Player/ControlBar/MetaPreviewButton/index.js deleted file mode 100644 index ecea92ab1..000000000 --- a/src/routes/Player/ControlBar/MetaPreviewButton/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const MetaPreviewButton = require('./MetaPreviewButton'); - -module.exports = MetaPreviewButton; diff --git a/src/routes/Player/ControlBar/MuteButton/MuteButton.js b/src/routes/Player/ControlBar/MuteButton/MuteButton.js deleted file mode 100644 index 7e3c2b0b5..000000000 --- a/src/routes/Player/ControlBar/MuteButton/MuteButton.js +++ /dev/null @@ -1,39 +0,0 @@ -const React = require('react'); -const PropTypes = require('prop-types'); -const classnames = require('classnames'); -const Icon = require('stremio-icons/dom'); -const { Button } = require('stremio/common'); - -const MuteButton = ({ className, muted, volume, onMuteRequested, onUnmuteRequested }) => { - const toggleMuted = React.useCallback(() => { - if (muted) { - if (typeof onUnmuteRequested === 'function') { - onUnmuteRequested(); - } - } else { - if (typeof onMuteRequested === 'function') { - onMuteRequested(); - } - } - }, [muted, onMuteRequested, onUnmuteRequested]); - const icon = (typeof muted === 'boolean' && muted) ? 'ic_volume0' : - (volume === null || isNaN(volume)) ? 'ic_volume3' : - volume < 30 ? 'ic_volume1' : - volume < 70 ? 'ic_volume2' : - 'ic_volume3'; - return ( - - ); -}; - -MuteButton.propTypes = { - className: PropTypes.string, - muted: PropTypes.bool, - volume: PropTypes.number, - onMuteRequested: PropTypes.func, - onUnmuteRequested: PropTypes.func -}; - -module.exports = MuteButton; diff --git a/src/routes/Player/ControlBar/MuteButton/index.js b/src/routes/Player/ControlBar/MuteButton/index.js deleted file mode 100644 index 3bc5c743b..000000000 --- a/src/routes/Player/ControlBar/MuteButton/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const MuteButton = require('./MuteButton'); - -module.exports = MuteButton; diff --git a/src/routes/Player/ControlBar/PlayPauseButton/PlayPauseButton.js b/src/routes/Player/ControlBar/PlayPauseButton/PlayPauseButton.js deleted file mode 100644 index 34d9bbd7f..000000000 --- a/src/routes/Player/ControlBar/PlayPauseButton/PlayPauseButton.js +++ /dev/null @@ -1,36 +0,0 @@ -const React = require('react'); -const PropTypes = require('prop-types'); -const classnames = require('classnames'); -const Icon = require('stremio-icons/dom'); -const { Button } = require('stremio/common'); - -const PlayPauseButton = ({ className, paused, onPlayRequested, onPauseRequested }) => { - const togglePaused = React.useCallback(() => { - if (paused) { - if (typeof onPlayRequested === 'function') { - onPlayRequested(); - } - } else { - if (typeof onPauseRequested === 'function') { - onPauseRequested(); - } - } - }, [paused, onPlayRequested, onPauseRequested]); - return ( - - ); -}; - -PlayPauseButton.propTypes = { - className: PropTypes.string, - paused: PropTypes.bool, - onPlayRequested: PropTypes.func, - onPauseRequested: PropTypes.func -}; - -module.exports = PlayPauseButton; diff --git a/src/routes/Player/ControlBar/PlayPauseButton/index.js b/src/routes/Player/ControlBar/PlayPauseButton/index.js deleted file mode 100644 index cdaf43c88..000000000 --- a/src/routes/Player/ControlBar/PlayPauseButton/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const PlayPauseButton = require('./PlayPauseButton'); - -module.exports = PlayPauseButton; diff --git a/src/routes/Player/ControlBar/SubtitlesButton/SubtitlesButton.js b/src/routes/Player/ControlBar/SubtitlesButton/SubtitlesButton.js deleted file mode 100644 index 891247025..000000000 --- a/src/routes/Player/ControlBar/SubtitlesButton/SubtitlesButton.js +++ /dev/null @@ -1,29 +0,0 @@ -const React = require('react'); -const PropTypes = require('prop-types'); -const classnames = require('classnames'); -const Icon = require('stremio-icons/dom'); -const { Button } = require('stremio/common'); - -const SubtitlesButton = ({ className, subtitlesTracks, onToggleSubtitlesPicker }) => { - const onMouseDown = React.useCallback((event) => { - event.nativeEvent.subtitlesPickerClosePrevented = true; - }, []); - const onClick = React.useCallback(() => { - if (typeof onToggleSubtitlesPicker === 'function') { - onToggleSubtitlesPicker(); - } - }, [onToggleSubtitlesPicker]); - return ( - - ); -}; - -SubtitlesButton.propTypes = { - className: PropTypes.string, - subtitlesTracks: PropTypes.array, - onToggleSubtitlesPicker: PropTypes.func -}; - -module.exports = SubtitlesButton; diff --git a/src/routes/Player/ControlBar/SubtitlesButton/index.js b/src/routes/Player/ControlBar/SubtitlesButton/index.js deleted file mode 100644 index 53fd48771..000000000 --- a/src/routes/Player/ControlBar/SubtitlesButton/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const SubtitlesButton = require('./SubtitlesButton'); - -module.exports = SubtitlesButton; diff --git a/src/routes/Player/ControlBar/styles.less b/src/routes/Player/ControlBar/styles.less index 5fa5ae688..6cd36691f 100644 --- a/src/routes/Player/ControlBar/styles.less +++ b/src/routes/Player/ControlBar/styles.less @@ -4,6 +4,9 @@ padding: 0 1.5rem; .seek-bar { + --track-size: 0.5rem; + --thumb-size: 1.5rem; + height: 2.5rem; } @@ -21,12 +24,12 @@ align-items: center; &:global(.disabled) { - :global(.icon) { + .icon { fill: @color-surface; } } - :global(.icon) { + .icon { flex: none; width: 3rem; height: 2rem; diff --git a/src/routes/Player/InfoMenu/InfoMenu.js b/src/routes/Player/InfoMenu/InfoMenu.js new file mode 100644 index 000000000..e47deff20 --- /dev/null +++ b/src/routes/Player/InfoMenu/InfoMenu.js @@ -0,0 +1,43 @@ +const React = require('react'); +const PropTypes = require('prop-types'); +const classnames = require('classnames'); +const { MetaPreview, CONSTANTS } = require('stremio/common'); +const styles = require('./styles'); + +const InfoMenu = ({ className, ...props }) => { + // TODO handle stream and addon + const metaItem = React.useMemo(() => { + return props.metaItem !== null ? + { + ...props.metaItem, + links: props.metaItem.links.filter(({ category }) => category === CONSTANTS.SHARE_LINK_CATEGORY) + } + : + null; + }, [props.metaItem]); + const onMouseDown = React.useCallback((event) => { + event.nativeEvent.infoMenuClosePrevented = true; + }, []); + return ( +
+ +
+ ); +}; + +InfoMenu.propTypes = { + className: PropTypes.string, + metaItem: PropTypes.object +}; + +module.exports = InfoMenu; diff --git a/src/routes/Player/InfoMenu/index.js b/src/routes/Player/InfoMenu/index.js new file mode 100644 index 000000000..a5f56d1ef --- /dev/null +++ b/src/routes/Player/InfoMenu/index.js @@ -0,0 +1,3 @@ +const InfoMenu = require('./InfoMenu'); + +module.exports = InfoMenu; diff --git a/src/routes/Player/InfoMenu/styles.less b/src/routes/Player/InfoMenu/styles.less new file mode 100644 index 000000000..e3f8f74e9 --- /dev/null +++ b/src/routes/Player/InfoMenu/styles.less @@ -0,0 +1,3 @@ +.info-menu-container { + width: 30rem; +} \ No newline at end of file diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 1e6861ac5..7f05e926e 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -4,26 +4,30 @@ const classnames = require('classnames'); const debounce = require('lodash.debounce'); const { useRouteFocused } = require('stremio-router'); const { useServices } = require('stremio/services'); -const { HorizontalNavBar, useDeepEqualEffect, useDeepEqualMemo, useFullscreen, useBinaryState, useToast } = require('stremio/common'); +const { HorizontalNavBar, useDeepEqualEffect, useFullscreen, useBinaryState, useToast, useProfile } = require('stremio/common'); const BufferingLoader = require('./BufferingLoader'); const ControlBar = require('./ControlBar'); -const SubtitlesPicker = require('./SubtitlesPicker'); +const InfoMenu = require('./InfoMenu'); +const SubtitlesMenu = require('./SubtitlesMenu'); const Video = require('./Video'); +const useInfo = require('./useInfo'); const usePlayer = require('./usePlayer'); const useSettings = require('./useSettings'); const styles = require('./styles'); const Player = ({ urlParams }) => { const { core } = useServices(); - const player = usePlayer(urlParams); - const [settings, updateSettings] = useSettings(); + const profile = useProfile(); + const [player, updateLibraryItemState, pushToLibrary] = usePlayer(urlParams); + const [settings, updateSettings] = useSettings(profile); + const info = useInfo(player, profile); const routeFocused = useRouteFocused(); const toast = useToast(); const [, , , toggleFullscreen] = useFullscreen(); const [immersed, setImmersed] = React.useState(true); const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []); - const [subtitlesPickerOpen, , closeSubtitlesPicker, toggleSubtitlesPicker] = useBinaryState(false); - const [metaPreviewOpen, , closeMetaPreview, toggleMetaPreview] = useBinaryState(false); + const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false); + const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false); const [error, setError] = React.useState(null); const [videoState, setVideoState] = React.useReducer( (videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }), @@ -63,7 +67,7 @@ const Player = ({ urlParams }) => { const onPropChanged = React.useCallback((propName, propValue) => { setVideoState({ [propName]: propValue }); }, []); - const onEnded = useDeepEqualMemo(() => () => { + const onEnded = React.useCallback(() => { core.dispatch({ action: 'Unload' }, 'player'); if (player.lib_item !== null) { core.dispatch({ @@ -78,7 +82,7 @@ const Player = ({ urlParams }) => { // TODO go to next video } window.history.back(); - }, [player.next_video, player.lib_item]); + }, [player]); const onError = React.useCallback((error) => { if (error.critical) { setError(error); @@ -102,9 +106,11 @@ const Player = ({ urlParams }) => { const onPlayRequested = React.useCallback(() => { dispatch({ propName: 'paused', propValue: false }); }, []); + const onPlayRequestedDebounced = React.useCallback(debounce(onPlayRequested, 200), []); const onPauseRequested = React.useCallback(() => { dispatch({ propName: 'paused', propValue: true }); }, []); + const onPauseRequestedDebounced = React.useCallback(debounce(onPauseRequested, 200), []); const onMuteRequested = React.useCallback(() => { dispatch({ propName: 'muted', propValue: true }); }, []); @@ -132,18 +138,23 @@ const Player = ({ urlParams }) => { const onVideoClick = React.useCallback(() => { if (videoState.paused !== null) { if (videoState.paused) { - onPlayRequested(); + onPlayRequestedDebounced(); } else { - onPauseRequested(); + onPauseRequestedDebounced(); } } }, [videoState.paused]); + const onVideoDoubleClick = React.useCallback(() => { + onPlayRequestedDebounced.cancel(); + onPauseRequestedDebounced.cancel(); + toggleFullscreen(); + }, [toggleFullscreen]); const onContainerMouseDown = React.useCallback((event) => { - if (!event.nativeEvent.subtitlesPickerClosePrevented) { - closeSubtitlesPicker(); + if (!event.nativeEvent.subtitlesMenuClosePrevented) { + closeSubtitlesMenu(); } - if (!event.nativeEvent.metaPreviewClosePrevented) { - closeMetaPreview(); + if (!event.nativeEvent.infoMenuClosePrevented) { + closeInfoMenu(); } }, []); const onContainerMouseMove = React.useCallback((event) => { @@ -181,7 +192,14 @@ const Player = ({ urlParams }) => { dispatch({ commandName: 'addSubtitlesTracks', commandArgs: { - tracks: player.selected.stream.subtitles + tracks: player.selected.stream.subtitles.map(({ url, lang }) => ({ + url, + lang, + origin: player.selected.stream.addon !== null ? + player.selected.stream.addon.manifest.name + : + 'EMBEDDED IN STREAM' + })) } }); } @@ -193,7 +211,11 @@ const Player = ({ urlParams }) => { tracks: player.subtitles_resources .filter((subtitles_resource) => subtitles_resource.content.type === 'Ready') .reduce((tracks, subtitles_resource) => { - return tracks.concat(subtitles_resource.content.content); + return tracks.concat(subtitles_resource.content.content.map(({ url, lang }) => ({ + url, + lang, + origin: subtitles_resource.addon !== null ? subtitles_resource.addon.manifest.name : subtitles_resource.request.base + }))); }, []) } }); @@ -215,36 +237,30 @@ const Player = ({ urlParams }) => { }, [settings.subtitles_offset]); React.useEffect(() => { if (videoState.time !== null && !isNaN(videoState.time) && videoState.duration !== null && !isNaN(videoState.duration)) { - core.dispatch({ - action: 'Player', - args: { - action: 'UpdateLibraryItemState', - args: { - time: videoState.time, - duration: videoState.duration - } - } - }, 'player'); + updateLibraryItemState(videoState.time, videoState.duration); } }, [videoState.time, videoState.duration]); React.useEffect(() => { - const interval = setInterval(() => { - core.dispatch({ - action: 'Player', - args: { - action: 'PushToLibrary' - } - }, 'player'); - }, 30000); + if (!Array.isArray(videoState.subtitlesTracks) || videoState.subtitlesTracks.length === 0) { + closeSubtitlesMenu(); + } + }, [videoState.subtitlesTracks]); + React.useEffect(() => { + if (info === null) { + closeInfoMenu(); + } + }, [info]); + React.useEffect(() => { + const intervalId = setInterval(pushToLibrary, 30000); return () => { - clearInterval(interval); + clearInterval(intervalId); }; }, []); React.useLayoutEffect(() => { const onKeyDown = (event) => { switch (event.code) { case 'Space': { - if (!subtitlesPickerOpen && !metaPreviewOpen && videoState.paused !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && videoState.paused !== null) { if (videoState.paused) { onPlayRequested(); } else { @@ -255,46 +271,52 @@ const Player = ({ urlParams }) => { break; } case 'ArrowRight': { - if (!subtitlesPickerOpen && !metaPreviewOpen && videoState.time !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) { onSeekRequested(videoState.time + 15000); } break; } case 'ArrowLeft': { - if (!subtitlesPickerOpen && !metaPreviewOpen && videoState.time !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) { onSeekRequested(videoState.time - 15000); } break; } case 'ArrowUp': { - if (!subtitlesPickerOpen && !metaPreviewOpen && videoState.volume !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && videoState.volume !== null) { onVolumeChangeRequested(videoState.volume + 5); } break; } case 'ArrowDown': { - if (!subtitlesPickerOpen && !metaPreviewOpen && videoState.volume !== null) { + if (!subtitlesMenuOpen && !infoMenuOpen && videoState.volume !== null) { onVolumeChangeRequested(videoState.volume - 5); } break; } case 'KeyS': { - closeMetaPreview(); - toggleSubtitlesPicker(); + closeInfoMenu(); + if (Array.isArray(videoState.subtitlesTracks) && videoState.subtitlesTracks.length > 0) { + toggleSubtitlesMenu(); + } + break; } case 'KeyM': { - closeSubtitlesPicker(); - toggleMetaPreview(); + closeSubtitlesMenu(); + if (info !== null) { + toggleInfoMenu(); + } + break; } case 'Escape': { - closeSubtitlesPicker(); - closeMetaPreview(); + closeSubtitlesMenu(); + closeInfoMenu(); break; } } @@ -305,14 +327,16 @@ const Player = ({ urlParams }) => { return () => { window.removeEventListener('keydown', onKeyDown); }; - }, [routeFocused, subtitlesPickerOpen, metaPreviewOpen, videoState.paused, videoState.time, videoState.volume, toggleSubtitlesPicker, toggleMetaPreview]); + }, [routeFocused, subtitlesMenuOpen, infoMenuOpen, info, videoState.paused, videoState.time, videoState.volume, videoState.subtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu]); React.useLayoutEffect(() => { return () => { setImmersedDebounced.cancel(); + onPlayRequestedDebounced.cancel(); + onPauseRequestedDebounced.cancel(); }; }, []); return ( -
{
+ { + subtitlesMenuOpen || infoMenuOpen ? +
+ : + null + } { volume={videoState.volume} muted={videoState.muted} subtitlesTracks={videoState.subtitlesTracks} - metaResource={null} + info={info} onPlayRequested={onPlayRequested} onPauseRequested={onPauseRequested} onMuteRequested={onMuteRequested} onUnmuteRequested={onUnmuteRequested} onVolumeChangeRequested={onVolumeChangeRequested} onSeekRequested={onSeekRequested} - onToggleSubtitlesPicker={toggleSubtitlesPicker} - onToggleMetaPreview={toggleMetaPreview} + onToggleSubtitlesMenu={toggleSubtitlesMenu} + onToggleInfoMenu={toggleInfoMenu} onMouseMove={onBarMouseMove} onMouseOver={onBarMouseMove} /> { - subtitlesPickerOpen ? - { : null } - {/* { - metaPreviewOpen ? -
event.nativeEvent.metaPreviewClosePrevented = true}> -
-
+ { + infoMenuOpen ? + : null - } */} + }
); }; diff --git a/src/routes/Player/SubtitlesPicker/DiscreteSelectInput/DiscreteSelectInput.js b/src/routes/Player/SubtitlesMenu/DiscreteSelectInput/DiscreteSelectInput.js similarity index 100% rename from src/routes/Player/SubtitlesPicker/DiscreteSelectInput/DiscreteSelectInput.js rename to src/routes/Player/SubtitlesMenu/DiscreteSelectInput/DiscreteSelectInput.js diff --git a/src/routes/Player/SubtitlesPicker/DiscreteSelectInput/index.js b/src/routes/Player/SubtitlesMenu/DiscreteSelectInput/index.js similarity index 100% rename from src/routes/Player/SubtitlesPicker/DiscreteSelectInput/index.js rename to src/routes/Player/SubtitlesMenu/DiscreteSelectInput/index.js diff --git a/src/routes/Player/SubtitlesPicker/DiscreteSelectInput/styles.less b/src/routes/Player/SubtitlesMenu/DiscreteSelectInput/styles.less similarity index 95% rename from src/routes/Player/SubtitlesPicker/DiscreteSelectInput/styles.less rename to src/routes/Player/SubtitlesMenu/DiscreteSelectInput/styles.less index 0c286ae78..657d46df1 100644 --- a/src/routes/Player/SubtitlesPicker/DiscreteSelectInput/styles.less +++ b/src/routes/Player/SubtitlesMenu/DiscreteSelectInput/styles.less @@ -34,7 +34,7 @@ display: block; width: 100%; height: 100%; - fill: @color-surface-light5; + fill: @color-surface-light5-90; } } diff --git a/src/routes/Player/SubtitlesPicker/SubtitlesPicker.js b/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js similarity index 97% rename from src/routes/Player/SubtitlesPicker/SubtitlesPicker.js rename to src/routes/Player/SubtitlesMenu/SubtitlesMenu.js index a04e470f2..b06dacb67 100644 --- a/src/routes/Player/SubtitlesPicker/SubtitlesPicker.js +++ b/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js @@ -6,14 +6,14 @@ const DiscreteSelectInput = require('./DiscreteSelectInput'); const styles = require('./styles'); const ORIGIN_PRIORITIES = { - 'EMBEDDED': 1, - 'Stream': 2 + 'EMBEDDED IN VIDEO': 1, + 'EMBEDDED IN STREAM': 2 }; const LANGUAGE_PRIORITIES = { 'eng': 1 }; -const SubtitlesPicker = (props) => { +const SubtitlesMenu = (props) => { const languages = React.useMemo(() => { return Array.isArray(props.tracks) ? props.tracks @@ -51,7 +51,7 @@ const SubtitlesPicker = (props) => { []; }, [props.tracks, selectedLanguage]); const onMouseDown = React.useCallback((event) => { - event.nativeEvent.subtitlesPickerClosePrevented = true; + event.nativeEvent.subtitlesMenuClosePrevented = true; }, []); const languageOnClick = React.useCallback((event) => { const trackId = Array.isArray(props.tracks) ? @@ -101,7 +101,7 @@ const SubtitlesPicker = (props) => { } }, [props.offset, props.onOffsetChanged]); return ( -
+
Languages
@@ -182,7 +182,7 @@ const SubtitlesPicker = (props) => { ); }; -SubtitlesPicker.propTypes = { +SubtitlesMenu.propTypes = { className: PropTypes.string, tracks: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string.isRequired, @@ -202,4 +202,4 @@ SubtitlesPicker.propTypes = { onOffsetChanged: PropTypes.func }; -module.exports = SubtitlesPicker; +module.exports = SubtitlesMenu; diff --git a/src/routes/Player/SubtitlesMenu/index.js b/src/routes/Player/SubtitlesMenu/index.js new file mode 100644 index 000000000..17f9571d4 --- /dev/null +++ b/src/routes/Player/SubtitlesMenu/index.js @@ -0,0 +1,3 @@ +const SubtitlesMenu = require('./SubtitlesMenu'); + +module.exports = SubtitlesMenu; diff --git a/src/routes/Player/SubtitlesPicker/styles.less b/src/routes/Player/SubtitlesMenu/styles.less similarity index 93% rename from src/routes/Player/SubtitlesPicker/styles.less rename to src/routes/Player/SubtitlesMenu/styles.less index eac05c0aa..8b60354ab 100644 --- a/src/routes/Player/SubtitlesPicker/styles.less +++ b/src/routes/Player/SubtitlesMenu/styles.less @@ -1,10 +1,9 @@ @import (reference) '~stremio-colors/dist/less/stremio-colors.less'; -.subtitles-picker-container { +.subtitles-menu-container { height: 23rem; display: flex; flex-direction: row; - background-color: @color-background-dark1; .languages-container, .variants-container, .subtitles-settings-container { flex: none; @@ -49,7 +48,7 @@ height: 0.5rem; border-radius: 100%; margin-left: 1rem; - background-color: @color-accent3; + background-color: @color-accent3-90; } } } @@ -100,7 +99,7 @@ } &:global(.disabled) { - color: @color-surface; + color: @color-surface-90; } } } diff --git a/src/routes/Player/SubtitlesPicker/index.js b/src/routes/Player/SubtitlesPicker/index.js deleted file mode 100644 index 0eb5e540e..000000000 --- a/src/routes/Player/SubtitlesPicker/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const SubtitlesPicker = require('./SubtitlesPicker'); - -module.exports = SubtitlesPicker; diff --git a/src/routes/Player/styles.less b/src/routes/Player/styles.less index 9c3c6ce50..01aad2eef 100644 --- a/src/routes/Player/styles.less +++ b/src/routes/Player/styles.less @@ -26,7 +26,7 @@ html:not(.active-slider-within) { z-index: 0; width: 100%; height: 100%; - background-color: @color-background-dark2; + background-color: @color-background-dark5; .layer { position: absolute; @@ -104,6 +104,7 @@ html:not(.active-slider-within) { bottom: 8rem; max-height: calc(100% - 13.5rem); max-width: calc(100% - 4rem); + background-color: @color-background-dark1; box-shadow: 0 1.35rem 2.7rem @color-background-dark5-40, 0 1.1rem 0.85rem @color-background-dark5-20; overflow: auto; diff --git a/src/routes/Player/useInfo.js b/src/routes/Player/useInfo.js new file mode 100644 index 000000000..08f85b7c6 --- /dev/null +++ b/src/routes/Player/useInfo.js @@ -0,0 +1,41 @@ +const React = require('react'); + +const useInfo = (player, profile) => { + const info = React.useMemo(() => { + if (player.selected === null) { + return null; + } + + const stream = player.selected.stream; + const addon = stream.addon; + const metaItem = player.meta_resource !== null && player.meta_resource.content.type === 'Ready' ? + player.meta_resource.content.content + : + null; + const video = metaItem !== null ? + metaItem.videos.reduce((result, video) => { + if (video.id === player.selected.video_id) { + return video; + } + + return result; + }, null) + : + null; + const streamTitle = typeof stream.title === 'string' ? stream.title : ''; + const metaItemTitle = metaItem !== null ? metaItem.name : ''; + const videoTitle = video !== null && typeof video.title === 'string' && video.title.length > 0 ? video.title : ''; + const seriesInfo = video !== null && !isNaN(video.season) && !isNaN(video.episode) ? `${video.season}x${video.episode}` : ''; + const title = metaItemTitle.length > 0 ? + metaItemTitle + .concat(videoTitle.length > 0 || seriesInfo.length > 0 ? ' -' : '') + .concat(videoTitle.length > 0 ? ` ${videoTitle}` : '') + .concat(seriesInfo.length > 0 ? ` (${seriesInfo})` : '') + : + streamTitle; + return { stream, addon, metaItem, title }; + }, [player, profile]); + return info; +}; + +module.exports = useInfo; diff --git a/src/routes/Player/usePlayer.js b/src/routes/Player/usePlayer.js index 526d95278..b7bb38b2f 100644 --- a/src/routes/Player/usePlayer.js +++ b/src/routes/Player/usePlayer.js @@ -1,5 +1,6 @@ const React = require('react'); const pako = require('pako'); +const { useServices } = require('stremio/services'); const { useModelState } = require('stremio/common'); const initPlayerState = () => ({ @@ -15,23 +16,13 @@ const mapPlayerStateWithCtx = (player, ctx) => { { stream: { ...player.selected.stream, - subtitles: Array.isArray(player.selected.stream.subtitles) ? - player.selected.stream.subtitles.map(({ url, lang }) => ({ - url, - lang, - origin: ctx.profile.addons.reduce((origin, addon) => { - if (player.selected.stream_resource_request !== null && addon.transportUrl === player.selected.stream_resource_request.base) { - return typeof addon.manifest.name === 'string' && addon.manifest.name.length > 0 ? - addon.manifest.name - : - addon.manifest.id; - } + addon: ctx.profile.addons.reduce((result, addon) => { + if (player.selected.stream_resource_request !== null && addon.transportUrl === player.selected.stream_resource_request.base) { + return addon; + } - return origin; - }, player.selected.stream_resource_request !== null ? player.selected.stream_resource_request.base : 'Stream') - })) - : - [] + return result; + }, null) }, stream_resource_request: player.selected.stream_resource_request, meta_resource_request: player.selected.meta_resource_request, @@ -40,48 +31,54 @@ const mapPlayerStateWithCtx = (player, ctx) => { } : null; - const meta_resource = player.meta_resource; + const meta_resource = player.meta_resource !== null && player.meta_resource.content.type === 'Ready' ? + { + request: player.meta_resource.request, + content: { + type: 'Ready', + content: { + ...player.meta_resource.content.content, + released: new Date( + typeof player.meta_resource.content.content.released === 'string' ? + player.meta_resource.content.content.released + : + NaN + ), + videos: player.meta_resource.content.content.videos.map((video) => ({ + ...video, + released: new Date( + typeof video.released === 'string' ? + video.released + : + NaN + ), + // TODO add watched and progress + href: `#/metadetails/${player.meta_resource.content.content.type}/${player.meta_resource.content.content.id}/${video.id}` + })) + } + } + } + : + player.meta_resource; const subtitles_resources = player.subtitles_resources.map((subtitles_resource) => { const request = subtitles_resource.request; - const origin = ctx.profile.addons.reduce((origin, addon) => { + const addon = ctx.profile.addons.reduce((result, addon) => { if (addon.transportUrl === subtitles_resource.request.base) { - return typeof addon.manifest.name === 'string' && addon.manifest.name.length > 0 ? - addon.manifest.name - : - addon.manifest.id; + return addon; } - return origin; - }, subtitles_resource.request.base); - const content = subtitles_resource.content.type === 'Ready' ? - { - type: 'Ready', - content: subtitles_resource.content.content.map(({ url, lang }) => ({ - url, - lang, - origin - })) - } - : - subtitles_resource.content; - return { - request, - origin, - content - }; + return result; + }, null); + const content = subtitles_resource.content; + return { request, addon, content }; }); const next_video = player.next_video; const lib_item = player.lib_item; - return { - selected, - meta_resource, - subtitles_resources, - next_video, - lib_item - }; + return { selected, meta_resource, subtitles_resources, next_video, lib_item }; }; const usePlayer = (urlParams) => { + const { core } = useServices(); const loadPlayerAction = React.useMemo(() => { try { return { @@ -123,7 +120,10 @@ const usePlayer = (urlParams) => { } : null, - video_id: urlParams.videoId + video_id: typeof urlParams.videoId === 'string' ? + urlParams.videoId + : + null } } }; @@ -133,12 +133,30 @@ const usePlayer = (urlParams) => { }; } }, [urlParams]); - return useModelState({ + const updateLibraryItemState = React.useCallback((time, duration) => { + core.dispatch({ + action: 'Player', + args: { + action: 'UpdateLibraryItemState', + args: { time, duration } + } + }, 'player'); + }, []); + const pushToLibrary = React.useCallback(() => { + core.dispatch({ + action: 'Player', + args: { + action: 'PushToLibrary' + } + }, 'player'); + }, []); + const player = useModelState({ model: 'player', action: loadPlayerAction, init: initPlayerState, mapWithCtx: mapPlayerStateWithCtx }); + return [player, updateLibraryItemState, pushToLibrary]; }; module.exports = usePlayer; diff --git a/src/routes/Player/useSettings.js b/src/routes/Player/useSettings.js index 8ad45791c..be82d6034 100644 --- a/src/routes/Player/useSettings.js +++ b/src/routes/Player/useSettings.js @@ -1,10 +1,8 @@ const React = require('react'); -const { useProfile } = require('stremio/common'); const { useServices } = require('stremio/services'); -const useSettings = () => { +const useSettings = (profile) => { const { core } = useServices(); - const profile = useProfile(); const updateSettings = React.useCallback((settings) => { core.dispatch({ action: 'Ctx', @@ -16,7 +14,7 @@ const useSettings = () => { } } }); - }, [profile.settings]); + }, [profile]); return [profile.settings, updateSettings]; }; diff --git a/src/video/HTMLSubtitles.js b/src/video/HTMLSubtitles.js index a9764557e..d4b6d73d5 100644 --- a/src/video/HTMLSubtitles.js +++ b/src/video/HTMLSubtitles.js @@ -55,7 +55,7 @@ function HTMLSubtitles(options) { track.url.length > 0 && typeof track.origin === 'string' && track.origin.length > 0 && - track.origin !== 'EMBEDDED'; + track.origin !== 'EMBEDDED IN VIDEO'; }) .map(function(track) { return Object.freeze(Object.assign({}, track, { diff --git a/src/video/YouTubeVideo.js b/src/video/YouTubeVideo.js index 1ef9bc466..e679e0a5e 100644 --- a/src/video/YouTubeVideo.js +++ b/src/video/YouTubeVideo.js @@ -84,7 +84,7 @@ function YouTubeVideo(options) { .map(function(track) { return Object.freeze({ id: track.languageCode, - origin: 'EMBEDDED', + origin: 'EMBEDDED IN VIDEO', label: track.languageName }); }); @@ -482,7 +482,7 @@ function YouTubeVideo(options) { embeddedSubtitlesSelectedTrackId = null; var tracks = getSubtitlesTracks(); for (var i = 0; i < tracks.length; i++) { - if (tracks[i].id === arguments[2] && tracks[i].origin === 'EMBEDDED') { + if (tracks[i].id === arguments[2] && tracks[i].origin === 'EMBEDDED IN VIDEO') { embeddedSubtitlesSelectedTrackId = tracks[i].id; break; }