mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 17:15:48 +00:00
commit
53c8e83426
21 changed files with 291 additions and 160 deletions
|
|
@ -9,22 +9,20 @@ const OPTIONS = [
|
|||
{ label: 'Dismiss', value: 'dismiss' }
|
||||
];
|
||||
|
||||
const LibItem = ({ id, videoId, ...props }) => {
|
||||
const LibItem = ({ id, ...props }) => {
|
||||
const { core } = useServices();
|
||||
const options = React.useMemo(() => {
|
||||
return OPTIONS.filter(({ value }) => {
|
||||
return value !== 'dismiss' || (props.progress !== null && !isNaN(props.progress));
|
||||
switch (value) {
|
||||
case 'play':
|
||||
return props.deepLinks && typeof props.deepLinks.player === 'string';
|
||||
case 'details':
|
||||
return props.deepLinks && (typeof props.deepLinks.meta_details_videos === 'string' || typeof props.deepLinks.meta_details_streams === 'string');
|
||||
case 'dismiss':
|
||||
return typeof id === 'string' && props.progress !== null && !isNaN(props.progress);
|
||||
}
|
||||
});
|
||||
}, [props.progress]);
|
||||
const playIcon = React.useMemo(() => {
|
||||
return props.progress !== null && !isNaN(props.progress);
|
||||
}, [props.progress]);
|
||||
const dataset = React.useMemo(() => ({
|
||||
id,
|
||||
videoId,
|
||||
type: props.type,
|
||||
...props.dataset
|
||||
}), [id, videoId, props.type, props.dataset]);
|
||||
}, [id, props.progress, props.deepLinks]);
|
||||
const optionOnSelect = React.useCallback((event) => {
|
||||
if (typeof props.optionOnSelect === 'function') {
|
||||
props.optionOnSelect(event);
|
||||
|
|
@ -33,32 +31,30 @@ const LibItem = ({ id, videoId, ...props }) => {
|
|||
if (!event.nativeEvent.optionSelectPrevented) {
|
||||
switch (event.value) {
|
||||
case 'play': {
|
||||
if (typeof event.dataset.id === 'string' && typeof event.dataset.type === 'string') {
|
||||
// TODO check streams storage
|
||||
// TODO check behaviour_hints
|
||||
// TODO add videos page to the history stack if needed
|
||||
window.location = `#/metadetails/${encodeURIComponent(event.dataset.type)}/${encodeURIComponent(event.dataset.id)}${typeof event.dataset.videoId === 'string' ? `/${encodeURIComponent(event.dataset.videoId)}` : ''}`;
|
||||
if (props.deepLinks && typeof props.deepLinks.player === 'string') {
|
||||
window.location = props.deepLinks.player;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'details': {
|
||||
if (typeof event.dataset.id === 'string' && typeof event.dataset.type === 'string') {
|
||||
// TODO check streams storage
|
||||
// TODO check behaviour_hints
|
||||
// TODO add videos page to the history stack if needed
|
||||
window.location = `#/metadetails/${encodeURIComponent(event.dataset.type)}/${encodeURIComponent(event.dataset.id)}${typeof event.dataset.videoId === 'string' ? `/${encodeURIComponent(event.dataset.videoId)}` : ''}`;
|
||||
if (props.deepLinks) {
|
||||
if (typeof props.deepLinks.meta_details_videos === 'string') {
|
||||
window.location = props.deepLinks.meta_details_videos;
|
||||
} else if (typeof props.deepLinks.meta_details_streams === 'string') {
|
||||
window.location = props.deepLinks.meta_details_streams;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'dismiss': {
|
||||
if (typeof event.dataset.id === 'string') {
|
||||
if (typeof id === 'string') {
|
||||
core.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'RewindLibraryItem',
|
||||
args: event.dataset.id
|
||||
args: id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -67,13 +63,12 @@ const LibItem = ({ id, videoId, ...props }) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
}, [props.optionOnSelect]);
|
||||
}, [id, props.deepLinks, props.optionOnSelect]);
|
||||
return (
|
||||
<MetaItem
|
||||
options={options}
|
||||
playIcon={playIcon}
|
||||
playIcon={props.progress !== null && !isNaN(props.progress)}
|
||||
{...props}
|
||||
dataset={dataset}
|
||||
options={options}
|
||||
optionOnSelect={optionOnSelect}
|
||||
/>
|
||||
);
|
||||
|
|
@ -81,10 +76,12 @@ const LibItem = ({ id, videoId, ...props }) => {
|
|||
|
||||
LibItem.propTypes = {
|
||||
id: PropTypes.string,
|
||||
videoId: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
progress: PropTypes.number,
|
||||
dataset: PropTypes.object,
|
||||
deepLinks: PropTypes.shape({
|
||||
meta_details_videos: PropTypes.string,
|
||||
meta_details_streams: PropTypes.string,
|
||||
player: PropTypes.string
|
||||
}),
|
||||
optionOnSelect: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -23,8 +23,23 @@ const ICON_FOR_TYPE = new Map([
|
|||
['other', 'ic_movies'],
|
||||
]);
|
||||
|
||||
const MetaItem = React.memo(({ className, type, name, poster, posterShape, playIcon, progress, options, dataset, optionOnSelect, ...props }) => {
|
||||
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(() => {
|
||||
return deepLinks ?
|
||||
typeof deepLinks.player === 'string' ?
|
||||
deepLinks.player
|
||||
:
|
||||
typeof deepLinks.meta_details_streams === 'string' ?
|
||||
deepLinks.meta_details_streams
|
||||
:
|
||||
typeof deepLinks.meta_details_videos === 'string' ?
|
||||
deepLinks.meta_details_videos
|
||||
:
|
||||
null
|
||||
:
|
||||
null;
|
||||
}, [deepLinks]);
|
||||
const metaItemOnClick = React.useCallback((event) => {
|
||||
if (typeof props.onClick === 'function') {
|
||||
props.onClick(event);
|
||||
|
|
@ -58,7 +73,7 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, playI
|
|||
<Icon className={styles['icon']} icon={'ic_more'} />
|
||||
), []);
|
||||
return (
|
||||
<Button title={name} {...props} className={classnames(className, styles['meta-item-container'], styles['poster-shape-poster'], styles[`poster-shape-${posterShape}`], { 'active': menuOpen })} onClick={metaItemOnClick}>
|
||||
<Button title={name} href={href} {...props} className={classnames(className, styles['meta-item-container'], styles['poster-shape-poster'], styles[`poster-shape-${posterShape}`], { 'active': menuOpen })} onClick={metaItemOnClick}>
|
||||
<div className={styles['poster-container']}>
|
||||
<div className={styles['poster-image-layer']}>
|
||||
<Image
|
||||
|
|
@ -125,6 +140,11 @@ MetaItem.propTypes = {
|
|||
playIcon: PropTypes.bool,
|
||||
progress: PropTypes.number,
|
||||
options: PropTypes.array,
|
||||
deepLinks: PropTypes.shape({
|
||||
meta_details_videos: PropTypes.string,
|
||||
meta_details_streams: PropTypes.string,
|
||||
player: PropTypes.string
|
||||
}),
|
||||
dataset: PropTypes.object,
|
||||
optionOnSelect: PropTypes.func,
|
||||
onClick: PropTypes.func
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ const React = require('react');
|
|||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const UrlUtils = require('url');
|
||||
const pako = require('pako');
|
||||
const Icon = require('stremio-icons/dom');
|
||||
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 deepLinking = require('stremio/common/deepLinking');
|
||||
const routesRegexp = require('stremio/common/routesRegexp');
|
||||
const useBinaryState = require('stremio/common/useBinaryState');
|
||||
const ActionButton = require('./ActionButton');
|
||||
|
|
@ -64,10 +64,12 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
|
|||
[];
|
||||
}, [links]);
|
||||
const trailerHref = React.useMemo(() => {
|
||||
return typeof trailer === 'object' && trailer !== null ?
|
||||
`#/player/${btoa(pako.deflate(JSON.stringify(trailer), { to: 'string' }))}`
|
||||
:
|
||||
null;
|
||||
if (typeof trailer !== 'object' || trailer === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deepLinks = deepLinking.withStream({ stream: trailer });
|
||||
return deepLinks.player;
|
||||
}, [trailer]);
|
||||
const renderLogoFallback = React.useMemo(() => () => (
|
||||
<Icon className={styles['logo-placeholder-icon']} icon={'ic_broken_link'} />
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ const CONSTANTS = require('stremio/common/CONSTANTS');
|
|||
const MetaRowPlaceholder = require('./MetaRowPlaceholder');
|
||||
const styles = require('./styles');
|
||||
|
||||
const MetaRow = ({ className, title, message, items, itemComponent, href }) => {
|
||||
const MetaRow = ({ className, title, message, items, itemComponent, deepLinks }) => {
|
||||
return (
|
||||
<div className={classnames(className, styles['meta-row-container'])}>
|
||||
{
|
||||
(typeof title === 'string' && title.length > 0) || (typeof href === 'string' && href.length > 0) ?
|
||||
(typeof title === 'string' && title.length > 0) || (deepLinks && typeof deepLinks.discover === 'string') ?
|
||||
<div className={styles['header-container']}>
|
||||
{
|
||||
typeof title === 'string' && title.length > 0 ?
|
||||
|
|
@ -21,8 +21,8 @@ const MetaRow = ({ className, title, message, items, itemComponent, href }) => {
|
|||
null
|
||||
}
|
||||
{
|
||||
typeof href === 'string' && href.length > 0 ?
|
||||
<Button className={styles['see-all-container']} title={'SEE ALL'} href={href}>
|
||||
deepLinks && typeof deepLinks.discover === 'string' ?
|
||||
<Button className={styles['see-all-container']} title={'SEE ALL'} href={deepLinks.discover}>
|
||||
<div className={styles['label']}>SEE ALL</div>
|
||||
<Icon className={styles['icon']} icon={'ic_arrow_thin_right'} />
|
||||
</Button>
|
||||
|
|
@ -69,7 +69,9 @@ MetaRow.propTypes = {
|
|||
posterShape: PropTypes.string
|
||||
})),
|
||||
itemComponent: PropTypes.elementType,
|
||||
href: PropTypes.string
|
||||
deepLinks: PropTypes.shape({
|
||||
discover: PropTypes.string
|
||||
})
|
||||
};
|
||||
|
||||
module.exports = MetaRow;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const Button = require('stremio/common/Button');
|
|||
const CONSTANTS = require('stremio/common/CONSTANTS');
|
||||
const styles = require('./styles');
|
||||
|
||||
const MetaRowPlaceholder = ({ className, title, href }) => {
|
||||
const MetaRowPlaceholder = ({ className, title, deepLinks }) => {
|
||||
return (
|
||||
<div className={classnames(className, styles['meta-row-placeholder-container'])}>
|
||||
<div className={styles['header-container']}>
|
||||
|
|
@ -14,8 +14,8 @@ const MetaRowPlaceholder = ({ className, title, href }) => {
|
|||
{typeof title === 'string' && title.length > 0 ? title : null}
|
||||
</div>
|
||||
{
|
||||
typeof href === 'string' && href.length > 0 ?
|
||||
<Button className={styles['see-all-container']} title={'SEE ALL'} href={href}>
|
||||
deepLinks && typeof deepLinks.discover === 'string' ?
|
||||
<Button className={styles['see-all-container']} title={'SEE ALL'} href={deepLinks.discover}>
|
||||
<div className={styles['label']}>SEE ALL</div>
|
||||
<Icon className={styles['icon']} icon={'ic_arrow_thin_right'} />
|
||||
</Button>
|
||||
|
|
@ -40,7 +40,9 @@ const MetaRowPlaceholder = ({ className, title, href }) => {
|
|||
MetaRowPlaceholder.propTypes = {
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
href: PropTypes.string
|
||||
deepLinks: PropTypes.shape({
|
||||
discover: PropTypes.string
|
||||
})
|
||||
};
|
||||
|
||||
module.exports = MetaRowPlaceholder;
|
||||
|
|
|
|||
90
src/common/deepLinking.js
Normal file
90
src/common/deepLinking.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
const pako = require('pako');
|
||||
|
||||
const serializeStream = (stream) => {
|
||||
return btoa(pako.deflate(JSON.stringify(stream), { to: 'string' }));
|
||||
};
|
||||
|
||||
const deserializeStream = (stream) => {
|
||||
return JSON.parse(pako.inflate(atob(stream), { to: 'string' }));
|
||||
};
|
||||
|
||||
const withMetaItem = ({ metaItem }) => {
|
||||
return {
|
||||
meta_details_videos: typeof metaItem.behaviorHints.defaultVideoId !== 'string' ?
|
||||
`#/metadetails/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}`
|
||||
:
|
||||
null,
|
||||
meta_details_streams: typeof metaItem.behaviorHints.defaultVideoId === 'string' ?
|
||||
`#/metadetails/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}/${encodeURIComponent(metaItem.behaviorHints.defaultVideoId)}`
|
||||
:
|
||||
null
|
||||
};
|
||||
};
|
||||
|
||||
const withLibItem = ({ libItem, streams = {} }) => {
|
||||
const [stream, streamTransportUrl, metaTransportUrl] = typeof libItem.state.video_id === 'string' && typeof streams[`${encodeURIComponent(libItem._id)}/${encodeURIComponent(libItem.state.video_id)}`] === 'object' ?
|
||||
streams[`${encodeURIComponent(libItem._id)}/${encodeURIComponent(libItem.state.video_id)}`]
|
||||
:
|
||||
[];
|
||||
return {
|
||||
meta_details_videos: typeof libItem.behaviorHints.defaultVideoId !== 'string' ?
|
||||
`#/metadetails/${encodeURIComponent(libItem.type)}/${encodeURIComponent(libItem._id)}`
|
||||
:
|
||||
null,
|
||||
meta_details_streams: typeof libItem.state.video_id === 'string' ?
|
||||
`#/metadetails/${encodeURIComponent(libItem.type)}/${encodeURIComponent(libItem._id)}/${encodeURIComponent(libItem.state.video_id)}`
|
||||
:
|
||||
typeof libItem.behaviorHints.defaultVideoId === 'string' ?
|
||||
`#/metadetails/${encodeURIComponent(libItem.type)}/${encodeURIComponent(libItem._id)}/${encodeURIComponent(libItem.behaviorHints.defaultVideoId)}`
|
||||
:
|
||||
null,
|
||||
// TODO check if stream is external
|
||||
player: typeof libItem.state.video_id === 'string' && typeof stream === 'object' && typeof streamTransportUrl === 'string' && typeof metaTransportUrl === 'string' ?
|
||||
`#/player/${encodeURIComponent(serializeStream(stream))}/${encodeURIComponent(streamTransportUrl)}/${encodeURIComponent(metaTransportUrl)}/${encodeURIComponent(libItem.type)}/${encodeURIComponent(libItem._id)}/${encodeURIComponent(libItem.state.video_id)}`
|
||||
:
|
||||
null
|
||||
};
|
||||
};
|
||||
|
||||
const withVideo = ({ video, metaTransportUrl, metaItem, streams = {} }) => {
|
||||
const [stream, streamTransportUrl] = typeof streams[`${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`] === 'object' ?
|
||||
streams[`${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`]
|
||||
:
|
||||
[];
|
||||
return {
|
||||
meta_details_streams: `#/metadetails/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`,
|
||||
// TODO check if stream is external
|
||||
player: typeof stream === 'object' && typeof streamTransportUrl === 'string' ?
|
||||
`#/player/${encodeURIComponent(serializeStream(stream))}/${encodeURIComponent(streamTransportUrl)}/${encodeURIComponent(metaTransportUrl)}/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`
|
||||
:
|
||||
Array.isArray(video.streams) && video.streams.length === 1 ?
|
||||
`#/player/${encodeURIComponent(serializeStream(video.streams[0]))}/${encodeURIComponent(metaTransportUrl)}/${encodeURIComponent(metaTransportUrl)}/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`
|
||||
:
|
||||
null
|
||||
};
|
||||
};
|
||||
|
||||
const withStream = ({ stream, streamTransportUrl, metaTransportUrl, type, id, videoId }) => {
|
||||
return {
|
||||
player: typeof metaTransportUrl === 'string' && typeof type === 'string' && typeof id === 'string' && typeof videoId === 'string' ?
|
||||
`#/player/${encodeURIComponent(serializeStream(stream))}/${encodeURIComponent(streamTransportUrl)}/${encodeURIComponent(metaTransportUrl)}/${encodeURIComponent(type)}/${encodeURIComponent(id)}/${encodeURIComponent(videoId)}`
|
||||
:
|
||||
`#/player/${encodeURIComponent(serializeStream(stream))}`
|
||||
};
|
||||
};
|
||||
|
||||
const withCatalog = ({ request }) => {
|
||||
return {
|
||||
discover: `#/discover/${encodeURIComponent(request.base)}/${encodeURIComponent(request.path.type_name)}/${encodeURIComponent(request.path.id)}?${new URLSearchParams(request.path.extra).toString()}`
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
withCatalog,
|
||||
withMetaItem,
|
||||
withLibItem,
|
||||
withVideo,
|
||||
withStream,
|
||||
serializeStream,
|
||||
deserializeStream
|
||||
};
|
||||
|
|
@ -21,6 +21,7 @@ const TextInput = require('./TextInput');
|
|||
const { ToastProvider, useToast } = require('./Toast');
|
||||
const comparatorWithPriorities = require('./comparatorWithPriorities');
|
||||
const CONSTANTS = require('./CONSTANTS');
|
||||
const deepLinking = require('./deepLinking');
|
||||
const languageNames = require('./languageNames');
|
||||
const routesRegexp = require('./routesRegexp');
|
||||
const useAnimationFrame = require('./useAnimationFrame');
|
||||
|
|
@ -60,6 +61,7 @@ module.exports = {
|
|||
useToast,
|
||||
comparatorWithPriorities,
|
||||
CONSTANTS,
|
||||
deepLinking,
|
||||
languageNames,
|
||||
routesRegexp,
|
||||
useAnimationFrame,
|
||||
|
|
|
|||
|
|
@ -18,13 +18,12 @@ const Board = () => {
|
|||
title={'Continue Watching'}
|
||||
items={continueWatchingPreview.lib_items}
|
||||
itemComponent={LibItem}
|
||||
href={'#/continuewatching'}
|
||||
deepLinks={continueWatchingPreview.deepLinks}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
{board.catalog_resources.map((catalog_resource, index) => {
|
||||
const href = `#/discover/${encodeURIComponent(catalog_resource.request.base)}/${encodeURIComponent(catalog_resource.request.path.type_name)}/${encodeURIComponent(catalog_resource.request.path.id)}`;
|
||||
const title = `${catalog_resource.origin} - ${catalog_resource.request.path.id} ${catalog_resource.request.path.type_name}`;
|
||||
switch (catalog_resource.content.type) {
|
||||
case 'Ready': {
|
||||
|
|
@ -39,7 +38,7 @@ const Board = () => {
|
|||
title={title}
|
||||
items={catalog_resource.content.content}
|
||||
itemComponent={MetaItem}
|
||||
href={href}
|
||||
deepLinks={catalog_resource.deepLinks}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -51,7 +50,7 @@ const Board = () => {
|
|||
className={styles['board-row']}
|
||||
title={title}
|
||||
message={message}
|
||||
href={href}
|
||||
deepLinks={catalog_resource.deepLinks}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -61,7 +60,7 @@ const Board = () => {
|
|||
key={index}
|
||||
className={classnames(styles['board-row'], styles['board-row-poster'])}
|
||||
title={title}
|
||||
href={href}
|
||||
deepLinks={catalog_resource.deepLinks}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.board-row-landscape {
|
||||
.board-row-landscape, .continue-watching-row {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+9) {
|
||||
display: none;
|
||||
|
|
@ -58,14 +58,6 @@
|
|||
@media only screen and (max-width: @normal) {
|
||||
.board-container {
|
||||
.board-content {
|
||||
.continue-watching-row {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+10) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.board-row-poster, .board-row-square {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+9) {
|
||||
|
|
@ -74,7 +66,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.board-row-landscape {
|
||||
.board-row-landscape, .continue-watching-row {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+8) {
|
||||
display: none;
|
||||
|
|
@ -88,14 +80,6 @@
|
|||
@media only screen and (max-width: @medium) {
|
||||
.board-container {
|
||||
.board-content {
|
||||
.continue-watching-row {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+9) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.board-row-poster, .board-row-square {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+8) {
|
||||
|
|
@ -104,7 +88,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.board-row-landscape {
|
||||
.board-row-landscape, .continue-watching-row {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+7) {
|
||||
display: none;
|
||||
|
|
@ -118,14 +102,6 @@
|
|||
@media only screen and (max-width: @small) {
|
||||
.board-container {
|
||||
.board-content {
|
||||
.continue-watching-row {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+8) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.board-row-poster, .board-row-square {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+7) {
|
||||
|
|
@ -134,7 +110,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.board-row-landscape {
|
||||
.board-row-landscape, .continue-watching-row {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+6) {
|
||||
display: none;
|
||||
|
|
@ -148,14 +124,6 @@
|
|||
@media only screen and (max-width: @xsmall) {
|
||||
.board-container {
|
||||
.board-content {
|
||||
.continue-watching-row {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+7) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.board-row-poster, .board-row-square {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+6) {
|
||||
|
|
@ -164,7 +132,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.board-row-landscape {
|
||||
.board-row-landscape, .continue-watching-row {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+5) {
|
||||
display: none;
|
||||
|
|
@ -178,14 +146,6 @@
|
|||
@media only screen and (max-width: @minimum) {
|
||||
.board-container {
|
||||
.board-content {
|
||||
.continue-watching-row {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+6) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.board-row-poster, .board-row-square {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+5) {
|
||||
|
|
@ -194,7 +154,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.board-row-landscape {
|
||||
.board-row-landscape, .continue-watching-row {
|
||||
.meta-item, .meta-item-placeholder {
|
||||
&:nth-child(n+4) {
|
||||
display: none;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const React = require('react');
|
||||
const { useModelState } = require('stremio/common');
|
||||
const { deepLinking, useModelState } = require('stremio/common');
|
||||
|
||||
const initBoardState = () => ({
|
||||
selected: null,
|
||||
|
|
@ -18,7 +18,7 @@ const mapBoardStateWithCtx = (board, ctx) => {
|
|||
name: metaItem.name,
|
||||
poster: metaItem.poster,
|
||||
posterShape: metaItems[0].posterShape,
|
||||
href: `#/metadetails/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}` // TODO this should redirect with videoId at some cases
|
||||
deepLinks: deepLinking.withMetaItem({ metaItem })
|
||||
}))
|
||||
}
|
||||
:
|
||||
|
|
@ -33,7 +33,8 @@ const mapBoardStateWithCtx = (board, ctx) => {
|
|||
|
||||
return origin;
|
||||
}, catalog_resource.request.base);
|
||||
return { request, content, origin };
|
||||
const deepLinks = deepLinking.withCatalog({ request });
|
||||
return { request, content, origin, deepLinks };
|
||||
});
|
||||
return { selected, catalog_resources };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
const React = require('react');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useModelState } = require('stremio/common');
|
||||
const { deepLinking, useModelState } = require('stremio/common');
|
||||
|
||||
const mapContinueWatchingPreviewState = (continue_watching_preview) => {
|
||||
const lib_items = continue_watching_preview.lib_items.map((lib_item) => ({
|
||||
id: lib_item._id,
|
||||
type: lib_item.type,
|
||||
name: lib_item.name,
|
||||
poster: lib_item.poster,
|
||||
posterShape: lib_item.posterShape === 'landscape' ? 'square' : lib_item.posterShape,
|
||||
videoId: lib_item.state.video_id,
|
||||
progress: lib_item.state.timeOffset > 0 && lib_item.state.duration > 0 ?
|
||||
lib_item.state.timeOffset / lib_item.state.duration
|
||||
const lib_items = continue_watching_preview.lib_items.map((libItem) => ({
|
||||
id: libItem._id,
|
||||
type: libItem.type,
|
||||
name: libItem.name,
|
||||
poster: libItem.poster,
|
||||
posterShape: libItem.posterShape === 'landscape' ? 'square' : libItem.posterShape,
|
||||
progress: libItem.state.timeOffset > 0 && libItem.state.duration > 0 ?
|
||||
libItem.state.timeOffset / libItem.state.duration
|
||||
:
|
||||
null,
|
||||
href: `#/metadetails/${encodeURIComponent(lib_item.type)}/${encodeURIComponent(lib_item._id)}${lib_item.state.video_id !== null ? `/${encodeURIComponent(lib_item.state.video_id)}` : ''}`
|
||||
deepLinks: deepLinking.withLibItem({ libItem })
|
||||
}));
|
||||
return { lib_items };
|
||||
const deepLinks = { discover: '#/continuewatching' };
|
||||
return { lib_items, deepLinks };
|
||||
};
|
||||
|
||||
const useContinueWatchingPreview = () => {
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
poster={metaItem.poster}
|
||||
posterShape={metaItem.posterShape}
|
||||
playIcon={selectedMetaItem === metaItem}
|
||||
href={metaItem.href}
|
||||
deepLinks={metaItem.deepLinks}
|
||||
data-index={index}
|
||||
onClick={metaItemOnClick}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const React = require('react');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { CONSTANTS, useModelState, comparatorWithPriorities } = require('stremio/common');
|
||||
const { CONSTANTS, deepLinking, useModelState, comparatorWithPriorities } = require('stremio/common');
|
||||
|
||||
const initDiscoverState = () => ({
|
||||
selected: null,
|
||||
|
|
@ -41,7 +41,7 @@ const mapDiscoverState = (discover) => {
|
|||
released: new Date(metaItem.released),
|
||||
description: metaItem.description,
|
||||
trailer: metaItem.trailer,
|
||||
href: `#/metadetails/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}` // TODO this should redirect with videoId at some cases
|
||||
deepLinks: deepLinking.withMetaItem({ metaItem })
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const React = require('react');
|
||||
const { CONSTANTS, useModelState, comparatorWithPriorities } = require('stremio/common');
|
||||
const { CONSTANTS, deepLinking, useModelState, comparatorWithPriorities } = require('stremio/common');
|
||||
|
||||
const initLibraryState = () => ({
|
||||
selected: null,
|
||||
|
|
@ -10,18 +10,17 @@ const initLibraryState = () => ({
|
|||
const mapLibraryState = (library) => {
|
||||
const selected = library.selected;
|
||||
const type_names = library.type_names.sort(comparatorWithPriorities(CONSTANTS.TYPE_PRIORITIES));
|
||||
const lib_items = library.lib_items.map((lib_item) => ({
|
||||
id: lib_item._id,
|
||||
type: lib_item.type,
|
||||
name: lib_item.name,
|
||||
poster: lib_item.poster,
|
||||
posterShape: lib_item.posterShape === 'landscape' ? 'square' : lib_item.posterShape,
|
||||
progress: lib_item.state.timeOffset > 0 && lib_item.state.duration > 0 ?
|
||||
lib_item.state.timeOffset / lib_item.state.duration
|
||||
const lib_items = library.lib_items.map((libItem) => ({
|
||||
id: libItem._id,
|
||||
type: libItem.type,
|
||||
name: libItem.name,
|
||||
poster: libItem.poster,
|
||||
posterShape: libItem.posterShape === 'landscape' ? 'square' : libItem.posterShape,
|
||||
progress: libItem.state.timeOffset > 0 && libItem.state.duration > 0 ?
|
||||
libItem.state.timeOffset / libItem.state.duration
|
||||
:
|
||||
null,
|
||||
videoId: lib_item.state.video_id,
|
||||
href: `#/metadetails/${encodeURIComponent(lib_item.type)}/${encodeURIComponent(lib_item._id)}${lib_item.state.video_id !== null ? `/${encodeURIComponent(lib_item.state.video_id)}` : ''}`
|
||||
deepLinks: deepLinking.withLibItem({ libItem })
|
||||
}));
|
||||
return { selected, type_names, lib_items };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,12 +6,21 @@ const { Button, Image, PlayIconCircleCentered } = require('stremio/common');
|
|||
const StreamPlaceholder = require('./StreamPlaceholder');
|
||||
const styles = require('./styles');
|
||||
|
||||
const Stream = ({ className, addonName, title, thumbnail, progress, ...props }) => {
|
||||
const Stream = ({ className, addonName, title, thumbnail, progress, deepLinks, ...props }) => {
|
||||
const href = React.useMemo(() => {
|
||||
return deepLinks ?
|
||||
typeof deepLinks.player === 'string' ?
|
||||
deepLinks.player
|
||||
:
|
||||
null
|
||||
:
|
||||
null;
|
||||
}, [deepLinks]);
|
||||
const renderThumbnailFallback = React.useMemo(() => () => (
|
||||
<Icon className={styles['placeholder-icon']} icon={'ic_broken_link'} />
|
||||
), []);
|
||||
return (
|
||||
<Button {...props} className={classnames(className, styles['stream-container'])} title={title}>
|
||||
<Button href={href} {...props} className={classnames(className, styles['stream-container'])} title={title}>
|
||||
{
|
||||
typeof thumbnail === 'string' && thumbnail.length > 0 ?
|
||||
<div className={styles['thumbnail-container']} title={addonName}>
|
||||
|
|
@ -49,6 +58,9 @@ Stream.propTypes = {
|
|||
title: PropTypes.string,
|
||||
thumbnail: PropTypes.string,
|
||||
progress: PropTypes.number,
|
||||
deepLinks: PropTypes.shape({
|
||||
player: PropTypes.string
|
||||
})
|
||||
};
|
||||
|
||||
module.exports = Stream;
|
||||
|
|
|
|||
|
|
@ -6,9 +6,21 @@ const { Button, Image } = require('stremio/common');
|
|||
const VideoPlaceholder = require('./VideoPlaceholder');
|
||||
const styles = require('./styles');
|
||||
|
||||
const Video = ({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, ...props }) => {
|
||||
const Video = ({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, deepLinks, ...props }) => {
|
||||
const href = React.useMemo(() => {
|
||||
return deepLinks ?
|
||||
typeof deepLinks.player === 'string' ?
|
||||
deepLinks.player
|
||||
:
|
||||
typeof deepLinks.meta_details_streams === 'string' ?
|
||||
deepLinks.meta_details_streams
|
||||
:
|
||||
null
|
||||
:
|
||||
null;
|
||||
}, [deepLinks]);
|
||||
return (
|
||||
<Button {...props} className={classnames(className, styles['video-container'])} title={title}>
|
||||
<Button href={href} {...props} className={classnames(className, styles['video-container'])} title={title}>
|
||||
{
|
||||
typeof thumbnail === 'string' && thumbnail.length > 0 ?
|
||||
<div className={styles['thumbnail-container']}>
|
||||
|
|
@ -84,7 +96,11 @@ Video.propTypes = {
|
|||
released: PropTypes.instanceOf(Date),
|
||||
upcoming: PropTypes.bool,
|
||||
watched: PropTypes.bool,
|
||||
progress: PropTypes.number
|
||||
progress: PropTypes.number,
|
||||
deepLinks: PropTypes.shape({
|
||||
meta_details_streams: PropTypes.string,
|
||||
player: PropTypes.string
|
||||
})
|
||||
};
|
||||
|
||||
module.exports = Video;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const React = require('react');
|
||||
const { useModelState } = require('stremio/common');
|
||||
const { deepLinking, useModelState } = require('stremio/common');
|
||||
|
||||
const initMetaDetailsState = () => ({
|
||||
selected: null,
|
||||
|
|
@ -32,7 +32,11 @@ const mapMetaDetailsState = (meta_details) => {
|
|||
NaN
|
||||
),
|
||||
// TODO add watched and progress
|
||||
href: `#/metadetails/${meta_resource.content.content.type}/${meta_resource.content.content.id}/${video.id}`
|
||||
deepLinks: deepLinking.withVideo({
|
||||
video,
|
||||
metaTransportUrl: meta_resource.request.base,
|
||||
metaItem: meta_resource.content.content
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
@ -40,8 +44,36 @@ const mapMetaDetailsState = (meta_details) => {
|
|||
:
|
||||
meta_resource;
|
||||
});
|
||||
// TODO map streams with progress
|
||||
const streams_resources = meta_details.streams_resources;
|
||||
const streams_resources = meta_details.streams_resources.map((stream_resource) => {
|
||||
return stream_resource.content.type === 'Ready' ?
|
||||
{
|
||||
request: stream_resource.request,
|
||||
content: {
|
||||
type: 'Ready',
|
||||
content: stream_resource.content.content.map((stream) => ({
|
||||
...stream,
|
||||
// TODO map progress
|
||||
deepLinks: deepLinking.withStream({
|
||||
stream,
|
||||
streamTransportUrl: stream_resource.request.base,
|
||||
// TODO metaTransportUrl should be based on state
|
||||
metaTransportUrl: meta_details.meta_resources.reduceRight((result, meta_resource) => {
|
||||
if (meta_resource.content.type === 'Ready') {
|
||||
return meta_resource.request.base;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, ''),
|
||||
type: selected.meta_resource_ref.type_name,
|
||||
id: selected.meta_resource_ref.id,
|
||||
videoId: selected.streams_resource_ref.id,
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
:
|
||||
stream_resource;
|
||||
});
|
||||
return { selected, meta_resources, streams_resources };
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -189,19 +189,18 @@ const Player = ({ urlParams }) => {
|
|||
0
|
||||
}
|
||||
});
|
||||
dispatch({
|
||||
commandName: 'addSubtitlesTracks',
|
||||
commandArgs: {
|
||||
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'
|
||||
}))
|
||||
}
|
||||
});
|
||||
if (Array.isArray(player.selected.stream.subtitles)) {
|
||||
dispatch({
|
||||
commandName: 'addSubtitlesTracks',
|
||||
commandArgs: {
|
||||
tracks: player.selected.stream.subtitles.map(({ url, lang }) => ({
|
||||
url,
|
||||
lang,
|
||||
origin: 'EMBEDDED IN STREAM'
|
||||
}))
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [player.selected]);
|
||||
useDeepEqualEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
const React = require('react');
|
||||
const pako = require('pako');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useModelState } = require('stremio/common');
|
||||
const { deepLinking, useModelState } = require('stremio/common');
|
||||
|
||||
const initPlayerState = () => ({
|
||||
selected: null,
|
||||
|
|
@ -86,7 +85,7 @@ const usePlayer = (urlParams) => {
|
|||
args: {
|
||||
model: 'Player',
|
||||
args: {
|
||||
stream: JSON.parse(pako.inflate(atob(urlParams.stream), { to: 'string' })),
|
||||
stream: deepLinking.deserializeStream(urlParams.stream),
|
||||
stream_resource_request: typeof urlParams.streamTransportUrl === 'string' && typeof urlParams.type === 'string' && typeof urlParams.videoId === 'string' ?
|
||||
{
|
||||
base: urlParams.streamTransportUrl,
|
||||
|
|
|
|||
|
|
@ -47,8 +47,6 @@ const Search = ({ queryParams }) => {
|
|||
</div>
|
||||
:
|
||||
search.catalog_resources.map((catalog_resource, index) => {
|
||||
const queryString = new URLSearchParams([['search', query]]).toString();
|
||||
const href = `#/discover/${encodeURIComponent(catalog_resource.request.base)}/${encodeURIComponent(catalog_resource.request.path.type_name)}/${encodeURIComponent(catalog_resource.request.path.id)}?${queryString}`;
|
||||
const title = `${catalog_resource.origin} - ${catalog_resource.request.path.id} ${catalog_resource.request.path.type_name}`;
|
||||
switch (catalog_resource.content.type) {
|
||||
case 'Ready': {
|
||||
|
|
@ -63,7 +61,7 @@ const Search = ({ queryParams }) => {
|
|||
title={title}
|
||||
items={catalog_resource.content.content}
|
||||
itemComponent={MetaItem}
|
||||
href={href}
|
||||
deepLinks={catalog_resource.deepLinks}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -75,7 +73,7 @@ const Search = ({ queryParams }) => {
|
|||
className={styles['search-row']}
|
||||
title={title}
|
||||
message={message}
|
||||
href={href}
|
||||
deepLinks={catalog_resource.deepLinks}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -85,7 +83,7 @@ const Search = ({ queryParams }) => {
|
|||
key={index}
|
||||
className={classnames(styles['search-row'], styles['search-row-poster'])}
|
||||
title={title}
|
||||
href={href}
|
||||
deepLinks={catalog_resource.deepLinks}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const React = require('react');
|
||||
const { useModelState } = require('stremio/common');
|
||||
const { deepLinking, useModelState } = require('stremio/common');
|
||||
|
||||
const initSearchState = () => ({
|
||||
selected: null,
|
||||
|
|
@ -18,7 +18,7 @@ const mapSearchStateWithCtx = (search, ctx) => {
|
|||
name: metaItem.name,
|
||||
poster: metaItem.poster,
|
||||
posterShape: metaItems[0].posterShape,
|
||||
href: `#/metadetails/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}` // TODO this should redirect with videoId at some cases
|
||||
deepLinks: deepLinking.withMetaItem({ metaItem })
|
||||
}))
|
||||
}
|
||||
:
|
||||
|
|
@ -33,7 +33,8 @@ const mapSearchStateWithCtx = (search, ctx) => {
|
|||
|
||||
return origin;
|
||||
}, catalog_resource.request.base);
|
||||
return { request, content, origin };
|
||||
const deepLinks = deepLinking.withCatalog({ request });
|
||||
return { request, content, origin, deepLinks };
|
||||
});
|
||||
return { selected, catalog_resources };
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue