Merge pull request #152 from Stremio/player-meta-preview

Player meta preview
This commit is contained in:
Nikola Hristov 2020-04-03 15:14:57 +03:00 committed by GitHub
commit dae56660dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 374 additions and 351 deletions

View file

@ -4,6 +4,8 @@ const CATALOG_PREVIEW_SIZE = 10;
const CATALOG_PAGE_SIZE = 100; const CATALOG_PAGE_SIZE = 100;
const NONE_EXTRA_VALUE = 'None'; const NONE_EXTRA_VALUE = 'None';
const SKIP_EXTRA_NAME = 'skip'; const SKIP_EXTRA_NAME = 'skip';
const IMDB_LINK_CATEGORY = 'imdb';
const SHARE_LINK_CATEGORY = 'share';
const TYPE_PRIORITIES = { const TYPE_PRIORITIES = {
movie: 10, movie: 10,
series: 9, series: 9,
@ -25,5 +27,7 @@ module.exports = {
CATALOG_PAGE_SIZE, CATALOG_PAGE_SIZE,
NONE_EXTRA_VALUE, NONE_EXTRA_VALUE,
SKIP_EXTRA_NAME, SKIP_EXTRA_NAME,
IMDB_LINK_CATEGORY,
SHARE_LINK_CATEGORY,
TYPE_PRIORITIES TYPE_PRIORITIES
}; };

View file

@ -8,6 +8,7 @@ const Button = require('stremio/common/Button');
const Image = require('stremio/common/Image'); const Image = require('stremio/common/Image');
const ModalDialog = require('stremio/common/ModalDialog'); const ModalDialog = require('stremio/common/ModalDialog');
const SharePrompt = require('stremio/common/SharePrompt'); const SharePrompt = require('stremio/common/SharePrompt');
const CONSTANTS = require('stremio/common/CONSTANTS');
const routesRegexp = require('stremio/common/routesRegexp'); const routesRegexp = require('stremio/common/routesRegexp');
const useBinaryState = require('stremio/common/useBinaryState'); const useBinaryState = require('stremio/common/useBinaryState');
const ActionButton = require('./ActionButton'); const ActionButton = require('./ActionButton');
@ -15,8 +16,6 @@ const MetaLinks = require('./MetaLinks');
const MetaPreviewPlaceholder = require('./MetaPreviewPlaceholder'); const MetaPreviewPlaceholder = require('./MetaPreviewPlaceholder');
const styles = require('./styles'); const styles = require('./styles');
const IMDB_LINK_CATEGORY = 'imdb';
const SHARE_LINK_CATEGORY = 'share';
const ALLOWED_LINK_REDIRECTS = [ const ALLOWED_LINK_REDIRECTS = [
routesRegexp.search.regexp, routesRegexp.search.regexp,
routesRegexp.discover.regexp, routesRegexp.discover.regexp,
@ -30,12 +29,12 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
links links
.filter((link) => link && typeof link.category === 'string' && typeof link.url === 'string') .filter((link) => link && typeof link.category === 'string' && typeof link.url === 'string')
.reduce((linksGroups, { category, name, url }) => { .reduce((linksGroups, { category, name, url }) => {
if (category === IMDB_LINK_CATEGORY) { if (category === CONSTANTS.IMDB_LINK_CATEGORY) {
linksGroups[category] = { linksGroups[category] = {
label: name, label: name,
href: `https://www.stremio.com/warning#${encodeURIComponent(`https://www.imdb.com/title/${encodeURIComponent(url)}`)}` 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] = { linksGroups[category] = {
label: name, label: name,
href: url href: url
@ -96,7 +95,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
null 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' ?
<div className={styles['runtime-release-info-container']}> <div className={styles['runtime-release-info-container']}>
{ {
typeof runtime === 'string' && runtime.length > 0 ? typeof runtime === 'string' && runtime.length > 0 ?
@ -114,16 +113,16 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
null null
} }
{ {
typeof linksGroups[IMDB_LINK_CATEGORY] === 'object' ? typeof linksGroups[CONSTANTS.IMDB_LINK_CATEGORY] === 'object' ?
<Button <Button
className={styles['imdb-button-container']} className={styles['imdb-button-container']}
title={linksGroups[IMDB_LINK_CATEGORY].label} title={linksGroups[CONSTANTS.IMDB_LINK_CATEGORY].label}
href={linksGroups[IMDB_LINK_CATEGORY].href} href={linksGroups[CONSTANTS.IMDB_LINK_CATEGORY].href}
target={'_blank'} target={'_blank'}
{...(compact ? { tabIndex: -1 } : null)} {...(compact ? { tabIndex: -1 } : null)}
> >
<Icon className={styles['icon']} icon={'ic_imdbnoframe'} /> <Icon className={styles['icon']} icon={'ic_imdbnoframe'} />
<div className={styles['label']}>{linksGroups[IMDB_LINK_CATEGORY].label}</div> <div className={styles['label']}>{linksGroups[CONSTANTS.IMDB_LINK_CATEGORY].label}</div>
</Button> </Button>
: :
null null
@ -149,8 +148,8 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
{ {
Object.keys(linksGroups) Object.keys(linksGroups)
.filter((category) => { .filter((category) => {
return category !== IMDB_LINK_CATEGORY && return category !== CONSTANTS.IMDB_LINK_CATEGORY &&
category !== SHARE_LINK_CATEGORY; category !== CONSTANTS.SHARE_LINK_CATEGORY;
}) })
.map((category, index) => ( .map((category, index) => (
<MetaLinks <MetaLinks
@ -188,7 +187,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
null null
} }
{ {
!compact && typeof linksGroups[SHARE_LINK_CATEGORY] === 'object' ? typeof linksGroups[CONSTANTS.SHARE_LINK_CATEGORY] === 'object' ?
<React.Fragment> <React.Fragment>
<ActionButton <ActionButton
className={styles['action-button']} className={styles['action-button']}
@ -202,7 +201,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
<ModalDialog title={'Share'} onCloseRequest={closeShareModal}> <ModalDialog title={'Share'} onCloseRequest={closeShareModal}>
<SharePrompt <SharePrompt
className={styles['share-prompt']} className={styles['share-prompt']}
url={linksGroups[SHARE_LINK_CATEGORY].href} url={linksGroups[CONSTANTS.SHARE_LINK_CATEGORY].href}
/> />
</ModalDialog> </ModalDialog>
: :

View file

@ -9,9 +9,6 @@ html.active-slider-within {
} }
.slider-container { .slider-container {
--track-size: 0.5rem;
--thumb-size: 1.5rem;
position: relative; position: relative;
z-index: 0; z-index: 0;
overflow: visible; overflow: visible;

View file

@ -3,11 +3,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const Icon = require('stremio-icons/dom'); const Icon = require('stremio-icons/dom');
const { Button } = require('stremio/common'); const { Button } = require('stremio/common');
const MetaPreviewButton = require('./MetaPreviewButton');
const MuteButton = require('./MuteButton');
const PlayPauseButton = require('./PlayPauseButton');
const SeekBar = require('./SeekBar'); const SeekBar = require('./SeekBar');
const SubtitlesButton = require('./SubtitlesButton');
const VolumeSlider = require('./VolumeSlider'); const VolumeSlider = require('./VolumeSlider');
const styles = require('./styles'); const styles = require('./styles');
@ -19,17 +15,55 @@ const ControlBar = ({
volume, volume,
muted, muted,
subtitlesTracks, subtitlesTracks,
metaResource, info,
onPlayRequested, onPlayRequested,
onPauseRequested, onPauseRequested,
onMuteRequested, onMuteRequested,
onUnmuteRequested, onUnmuteRequested,
onVolumeChangeRequested, onVolumeChangeRequested,
onSeekRequested, onSeekRequested,
onToggleSubtitlesPicker, onToggleSubtitlesMenu,
onToggleMetaPreview, onToggleInfoMenu,
...props ...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 ( return (
<div {...props} className={classnames(className, styles['control-bar-container'])}> <div {...props} className={classnames(className, styles['control-bar-container'])}>
<SeekBar <SeekBar
@ -39,19 +73,21 @@ const ControlBar = ({
onSeekRequested={onSeekRequested} onSeekRequested={onSeekRequested}
/> />
<div className={styles['control-bar-buttons-container']}> <div className={styles['control-bar-buttons-container']}>
<PlayPauseButton <Button className={classnames(styles['control-bar-button'], { 'disabled': typeof paused !== 'boolean' })} title={paused ? 'Play' : 'Pause'} tabIndex={-1} onClick={onPlayPauseButtonClick}>
className={styles['control-bar-button']} <Icon className={styles['icon']} icon={typeof paused !== 'boolean' || paused ? 'ic_play' : 'ic_pause'} />
paused={paused} </Button>
onPlayRequested={onPlayRequested} <Button className={classnames(styles['control-bar-button'], { 'disabled': typeof muted !== 'boolean' })} title={muted ? 'Unmute' : 'Mute'} tabIndex={-1} onClick={onMuteButtonClick}>
onPauseRequested={onPauseRequested} <Icon
/> className={styles['icon']}
<MuteButton icon={
className={styles['control-bar-button']} (typeof muted === 'boolean' && muted) ? 'ic_volume0' :
volume={volume} (volume === null || isNaN(volume)) ? 'ic_volume3' :
muted={muted} volume < 30 ? 'ic_volume1' :
onMuteRequested={onMuteRequested} volume < 70 ? 'ic_volume2' :
onUnmuteRequested={onUnmuteRequested} 'ic_volume3'
/> }
/>
</Button>
<VolumeSlider <VolumeSlider
className={styles['volume-slider']} className={styles['volume-slider']}
volume={volume} volume={volume}
@ -59,23 +95,19 @@ const ControlBar = ({
/> />
<div className={styles['spacing']} /> <div className={styles['spacing']} />
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}> <Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
<Icon className={'icon'} icon={'ic_network'} /> <Icon className={styles['icon']} icon={'ic_network'} />
</Button> </Button>
<MetaPreviewButton <Button className={classnames(styles['control-bar-button'], { 'disabled': typeof info !== 'object' || info === null })} tabIndex={-1} onMouseDown={onInfoButtonMouseDown} onClick={onInfoButtonClick}>
className={styles['control-bar-button']} <Icon className={styles['icon']} icon={'ic_info'} />
metaResource={metaResource}
onToggleMetaPreview={onToggleMetaPreview}
/>
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
<Icon className={'icon'} icon={'ic_cast'} />
</Button> </Button>
<SubtitlesButton
className={styles['control-bar-button']}
subtitlesTracks={subtitlesTracks}
onToggleSubtitlesPicker={onToggleSubtitlesPicker}
/>
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}> <Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
<Icon className={'icon'} icon={'ic_videos'} /> <Icon className={styles['icon']} icon={'ic_cast'} />
</Button>
<Button className={classnames(styles['control-bar-button'], { 'disabled': !Array.isArray(subtitlesTracks) || subtitlesTracks.length === 0 })} tabIndex={-1} onMouseDown={onSubtitlesButtonMouseDown} onClick={onSubtitlesButtonClick}>
<Icon className={styles['icon']} icon={'ic_sub'} />
</Button>
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
<Icon className={styles['icon']} icon={'ic_videos'} />
</Button> </Button>
</div> </div>
</div> </div>
@ -84,21 +116,21 @@ const ControlBar = ({
ControlBar.propTypes = { ControlBar.propTypes = {
className: PropTypes.string, className: PropTypes.string,
paused: PropTypes.any, paused: PropTypes.bool,
time: PropTypes.any, time: PropTypes.number,
duration: PropTypes.any, duration: PropTypes.number,
volume: PropTypes.any, volume: PropTypes.number,
muted: PropTypes.any, muted: PropTypes.bool,
subtitlesTracks: PropTypes.any, subtitlesTracks: PropTypes.array,
metaResource: PropTypes.any, info: PropTypes.object,
onPlayRequested: PropTypes.any, onPlayRequested: PropTypes.func,
onPauseRequested: PropTypes.any, onPauseRequested: PropTypes.func,
onMuteRequested: PropTypes.any, onMuteRequested: PropTypes.func,
onUnmuteRequested: PropTypes.any, onUnmuteRequested: PropTypes.func,
onVolumeChangeRequested: PropTypes.any, onVolumeChangeRequested: PropTypes.func,
onSeekRequested: PropTypes.any, onSeekRequested: PropTypes.func,
onToggleSubtitlesPicker: PropTypes.any, onToggleSubtitlesMenu: PropTypes.func,
onToggleMetaPreview: PropTypes.any onToggleInfoMenu: PropTypes.func
}; };
module.exports = ControlBar; module.exports = ControlBar;

View file

@ -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 (
<Button className={classnames(className, { 'disabled': metaResource === null || metaResource.content.type !== 'Ready' })} tabIndex={-1} onMouseDown={onMouseDown} onClick={onClick}>
<Icon className={'icon'} icon={'ic_info'} />
</Button>
);
};
MetaPreviewButton.propTypes = {
className: PropTypes.string,
metaResource: PropTypes.object,
onToggleMetaPreview: PropTypes.func
};
module.exports = MetaPreviewButton;

View file

@ -1,3 +0,0 @@
const MetaPreviewButton = require('./MetaPreviewButton');
module.exports = MetaPreviewButton;

View file

@ -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 (
<Button className={classnames(className, { 'disabled': typeof muted !== 'boolean' })} title={muted ? 'Unmute' : 'Mute'} tabIndex={-1} onClick={toggleMuted}>
<Icon className={'icon'} icon={icon} />
</Button>
);
};
MuteButton.propTypes = {
className: PropTypes.string,
muted: PropTypes.bool,
volume: PropTypes.number,
onMuteRequested: PropTypes.func,
onUnmuteRequested: PropTypes.func
};
module.exports = MuteButton;

View file

@ -1,3 +0,0 @@
const MuteButton = require('./MuteButton');
module.exports = MuteButton;

View file

@ -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 (
<Button className={classnames(className, { 'disabled': typeof paused !== 'boolean' })} title={paused ? 'Play' : 'Pause'} tabIndex={-1} onClick={togglePaused}>
<Icon
className={'icon'}
icon={typeof paused !== 'boolean' || paused ? 'ic_play' : 'ic_pause'}
/>
</Button>
);
};
PlayPauseButton.propTypes = {
className: PropTypes.string,
paused: PropTypes.bool,
onPlayRequested: PropTypes.func,
onPauseRequested: PropTypes.func
};
module.exports = PlayPauseButton;

View file

@ -1,3 +0,0 @@
const PlayPauseButton = require('./PlayPauseButton');
module.exports = PlayPauseButton;

View file

@ -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 (
<Button className={classnames(className, { 'disabled': !Array.isArray(subtitlesTracks) || subtitlesTracks.length === 0 })} tabIndex={-1} onMouseDown={onMouseDown} onClick={onClick}>
<Icon className={'icon'} icon={'ic_sub'} />
</Button>
);
};
SubtitlesButton.propTypes = {
className: PropTypes.string,
subtitlesTracks: PropTypes.array,
onToggleSubtitlesPicker: PropTypes.func
};
module.exports = SubtitlesButton;

View file

@ -1,3 +0,0 @@
const SubtitlesButton = require('./SubtitlesButton');
module.exports = SubtitlesButton;

View file

@ -4,6 +4,9 @@
padding: 0 1.5rem; padding: 0 1.5rem;
.seek-bar { .seek-bar {
--track-size: 0.5rem;
--thumb-size: 1.5rem;
height: 2.5rem; height: 2.5rem;
} }
@ -21,12 +24,12 @@
align-items: center; align-items: center;
&:global(.disabled) { &:global(.disabled) {
:global(.icon) { .icon {
fill: @color-surface; fill: @color-surface;
} }
} }
:global(.icon) { .icon {
flex: none; flex: none;
width: 3rem; width: 3rem;
height: 2rem; height: 2rem;

View file

@ -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 (
<div className={classnames(className, styles['info-menu-container'])} onMouseDown={onMouseDown}>
<MetaPreview
className={styles['meta-preview']}
compact={true}
name={metaItem.name}
logo={metaItem.logo}
runtime={metaItem.runtime}
releaseInfo={metaItem.releaseInfo}
released={metaItem.released}
description={metaItem.description}
links={metaItem.links}
/>
</div>
);
};
InfoMenu.propTypes = {
className: PropTypes.string,
metaItem: PropTypes.object
};
module.exports = InfoMenu;

View file

@ -0,0 +1,3 @@
const InfoMenu = require('./InfoMenu');
module.exports = InfoMenu;

View file

@ -0,0 +1,3 @@
.info-menu-container {
width: 30rem;
}

View file

@ -4,26 +4,30 @@ const classnames = require('classnames');
const debounce = require('lodash.debounce'); const debounce = require('lodash.debounce');
const { useRouteFocused } = require('stremio-router'); const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services'); 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 BufferingLoader = require('./BufferingLoader');
const ControlBar = require('./ControlBar'); const ControlBar = require('./ControlBar');
const SubtitlesPicker = require('./SubtitlesPicker'); const InfoMenu = require('./InfoMenu');
const SubtitlesMenu = require('./SubtitlesMenu');
const Video = require('./Video'); const Video = require('./Video');
const useInfo = require('./useInfo');
const usePlayer = require('./usePlayer'); const usePlayer = require('./usePlayer');
const useSettings = require('./useSettings'); const useSettings = require('./useSettings');
const styles = require('./styles'); const styles = require('./styles');
const Player = ({ urlParams }) => { const Player = ({ urlParams }) => {
const { core } = useServices(); const { core } = useServices();
const player = usePlayer(urlParams); const profile = useProfile();
const [settings, updateSettings] = useSettings(); const [player, updateLibraryItemState, pushToLibrary] = usePlayer(urlParams);
const [settings, updateSettings] = useSettings(profile);
const info = useInfo(player, profile);
const routeFocused = useRouteFocused(); const routeFocused = useRouteFocused();
const toast = useToast(); const toast = useToast();
const [, , , toggleFullscreen] = useFullscreen(); const [, , , toggleFullscreen] = useFullscreen();
const [immersed, setImmersed] = React.useState(true); const [immersed, setImmersed] = React.useState(true);
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []); const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
const [subtitlesPickerOpen, , closeSubtitlesPicker, toggleSubtitlesPicker] = useBinaryState(false); const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
const [metaPreviewOpen, , closeMetaPreview, toggleMetaPreview] = useBinaryState(false); const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
const [error, setError] = React.useState(null); const [error, setError] = React.useState(null);
const [videoState, setVideoState] = React.useReducer( const [videoState, setVideoState] = React.useReducer(
(videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }), (videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }),
@ -63,7 +67,7 @@ const Player = ({ urlParams }) => {
const onPropChanged = React.useCallback((propName, propValue) => { const onPropChanged = React.useCallback((propName, propValue) => {
setVideoState({ [propName]: propValue }); setVideoState({ [propName]: propValue });
}, []); }, []);
const onEnded = useDeepEqualMemo(() => () => { const onEnded = React.useCallback(() => {
core.dispatch({ action: 'Unload' }, 'player'); core.dispatch({ action: 'Unload' }, 'player');
if (player.lib_item !== null) { if (player.lib_item !== null) {
core.dispatch({ core.dispatch({
@ -78,7 +82,7 @@ const Player = ({ urlParams }) => {
// TODO go to next video // TODO go to next video
} }
window.history.back(); window.history.back();
}, [player.next_video, player.lib_item]); }, [player]);
const onError = React.useCallback((error) => { const onError = React.useCallback((error) => {
if (error.critical) { if (error.critical) {
setError(error); setError(error);
@ -102,9 +106,11 @@ const Player = ({ urlParams }) => {
const onPlayRequested = React.useCallback(() => { const onPlayRequested = React.useCallback(() => {
dispatch({ propName: 'paused', propValue: false }); dispatch({ propName: 'paused', propValue: false });
}, []); }, []);
const onPlayRequestedDebounced = React.useCallback(debounce(onPlayRequested, 200), []);
const onPauseRequested = React.useCallback(() => { const onPauseRequested = React.useCallback(() => {
dispatch({ propName: 'paused', propValue: true }); dispatch({ propName: 'paused', propValue: true });
}, []); }, []);
const onPauseRequestedDebounced = React.useCallback(debounce(onPauseRequested, 200), []);
const onMuteRequested = React.useCallback(() => { const onMuteRequested = React.useCallback(() => {
dispatch({ propName: 'muted', propValue: true }); dispatch({ propName: 'muted', propValue: true });
}, []); }, []);
@ -132,18 +138,23 @@ const Player = ({ urlParams }) => {
const onVideoClick = React.useCallback(() => { const onVideoClick = React.useCallback(() => {
if (videoState.paused !== null) { if (videoState.paused !== null) {
if (videoState.paused) { if (videoState.paused) {
onPlayRequested(); onPlayRequestedDebounced();
} else { } else {
onPauseRequested(); onPauseRequestedDebounced();
} }
} }
}, [videoState.paused]); }, [videoState.paused]);
const onVideoDoubleClick = React.useCallback(() => {
onPlayRequestedDebounced.cancel();
onPauseRequestedDebounced.cancel();
toggleFullscreen();
}, [toggleFullscreen]);
const onContainerMouseDown = React.useCallback((event) => { const onContainerMouseDown = React.useCallback((event) => {
if (!event.nativeEvent.subtitlesPickerClosePrevented) { if (!event.nativeEvent.subtitlesMenuClosePrevented) {
closeSubtitlesPicker(); closeSubtitlesMenu();
} }
if (!event.nativeEvent.metaPreviewClosePrevented) { if (!event.nativeEvent.infoMenuClosePrevented) {
closeMetaPreview(); closeInfoMenu();
} }
}, []); }, []);
const onContainerMouseMove = React.useCallback((event) => { const onContainerMouseMove = React.useCallback((event) => {
@ -181,7 +192,14 @@ const Player = ({ urlParams }) => {
dispatch({ dispatch({
commandName: 'addSubtitlesTracks', commandName: 'addSubtitlesTracks',
commandArgs: { 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 tracks: player.subtitles_resources
.filter((subtitles_resource) => subtitles_resource.content.type === 'Ready') .filter((subtitles_resource) => subtitles_resource.content.type === 'Ready')
.reduce((tracks, subtitles_resource) => { .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]); }, [settings.subtitles_offset]);
React.useEffect(() => { React.useEffect(() => {
if (videoState.time !== null && !isNaN(videoState.time) && videoState.duration !== null && !isNaN(videoState.duration)) { if (videoState.time !== null && !isNaN(videoState.time) && videoState.duration !== null && !isNaN(videoState.duration)) {
core.dispatch({ updateLibraryItemState(videoState.time, videoState.duration);
action: 'Player',
args: {
action: 'UpdateLibraryItemState',
args: {
time: videoState.time,
duration: videoState.duration
}
}
}, 'player');
} }
}, [videoState.time, videoState.duration]); }, [videoState.time, videoState.duration]);
React.useEffect(() => { React.useEffect(() => {
const interval = setInterval(() => { if (!Array.isArray(videoState.subtitlesTracks) || videoState.subtitlesTracks.length === 0) {
core.dispatch({ closeSubtitlesMenu();
action: 'Player', }
args: { }, [videoState.subtitlesTracks]);
action: 'PushToLibrary' React.useEffect(() => {
} if (info === null) {
}, 'player'); closeInfoMenu();
}, 30000); }
}, [info]);
React.useEffect(() => {
const intervalId = setInterval(pushToLibrary, 30000);
return () => { return () => {
clearInterval(interval); clearInterval(intervalId);
}; };
}, []); }, []);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
const onKeyDown = (event) => { const onKeyDown = (event) => {
switch (event.code) { switch (event.code) {
case 'Space': { case 'Space': {
if (!subtitlesPickerOpen && !metaPreviewOpen && videoState.paused !== null) { if (!subtitlesMenuOpen && !infoMenuOpen && videoState.paused !== null) {
if (videoState.paused) { if (videoState.paused) {
onPlayRequested(); onPlayRequested();
} else { } else {
@ -255,46 +271,52 @@ const Player = ({ urlParams }) => {
break; break;
} }
case 'ArrowRight': { case 'ArrowRight': {
if (!subtitlesPickerOpen && !metaPreviewOpen && videoState.time !== null) { if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) {
onSeekRequested(videoState.time + 15000); onSeekRequested(videoState.time + 15000);
} }
break; break;
} }
case 'ArrowLeft': { case 'ArrowLeft': {
if (!subtitlesPickerOpen && !metaPreviewOpen && videoState.time !== null) { if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) {
onSeekRequested(videoState.time - 15000); onSeekRequested(videoState.time - 15000);
} }
break; break;
} }
case 'ArrowUp': { case 'ArrowUp': {
if (!subtitlesPickerOpen && !metaPreviewOpen && videoState.volume !== null) { if (!subtitlesMenuOpen && !infoMenuOpen && videoState.volume !== null) {
onVolumeChangeRequested(videoState.volume + 5); onVolumeChangeRequested(videoState.volume + 5);
} }
break; break;
} }
case 'ArrowDown': { case 'ArrowDown': {
if (!subtitlesPickerOpen && !metaPreviewOpen && videoState.volume !== null) { if (!subtitlesMenuOpen && !infoMenuOpen && videoState.volume !== null) {
onVolumeChangeRequested(videoState.volume - 5); onVolumeChangeRequested(videoState.volume - 5);
} }
break; break;
} }
case 'KeyS': { case 'KeyS': {
closeMetaPreview(); closeInfoMenu();
toggleSubtitlesPicker(); if (Array.isArray(videoState.subtitlesTracks) && videoState.subtitlesTracks.length > 0) {
toggleSubtitlesMenu();
}
break; break;
} }
case 'KeyM': { case 'KeyM': {
closeSubtitlesPicker(); closeSubtitlesMenu();
toggleMetaPreview(); if (info !== null) {
toggleInfoMenu();
}
break; break;
} }
case 'Escape': { case 'Escape': {
closeSubtitlesPicker(); closeSubtitlesMenu();
closeMetaPreview(); closeInfoMenu();
break; break;
} }
} }
@ -305,14 +327,16 @@ const Player = ({ urlParams }) => {
return () => { return () => {
window.removeEventListener('keydown', onKeyDown); 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(() => { React.useLayoutEffect(() => {
return () => { return () => {
setImmersedDebounced.cancel(); setImmersedDebounced.cancel();
onPlayRequestedDebounced.cancel();
onPauseRequestedDebounced.cancel();
}; };
}, []); }, []);
return ( return (
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && videoState.paused !== null && !videoState.paused && !subtitlesPickerOpen && !metaPreviewOpen })} <div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen })}
onMouseDown={onContainerMouseDown} onMouseDown={onContainerMouseDown}
onMouseMove={onContainerMouseMove} onMouseMove={onContainerMouseMove}
onMouseOver={onContainerMouseMove} onMouseOver={onContainerMouseMove}
@ -341,17 +365,17 @@ const Player = ({ urlParams }) => {
<div <div
className={styles['layer']} className={styles['layer']}
onClick={onVideoClick} onClick={onVideoClick}
onDoubleClick={toggleFullscreen} onDoubleClick={onVideoDoubleClick}
/> />
{
subtitlesMenuOpen || infoMenuOpen ?
<div className={styles['layer']} />
:
null
}
<HorizontalNavBar <HorizontalNavBar
className={classnames(styles['layer'], styles['nav-bar-layer'])} className={classnames(styles['layer'], styles['nav-bar-layer'])}
title={ title={info !== null ? info.title : ''}
// TODO consider use video.title and fallback to stream.title
player.meta_resource !== null && player.meta_resource.content.type === 'Ready' ?
player.meta_resource.content.content.name
:
null
}
backButton={true} backButton={true}
fullscreenButton={true} fullscreenButton={true}
onMouseMove={onBarMouseMove} onMouseMove={onBarMouseMove}
@ -365,21 +389,21 @@ const Player = ({ urlParams }) => {
volume={videoState.volume} volume={videoState.volume}
muted={videoState.muted} muted={videoState.muted}
subtitlesTracks={videoState.subtitlesTracks} subtitlesTracks={videoState.subtitlesTracks}
metaResource={null} info={info}
onPlayRequested={onPlayRequested} onPlayRequested={onPlayRequested}
onPauseRequested={onPauseRequested} onPauseRequested={onPauseRequested}
onMuteRequested={onMuteRequested} onMuteRequested={onMuteRequested}
onUnmuteRequested={onUnmuteRequested} onUnmuteRequested={onUnmuteRequested}
onVolumeChangeRequested={onVolumeChangeRequested} onVolumeChangeRequested={onVolumeChangeRequested}
onSeekRequested={onSeekRequested} onSeekRequested={onSeekRequested}
onToggleSubtitlesPicker={toggleSubtitlesPicker} onToggleSubtitlesMenu={toggleSubtitlesMenu}
onToggleMetaPreview={toggleMetaPreview} onToggleInfoMenu={toggleInfoMenu}
onMouseMove={onBarMouseMove} onMouseMove={onBarMouseMove}
onMouseOver={onBarMouseMove} onMouseOver={onBarMouseMove}
/> />
{ {
subtitlesPickerOpen ? subtitlesMenuOpen ?
<SubtitlesPicker <SubtitlesMenu
className={classnames(styles['layer'], styles['menu-layer'])} className={classnames(styles['layer'], styles['menu-layer'])}
tracks={videoState.subtitlesTracks} tracks={videoState.subtitlesTracks}
selectedTrackId={videoState.selectedSubtitlesTrackId} selectedTrackId={videoState.selectedSubtitlesTrackId}
@ -397,14 +421,17 @@ const Player = ({ urlParams }) => {
: :
null null
} }
{/* { {
metaPreviewOpen ? infoMenuOpen ?
<div className={classnames(styles['layer'], styles['menu-layer'])} onMouseDown={(event) => event.nativeEvent.metaPreviewClosePrevented = true}> <InfoMenu
<div style={{ width: 300, height: 800, background: 'red' }} /> className={classnames(styles['layer'], styles['menu-layer'])}
</div> stream={info !== null ? info.stream : null}
addon={info !== null ? info.addon : null}
metaItem={info !== null ? info.metaItem : null}
/>
: :
null null
} */} }
</div> </div>
); );
}; };

View file

@ -34,7 +34,7 @@
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
fill: @color-surface-light5; fill: @color-surface-light5-90;
} }
} }

View file

@ -6,14 +6,14 @@ const DiscreteSelectInput = require('./DiscreteSelectInput');
const styles = require('./styles'); const styles = require('./styles');
const ORIGIN_PRIORITIES = { const ORIGIN_PRIORITIES = {
'EMBEDDED': 1, 'EMBEDDED IN VIDEO': 1,
'Stream': 2 'EMBEDDED IN STREAM': 2
}; };
const LANGUAGE_PRIORITIES = { const LANGUAGE_PRIORITIES = {
'eng': 1 'eng': 1
}; };
const SubtitlesPicker = (props) => { const SubtitlesMenu = (props) => {
const languages = React.useMemo(() => { const languages = React.useMemo(() => {
return Array.isArray(props.tracks) ? return Array.isArray(props.tracks) ?
props.tracks props.tracks
@ -51,7 +51,7 @@ const SubtitlesPicker = (props) => {
[]; [];
}, [props.tracks, selectedLanguage]); }, [props.tracks, selectedLanguage]);
const onMouseDown = React.useCallback((event) => { const onMouseDown = React.useCallback((event) => {
event.nativeEvent.subtitlesPickerClosePrevented = true; event.nativeEvent.subtitlesMenuClosePrevented = true;
}, []); }, []);
const languageOnClick = React.useCallback((event) => { const languageOnClick = React.useCallback((event) => {
const trackId = Array.isArray(props.tracks) ? const trackId = Array.isArray(props.tracks) ?
@ -101,7 +101,7 @@ const SubtitlesPicker = (props) => {
} }
}, [props.offset, props.onOffsetChanged]); }, [props.offset, props.onOffsetChanged]);
return ( return (
<div className={classnames(props.className, styles['subtitles-picker-container'])} onMouseDown={onMouseDown}> <div className={classnames(props.className, styles['subtitles-menu-container'])} onMouseDown={onMouseDown}>
<div className={styles['languages-container']}> <div className={styles['languages-container']}>
<div className={styles['languages-header']}>Languages</div> <div className={styles['languages-header']}>Languages</div>
<div className={styles['languages-list']}> <div className={styles['languages-list']}>
@ -182,7 +182,7 @@ const SubtitlesPicker = (props) => {
); );
}; };
SubtitlesPicker.propTypes = { SubtitlesMenu.propTypes = {
className: PropTypes.string, className: PropTypes.string,
tracks: PropTypes.arrayOf(PropTypes.shape({ tracks: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
@ -202,4 +202,4 @@ SubtitlesPicker.propTypes = {
onOffsetChanged: PropTypes.func onOffsetChanged: PropTypes.func
}; };
module.exports = SubtitlesPicker; module.exports = SubtitlesMenu;

View file

@ -0,0 +1,3 @@
const SubtitlesMenu = require('./SubtitlesMenu');
module.exports = SubtitlesMenu;

View file

@ -1,10 +1,9 @@
@import (reference) '~stremio-colors/dist/less/stremio-colors.less'; @import (reference) '~stremio-colors/dist/less/stremio-colors.less';
.subtitles-picker-container { .subtitles-menu-container {
height: 23rem; height: 23rem;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
background-color: @color-background-dark1;
.languages-container, .variants-container, .subtitles-settings-container { .languages-container, .variants-container, .subtitles-settings-container {
flex: none; flex: none;
@ -49,7 +48,7 @@
height: 0.5rem; height: 0.5rem;
border-radius: 100%; border-radius: 100%;
margin-left: 1rem; margin-left: 1rem;
background-color: @color-accent3; background-color: @color-accent3-90;
} }
} }
} }
@ -100,7 +99,7 @@
} }
&:global(.disabled) { &:global(.disabled) {
color: @color-surface; color: @color-surface-90;
} }
} }
} }

View file

@ -1,3 +0,0 @@
const SubtitlesPicker = require('./SubtitlesPicker');
module.exports = SubtitlesPicker;

View file

@ -26,7 +26,7 @@ html:not(.active-slider-within) {
z-index: 0; z-index: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: @color-background-dark2; background-color: @color-background-dark5;
.layer { .layer {
position: absolute; position: absolute;
@ -104,6 +104,7 @@ html:not(.active-slider-within) {
bottom: 8rem; bottom: 8rem;
max-height: calc(100% - 13.5rem); max-height: calc(100% - 13.5rem);
max-width: calc(100% - 4rem); max-width: calc(100% - 4rem);
background-color: @color-background-dark1;
box-shadow: 0 1.35rem 2.7rem @color-background-dark5-40, box-shadow: 0 1.35rem 2.7rem @color-background-dark5-40,
0 1.1rem 0.85rem @color-background-dark5-20; 0 1.1rem 0.85rem @color-background-dark5-20;
overflow: auto; overflow: auto;

View file

@ -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;

View file

@ -1,5 +1,6 @@
const React = require('react'); const React = require('react');
const pako = require('pako'); const pako = require('pako');
const { useServices } = require('stremio/services');
const { useModelState } = require('stremio/common'); const { useModelState } = require('stremio/common');
const initPlayerState = () => ({ const initPlayerState = () => ({
@ -15,23 +16,13 @@ const mapPlayerStateWithCtx = (player, ctx) => {
{ {
stream: { stream: {
...player.selected.stream, ...player.selected.stream,
subtitles: Array.isArray(player.selected.stream.subtitles) ? addon: ctx.profile.addons.reduce((result, addon) => {
player.selected.stream.subtitles.map(({ url, lang }) => ({ if (player.selected.stream_resource_request !== null && addon.transportUrl === player.selected.stream_resource_request.base) {
url, return addon;
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;
}
return origin; return result;
}, player.selected.stream_resource_request !== null ? player.selected.stream_resource_request.base : 'Stream') }, null)
}))
:
[]
}, },
stream_resource_request: player.selected.stream_resource_request, stream_resource_request: player.selected.stream_resource_request,
meta_resource_request: player.selected.meta_resource_request, meta_resource_request: player.selected.meta_resource_request,
@ -40,48 +31,54 @@ const mapPlayerStateWithCtx = (player, ctx) => {
} }
: :
null; 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 subtitles_resources = player.subtitles_resources.map((subtitles_resource) => {
const request = subtitles_resource.request; 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) { if (addon.transportUrl === subtitles_resource.request.base) {
return typeof addon.manifest.name === 'string' && addon.manifest.name.length > 0 ? return addon;
addon.manifest.name
:
addon.manifest.id;
} }
return origin; return result;
}, subtitles_resource.request.base); }, null);
const content = subtitles_resource.content.type === 'Ready' ? const content = subtitles_resource.content;
{ return { request, addon, content };
type: 'Ready',
content: subtitles_resource.content.content.map(({ url, lang }) => ({
url,
lang,
origin
}))
}
:
subtitles_resource.content;
return {
request,
origin,
content
};
}); });
const next_video = player.next_video; const next_video = player.next_video;
const lib_item = player.lib_item; const lib_item = player.lib_item;
return { return { selected, meta_resource, subtitles_resources, next_video, lib_item };
selected,
meta_resource,
subtitles_resources,
next_video,
lib_item
};
}; };
const usePlayer = (urlParams) => { const usePlayer = (urlParams) => {
const { core } = useServices();
const loadPlayerAction = React.useMemo(() => { const loadPlayerAction = React.useMemo(() => {
try { try {
return { return {
@ -123,7 +120,10 @@ const usePlayer = (urlParams) => {
} }
: :
null, null,
video_id: urlParams.videoId video_id: typeof urlParams.videoId === 'string' ?
urlParams.videoId
:
null
} }
} }
}; };
@ -133,12 +133,30 @@ const usePlayer = (urlParams) => {
}; };
} }
}, [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', model: 'player',
action: loadPlayerAction, action: loadPlayerAction,
init: initPlayerState, init: initPlayerState,
mapWithCtx: mapPlayerStateWithCtx mapWithCtx: mapPlayerStateWithCtx
}); });
return [player, updateLibraryItemState, pushToLibrary];
}; };
module.exports = usePlayer; module.exports = usePlayer;

View file

@ -1,10 +1,8 @@
const React = require('react'); const React = require('react');
const { useProfile } = require('stremio/common');
const { useServices } = require('stremio/services'); const { useServices } = require('stremio/services');
const useSettings = () => { const useSettings = (profile) => {
const { core } = useServices(); const { core } = useServices();
const profile = useProfile();
const updateSettings = React.useCallback((settings) => { const updateSettings = React.useCallback((settings) => {
core.dispatch({ core.dispatch({
action: 'Ctx', action: 'Ctx',
@ -16,7 +14,7 @@ const useSettings = () => {
} }
} }
}); });
}, [profile.settings]); }, [profile]);
return [profile.settings, updateSettings]; return [profile.settings, updateSettings];
}; };

View file

@ -55,7 +55,7 @@ function HTMLSubtitles(options) {
track.url.length > 0 && track.url.length > 0 &&
typeof track.origin === 'string' && typeof track.origin === 'string' &&
track.origin.length > 0 && track.origin.length > 0 &&
track.origin !== 'EMBEDDED'; track.origin !== 'EMBEDDED IN VIDEO';
}) })
.map(function(track) { .map(function(track) {
return Object.freeze(Object.assign({}, track, { return Object.freeze(Object.assign({}, track, {

View file

@ -84,7 +84,7 @@ function YouTubeVideo(options) {
.map(function(track) { .map(function(track) {
return Object.freeze({ return Object.freeze({
id: track.languageCode, id: track.languageCode,
origin: 'EMBEDDED', origin: 'EMBEDDED IN VIDEO',
label: track.languageName label: track.languageName
}); });
}); });
@ -482,7 +482,7 @@ function YouTubeVideo(options) {
embeddedSubtitlesSelectedTrackId = null; embeddedSubtitlesSelectedTrackId = null;
var tracks = getSubtitlesTracks(); var tracks = getSubtitlesTracks();
for (var i = 0; i < tracks.length; i++) { 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; embeddedSubtitlesSelectedTrackId = tracks[i].id;
break; break;
} }