mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-21 03:22:11 +00:00
Merge branch 'development' of github.com:Stremio/stremio-web into streaming-server-middleware
This commit is contained in:
commit
db516eb45e
12 changed files with 192 additions and 264 deletions
|
|
@ -7,6 +7,7 @@ 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 META_LINK_CATEGORY = 'meta';
|
||||||
const IMDB_LINK_CATEGORY = 'imdb';
|
const IMDB_LINK_CATEGORY = 'imdb';
|
||||||
const SHARE_LINK_CATEGORY = 'share';
|
const SHARE_LINK_CATEGORY = 'share';
|
||||||
const TYPE_PRIORITIES = {
|
const TYPE_PRIORITIES = {
|
||||||
|
|
@ -31,6 +32,7 @@ module.exports = {
|
||||||
CATALOG_PAGE_SIZE,
|
CATALOG_PAGE_SIZE,
|
||||||
NONE_EXTRA_VALUE,
|
NONE_EXTRA_VALUE,
|
||||||
SKIP_EXTRA_NAME,
|
SKIP_EXTRA_NAME,
|
||||||
|
META_LINK_CATEGORY,
|
||||||
IMDB_LINK_CATEGORY,
|
IMDB_LINK_CATEGORY,
|
||||||
SHARE_LINK_CATEGORY,
|
SHARE_LINK_CATEGORY,
|
||||||
TYPE_PRIORITIES
|
TYPE_PRIORITIES
|
||||||
|
|
|
||||||
|
|
@ -27,43 +27,44 @@ const ALLOWED_LINK_REDIRECTS = [
|
||||||
const MetaPreview = ({ className, compact, name, logo, background, runtime, releaseInfo, released, description, links, trailer, inLibrary, toggleInLibrary }) => {
|
const MetaPreview = ({ className, compact, name, logo, background, runtime, releaseInfo, released, description, links, trailer, inLibrary, toggleInLibrary }) => {
|
||||||
const [shareModalOpen, openShareModal, closeShareModal] = useBinaryState(false);
|
const [shareModalOpen, openShareModal, closeShareModal] = useBinaryState(false);
|
||||||
const linksGroups = React.useMemo(() => {
|
const linksGroups = React.useMemo(() => {
|
||||||
return Array.isArray(links) ?
|
return 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 === CONSTANTS.IMDB_LINK_CATEGORY) {
|
||||||
if (category === CONSTANTS.IMDB_LINK_CATEGORY) {
|
linksGroups.set(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 === CONSTANTS.SHARE_LINK_CATEGORY) {
|
||||||
} else if (category === CONSTANTS.SHARE_LINK_CATEGORY) {
|
linksGroups.set(category, {
|
||||||
linksGroups[category] = {
|
label: name,
|
||||||
label: name,
|
href: url
|
||||||
href: url
|
});
|
||||||
};
|
} else {
|
||||||
} else {
|
const { protocol, host, path, pathname } = UrlUtils.parse(url);
|
||||||
const { protocol, host, path, pathname } = UrlUtils.parse(url);
|
if (protocol === 'stremio:') {
|
||||||
if (protocol === 'stremio:') {
|
if (pathname !== null && ALLOWED_LINK_REDIRECTS.some((regexp) => pathname.match(regexp))) {
|
||||||
if (ALLOWED_LINK_REDIRECTS.some((regexp) => pathname.match(regexp))) {
|
if (!linksGroups.has(category)) {
|
||||||
linksGroups[category] = linksGroups[category] || [];
|
linksGroups.set(category, []);
|
||||||
linksGroups[category].push({
|
|
||||||
label: name,
|
|
||||||
href: `#${path}`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (typeof host === 'string' && host.length > 0) {
|
linksGroups.get(category).push({
|
||||||
linksGroups[category] = linksGroups[category] || [];
|
|
||||||
linksGroups[category].push({
|
|
||||||
label: name,
|
label: name,
|
||||||
href: `https://www.stremio.com/warning#${encodeURIComponent(url)}`
|
href: `#${path}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (typeof host === 'string' && host.length > 0) {
|
||||||
|
if (!linksGroups.has(category)) {
|
||||||
|
linksGroups.set(category, []);
|
||||||
|
}
|
||||||
|
linksGroups.get(category).push({
|
||||||
|
label: name,
|
||||||
|
href: `https://www.stremio.com/warning#${encodeURIComponent(url)}`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return linksGroups;
|
return linksGroups;
|
||||||
}, {})
|
}, new Map());
|
||||||
:
|
|
||||||
[];
|
|
||||||
}, [links]);
|
}, [links]);
|
||||||
const trailerHref = React.useMemo(() => {
|
const trailerHref = React.useMemo(() => {
|
||||||
if (typeof trailer !== 'object' || trailer === null) {
|
if (typeof trailer !== 'object' || trailer === null) {
|
||||||
|
|
@ -99,7 +100,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[CONSTANTS.IMDB_LINK_CATEGORY] === 'object' ?
|
(typeof releaseInfo === 'string' && releaseInfo.length > 0) || (released instanceof Date && !isNaN(released.getTime())) || (typeof runtime === 'string' && runtime.length > 0) || linksGroups.has(CONSTANTS.IMDB_LINK_CATEGORY) ?
|
||||||
<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 ?
|
||||||
|
|
@ -117,16 +118,16 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
typeof linksGroups[CONSTANTS.IMDB_LINK_CATEGORY] === 'object' ?
|
linksGroups.has(CONSTANTS.IMDB_LINK_CATEGORY) ?
|
||||||
<Button
|
<Button
|
||||||
className={styles['imdb-button-container']}
|
className={styles['imdb-button-container']}
|
||||||
title={linksGroups[CONSTANTS.IMDB_LINK_CATEGORY].label}
|
title={linksGroups.get(CONSTANTS.IMDB_LINK_CATEGORY).label}
|
||||||
href={linksGroups[CONSTANTS.IMDB_LINK_CATEGORY].href}
|
href={linksGroups.get(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[CONSTANTS.IMDB_LINK_CATEGORY].label}</div>
|
<div className={styles['label']}>{linksGroups.get(CONSTANTS.IMDB_LINK_CATEGORY).label}</div>
|
||||||
</Button>
|
</Button>
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
|
|
@ -150,7 +151,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
Object.keys(linksGroups)
|
Array.from(linksGroups.keys())
|
||||||
.filter((category) => {
|
.filter((category) => {
|
||||||
return category !== CONSTANTS.IMDB_LINK_CATEGORY &&
|
return category !== CONSTANTS.IMDB_LINK_CATEGORY &&
|
||||||
category !== CONSTANTS.SHARE_LINK_CATEGORY;
|
category !== CONSTANTS.SHARE_LINK_CATEGORY;
|
||||||
|
|
@ -160,7 +161,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
|
||||||
key={index}
|
key={index}
|
||||||
className={styles['meta-links']}
|
className={styles['meta-links']}
|
||||||
label={category}
|
label={category}
|
||||||
links={linksGroups[category]}
|
links={linksGroups.get(category)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +192,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
typeof linksGroups[CONSTANTS.SHARE_LINK_CATEGORY] === 'object' ?
|
linksGroups.has(CONSTANTS.SHARE_LINK_CATEGORY) ?
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
className={styles['action-button']}
|
className={styles['action-button']}
|
||||||
|
|
@ -205,7 +206,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[CONSTANTS.SHARE_LINK_CATEGORY].href}
|
url={linksGroups.get(CONSTANTS.SHARE_LINK_CATEGORY).href}
|
||||||
/>
|
/>
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
:
|
:
|
||||||
|
|
|
||||||
|
|
@ -23,17 +23,6 @@ const withMetaItem = ({ metaItem }) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const withMetaResource = ({ metaResource, type, id, videoId }) => {
|
|
||||||
const queryParams = new URLSearchParams([['metaTransportUrl', metaResource.request.base]]);
|
|
||||||
return {
|
|
||||||
meta_details_videos: `#/metadetails/${encodeURIComponent(type)}/${encodeURIComponent(id)}?${queryParams.toString()}`,
|
|
||||||
meta_details_streams: typeof videoId === 'string' ?
|
|
||||||
`#/metadetails/${encodeURIComponent(type)}/${encodeURIComponent(id)}/${encodeURIComponent(videoId)}?${queryParams.toString()}`
|
|
||||||
:
|
|
||||||
null
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const withLibItem = ({ libItem, streams = {} }) => {
|
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' ?
|
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)}`]
|
streams[`${encodeURIComponent(libItem._id)}/${encodeURIComponent(libItem.state.video_id)}`]
|
||||||
|
|
@ -60,13 +49,12 @@ const withLibItem = ({ libItem, streams = {} }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const withVideo = ({ video, metaTransportUrl, metaItem, streams = {} }) => {
|
const withVideo = ({ video, metaTransportUrl, metaItem, streams = {} }) => {
|
||||||
const queryParams = new URLSearchParams([['metaTransportUrl', metaTransportUrl]]);
|
|
||||||
const [stream, streamTransportUrl] = typeof streams[`${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`] === 'object' ?
|
const [stream, streamTransportUrl] = typeof streams[`${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`] === 'object' ?
|
||||||
streams[`${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`]
|
streams[`${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`]
|
||||||
:
|
:
|
||||||
[];
|
[];
|
||||||
return {
|
return {
|
||||||
meta_details_streams: `#/metadetails/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}?${queryParams.toString()}`,
|
meta_details_streams: `#/metadetails/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`,
|
||||||
// TODO check if stream is external
|
// TODO check if stream is external
|
||||||
player: typeof stream === 'object' && typeof streamTransportUrl === 'string' ?
|
player: typeof stream === 'object' && typeof streamTransportUrl === 'string' ?
|
||||||
`#/player/${encodeURIComponent(serializeStream(stream))}/${encodeURIComponent(streamTransportUrl)}/${encodeURIComponent(metaTransportUrl)}/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`
|
`#/player/${encodeURIComponent(serializeStream(stream))}/${encodeURIComponent(streamTransportUrl)}/${encodeURIComponent(metaTransportUrl)}/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`
|
||||||
|
|
@ -96,7 +84,6 @@ const withCatalog = ({ request }) => {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
withCatalog,
|
withCatalog,
|
||||||
withMetaItem,
|
withMetaItem,
|
||||||
withMetaResource,
|
|
||||||
withLibItem,
|
withLibItem,
|
||||||
withVideo,
|
withVideo,
|
||||||
withStream,
|
withStream,
|
||||||
|
|
|
||||||
|
|
@ -277,23 +277,20 @@ const Intro = ({ queryParams }) => {
|
||||||
};
|
};
|
||||||
}, [routeFocused]);
|
}, [routeFocused]);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
var initScriptElement = document.createElement('script');
|
window.fbAsyncInit = function() {
|
||||||
var sdkScriptElement = document.createElement('script');
|
FB.init({
|
||||||
initScriptElement.innerHTML = `window.fbAsyncInit = function() {
|
appId: '1537119779906825',
|
||||||
FB.init({
|
autoLogAppEvents: false,
|
||||||
appId: '1537119779906825',
|
xfbml: false,
|
||||||
autoLogAppEvents: false,
|
version: 'v2.5'
|
||||||
xfbml: false,
|
});
|
||||||
version: 'v2.5'
|
};
|
||||||
});
|
const sdkScriptElement = document.createElement('script');
|
||||||
};`;
|
|
||||||
sdkScriptElement.src = 'https://connect.facebook.net/en_US/sdk.js';
|
sdkScriptElement.src = 'https://connect.facebook.net/en_US/sdk.js';
|
||||||
sdkScriptElement.async = true;
|
sdkScriptElement.async = true;
|
||||||
sdkScriptElement.defer = true;
|
sdkScriptElement.defer = true;
|
||||||
document.body.appendChild(initScriptElement);
|
|
||||||
document.body.appendChild(sdkScriptElement);
|
document.body.appendChild(sdkScriptElement);
|
||||||
return () => {
|
return () => {
|
||||||
document.body.removeChild(initScriptElement);
|
|
||||||
document.body.removeChild(sdkScriptElement);
|
document.body.removeChild(sdkScriptElement);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
|
|
@ -2,35 +2,41 @@
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const { VerticalNavBar, HorizontalNavBar, MetaPreview, Image, useInLibrary } = require('stremio/common');
|
const { VerticalNavBar, HorizontalNavBar, MetaPreview, ModalDialog, Image, useInLibrary } = require('stremio/common');
|
||||||
const StreamsList = require('./StreamsList');
|
const StreamsList = require('./StreamsList');
|
||||||
const VideosList = require('./VideosList');
|
const VideosList = require('./VideosList');
|
||||||
const useMetaDetails = require('./useMetaDetails');
|
const useMetaDetails = require('./useMetaDetails');
|
||||||
|
const useMetaExtensions = require('./useMetaExtensions');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
const MetaDetails = ({ urlParams, queryParams }) => {
|
const MetaDetails = ({ urlParams, queryParams }) => {
|
||||||
const metaDetails = useMetaDetails(urlParams);
|
const metaDetails = useMetaDetails(urlParams);
|
||||||
|
const { tabs, selectedMetaExtension, clearSelectedMetaExtension } = useMetaExtensions(metaDetails.meta_resources);
|
||||||
const metaResourceRef = React.useMemo(() => {
|
const metaResourceRef = React.useMemo(() => {
|
||||||
return metaDetails.selected !== null ? metaDetails.selected.meta_resources_ref : null;
|
return metaDetails.selected !== null ? metaDetails.selected.meta_resources_ref : null;
|
||||||
}, [metaDetails.selected]);
|
}, [metaDetails.selected]);
|
||||||
const selectedAddon = queryParams.get('metaTransportUrl');
|
|
||||||
const selectedMetaResource = React.useMemo(() => {
|
const selectedMetaResource = React.useMemo(() => {
|
||||||
return metaDetails.meta_resources.reduceRight((result, metaResource) => {
|
return metaDetails.meta_resources.reduceRight((result, metaResource) => {
|
||||||
if (typeof selectedAddon === 'string') {
|
if (metaResource.content.type === 'Ready') {
|
||||||
if (metaResource.request.base === selectedAddon) {
|
|
||||||
return metaResource;
|
|
||||||
}
|
|
||||||
} else if (metaResource.content.type === 'Ready') {
|
|
||||||
return metaResource;
|
return metaResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, null);
|
}, null);
|
||||||
}, [metaDetails, selectedAddon]);
|
}, [metaDetails]);
|
||||||
const streamsResourceRef = metaDetails.selected !== null ? metaDetails.selected.streams_resource_ref : null;
|
const streamsResourceRef = metaDetails.selected !== null ? metaDetails.selected.streams_resource_ref : null;
|
||||||
const streamsResources = metaDetails.streams_resources;
|
const streamsResources = metaDetails.streams_resources;
|
||||||
|
const seasonQueryParam = React.useMemo(() => {
|
||||||
|
return queryParams.has('season') && !isNaN(queryParams.get('season')) ?
|
||||||
|
parseInt(queryParams.get('season'))
|
||||||
|
:
|
||||||
|
null;
|
||||||
|
}, [queryParams]);
|
||||||
|
const seasonOnSelect = React.useCallback((event) => {
|
||||||
|
window.location.replace(`#/metadetails/${selectedMetaResource.request.path.type_name}/${selectedMetaResource.request.path.id}?season=${event.value}`);
|
||||||
|
}, [selectedMetaResource]);
|
||||||
const selectedVideo = React.useMemo(() => {
|
const selectedVideo = React.useMemo(() => {
|
||||||
return streamsResourceRef !== null && selectedMetaResource !== null && selectedMetaResource.content.type === 'Ready' ?
|
return streamsResourceRef !== null && selectedMetaResource !== null ?
|
||||||
selectedMetaResource.content.content.videos.reduce((result, video) => {
|
selectedMetaResource.content.content.videos.reduce((result, video) => {
|
||||||
if (video.id === streamsResourceRef.id) {
|
if (video.id === streamsResourceRef.id) {
|
||||||
return video;
|
return video;
|
||||||
|
|
@ -41,30 +47,21 @@ const MetaDetails = ({ urlParams, queryParams }) => {
|
||||||
:
|
:
|
||||||
null;
|
null;
|
||||||
}, [selectedMetaResource, streamsResourceRef]);
|
}, [selectedMetaResource, streamsResourceRef]);
|
||||||
const tabs = React.useMemo(() => {
|
const [inLibrary, toggleInLibrary] = useInLibrary(selectedMetaResource !== null ? selectedMetaResource.content.content : null);
|
||||||
return metaDetails.meta_resources.map((metaResource) => ({
|
|
||||||
id: metaResource.addon.transportUrl,
|
|
||||||
label: metaResource.addon.manifest.name,
|
|
||||||
logo: metaResource.addon.manifest.logo,
|
|
||||||
icon: 'ic_addons',
|
|
||||||
href: metaResource.deepLinks.meta_details_streams !== null ? metaResource.deepLinks.meta_details_streams : metaResource.deepLinks.meta_details_videos
|
|
||||||
}));
|
|
||||||
}, [metaDetails]);
|
|
||||||
const [inLibrary, toggleInLibrary] = useInLibrary(selectedMetaResource !== null && selectedMetaResource.content.type === 'Ready' ? selectedMetaResource.content.content : null);
|
|
||||||
return (
|
return (
|
||||||
<div className={styles['metadetails-container']}>
|
<div className={styles['metadetails-container']}>
|
||||||
<HorizontalNavBar
|
<HorizontalNavBar
|
||||||
className={styles['nav-bar']}
|
className={styles['nav-bar']}
|
||||||
backButton={true}
|
backButton={true}
|
||||||
title={selectedMetaResource !== null && selectedMetaResource.content.type === 'Ready' ? selectedMetaResource.content.content.name : null}
|
title={selectedMetaResource !== null ? selectedMetaResource.content.content.name : null}
|
||||||
/>
|
/>
|
||||||
<div className={styles['metadetails-content']}>
|
<div className={styles['metadetails-content']}>
|
||||||
{
|
{
|
||||||
metaDetails.meta_resources.length > 0 ?
|
tabs.length > 0 ?
|
||||||
<VerticalNavBar
|
<VerticalNavBar
|
||||||
className={styles['vertical-nav-bar']}
|
className={styles['vertical-nav-bar']}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
selected={selectedMetaResource !== null ? selectedMetaResource.request.base : null}
|
selected={selectedMetaExtension !== null ? selectedMetaExtension.url : null}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
|
|
@ -88,7 +85,7 @@ const MetaDetails = ({ urlParams, queryParams }) => {
|
||||||
<div className={styles['message-label']}>No metadata was found!</div>
|
<div className={styles['message-label']}>No metadata was found!</div>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
selectedMetaResource !== null && selectedMetaResource.content.type === 'Ready' ?
|
selectedMetaResource !== null ?
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{
|
{
|
||||||
typeof selectedMetaResource.content.content.background === 'string' &&
|
typeof selectedMetaResource.content.content.background === 'string' &&
|
||||||
|
|
@ -137,11 +134,28 @@ const MetaDetails = ({ urlParams, queryParams }) => {
|
||||||
<VideosList
|
<VideosList
|
||||||
className={styles['videos-list']}
|
className={styles['videos-list']}
|
||||||
metaResource={selectedMetaResource}
|
metaResource={selectedMetaResource}
|
||||||
|
season={seasonQueryParam}
|
||||||
|
seasonOnSelect={seasonOnSelect}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
selectedMetaExtension !== null ?
|
||||||
|
<ModalDialog
|
||||||
|
className={styles['meta-extension-modal-container']}
|
||||||
|
title={selectedMetaExtension.name}
|
||||||
|
onCloseRequest={clearSelectedMetaExtension}>
|
||||||
|
<iframe
|
||||||
|
className={styles['meta-extension-modal-iframe']}
|
||||||
|
sandbox={'allow-forms allow-scripts allow-same-origin'}
|
||||||
|
src={selectedMetaExtension.url}
|
||||||
|
/>
|
||||||
|
</ModalDialog>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-height: 3.6em;
|
max-height: 3.6em;
|
||||||
margin: 0.5rem 1rem;
|
margin: 0.5rem 1rem;
|
||||||
|
white-space: pre;
|
||||||
color: @color-surface-light5-90;
|
color: @color-surface-light5-90;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,44 @@ const Image = require('stremio/common/Image');
|
||||||
const SearchBar = require('stremio/common/SearchBar');
|
const SearchBar = require('stremio/common/SearchBar');
|
||||||
const SeasonsBar = require('./SeasonsBar');
|
const SeasonsBar = require('./SeasonsBar');
|
||||||
const Video = require('./Video');
|
const Video = require('./Video');
|
||||||
const useSelectableSeasons = require('./useSelectableSeasons');
|
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
const VideosList = ({ className, metaResource }) => {
|
const VideosList = ({ className, metaResource, season, seasonOnSelect }) => {
|
||||||
const videos = React.useMemo(() => {
|
const videos = React.useMemo(() => {
|
||||||
return metaResource && metaResource.content.type === 'Ready' ?
|
return metaResource && metaResource.content.type === 'Ready' ?
|
||||||
metaResource.content.content.videos
|
metaResource.content.content.videos
|
||||||
:
|
:
|
||||||
[];
|
[];
|
||||||
}, [metaResource]);
|
}, [metaResource]);
|
||||||
const [seasons, selectedSeason, videosForSeason, selectSeason] = useSelectableSeasons(videos);
|
const seasons = React.useMemo(() => {
|
||||||
const seasonOnSelect = React.useCallback((event) => {
|
return videos
|
||||||
selectSeason(event.value);
|
.map(({ season }) => season)
|
||||||
}, []);
|
.filter((season, index, seasons) => {
|
||||||
|
return season !== null &&
|
||||||
|
!isNaN(season) &&
|
||||||
|
typeof season === 'number' &&
|
||||||
|
seasons.indexOf(season) === index;
|
||||||
|
})
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
}, [videos]);
|
||||||
|
const selectedSeason = React.useMemo(() => {
|
||||||
|
return seasons.includes(season) ?
|
||||||
|
season
|
||||||
|
:
|
||||||
|
seasons.length > 0 ?
|
||||||
|
seasons[seasons.length - 1]
|
||||||
|
:
|
||||||
|
null;
|
||||||
|
}, [seasons, season]);
|
||||||
|
const videosForSeason = React.useMemo(() => {
|
||||||
|
return videos
|
||||||
|
.filter((video) => {
|
||||||
|
return selectedSeason === null || video.season === selectedSeason;
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.episode - b.episode;
|
||||||
|
});
|
||||||
|
}, [videos, selectedSeason]);
|
||||||
const [search, setSearch] = React.useState('');
|
const [search, setSearch] = React.useState('');
|
||||||
const searchInputOnChange = React.useCallback((event) => {
|
const searchInputOnChange = React.useCallback((event) => {
|
||||||
setSearch(event.currentTarget.value);
|
setSearch(event.currentTarget.value);
|
||||||
|
|
@ -88,7 +112,9 @@ const VideosList = ({ className, metaResource }) => {
|
||||||
|
|
||||||
VideosList.propTypes = {
|
VideosList.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
metaResource: PropTypes.object
|
metaResource: PropTypes.object,
|
||||||
|
season: PropTypes.number,
|
||||||
|
seasonOnSelect: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = VideosList;
|
module.exports = VideosList;
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
// Copyright (C) 2017-2020 Smart code 203358507
|
|
||||||
|
|
||||||
const React = require('react');
|
|
||||||
|
|
||||||
const reducer = (state, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'videos-changed': {
|
|
||||||
const seasons = action.videos
|
|
||||||
.map(({ season }) => season)
|
|
||||||
.filter((season, index, seasons) => {
|
|
||||||
return season !== null &&
|
|
||||||
!isNaN(season) &&
|
|
||||||
typeof season === 'number' &&
|
|
||||||
seasons.indexOf(season) === index;
|
|
||||||
})
|
|
||||||
.sort((a, b) => a - b);
|
|
||||||
const selectedSeason = seasons.includes(state.selectedSeason) ?
|
|
||||||
state.selectedSeason
|
|
||||||
:
|
|
||||||
seasons.length > 0 ?
|
|
||||||
seasons[0]
|
|
||||||
:
|
|
||||||
null;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
videos: action.videos,
|
|
||||||
seasons,
|
|
||||||
selectedSeason
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'season-changed': {
|
|
||||||
const selectedSeason = state.seasons.includes(action.season) ?
|
|
||||||
action.season
|
|
||||||
:
|
|
||||||
state.season;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
selectedSeason
|
|
||||||
};
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const initializer = (videos) => {
|
|
||||||
const initialState = {
|
|
||||||
videos: [],
|
|
||||||
seasons: [],
|
|
||||||
selectedSeason: null
|
|
||||||
};
|
|
||||||
const initAction = {
|
|
||||||
type: 'videos-changed',
|
|
||||||
videos
|
|
||||||
};
|
|
||||||
|
|
||||||
return reducer(initialState, initAction);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSelectableSeasons = (videos) => {
|
|
||||||
const [state, dispatch] = React.useReducer(
|
|
||||||
reducer,
|
|
||||||
videos,
|
|
||||||
initializer
|
|
||||||
);
|
|
||||||
const selectSeason = React.useCallback((season) => {
|
|
||||||
dispatch({
|
|
||||||
type: 'season-changed',
|
|
||||||
season
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
const videosForSeason = React.useMemo(() => {
|
|
||||||
return state.videos
|
|
||||||
.filter((video) => {
|
|
||||||
return state.selectedSeason === null || video.season === state.selectedSeason;
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
return a.episode - b.episode;
|
|
||||||
});
|
|
||||||
}, [state.videos, state.selectedSeason]);
|
|
||||||
React.useEffect(() => {
|
|
||||||
dispatch({
|
|
||||||
type: 'videos-changed',
|
|
||||||
videos
|
|
||||||
});
|
|
||||||
}, [videos]);
|
|
||||||
return [state.seasons, state.selectedSeason, videosForSeason, selectSeason];
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = useSelectableSeasons;
|
|
||||||
|
|
@ -7,6 +7,11 @@
|
||||||
meta-info-container: meta-info-container;
|
meta-info-container: meta-info-container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:import('~stremio/common/ModalDialog/styles.less') {
|
||||||
|
modal-dialog-container: modal-dialog-container;
|
||||||
|
title-modal-container: title-container;
|
||||||
|
}
|
||||||
|
|
||||||
.metadetails-container {
|
.metadetails-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -104,4 +109,20 @@
|
||||||
background-color: @color-background-dark5-70;
|
background-color: @color-background-dark5-70;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-extension-modal-container {
|
||||||
|
.modal-dialog-container {
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
|
||||||
|
.title-modal-container {
|
||||||
|
max-height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-extension-modal-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (C) 2017-2020 Smart code 203358507
|
// Copyright (C) 2017-2020 Smart code 203358507
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { deepLinking, useModelState } = require('stremio/common');
|
const { CONSTANTS, deepLinking, useModelState } = require('stremio/common');
|
||||||
|
|
||||||
const initMetaDetailsState = () => ({
|
const initMetaDetailsState = () => ({
|
||||||
selected: null,
|
selected: null,
|
||||||
|
|
@ -12,10 +12,10 @@ const initMetaDetailsState = () => ({
|
||||||
const mapMetaDetailsStateWithCtx = (meta_details, ctx) => {
|
const mapMetaDetailsStateWithCtx = (meta_details, ctx) => {
|
||||||
const selected = meta_details.selected;
|
const selected = meta_details.selected;
|
||||||
const meta_resources = meta_details.meta_resources.map((meta_resource) => {
|
const meta_resources = meta_details.meta_resources.map((meta_resource) => {
|
||||||
return {
|
return meta_resource.content.type === 'Ready' ?
|
||||||
request: meta_resource.request,
|
{
|
||||||
content: meta_resource.content.type === 'Ready' ?
|
request: meta_resource.request,
|
||||||
{
|
content: {
|
||||||
type: 'Ready',
|
type: 'Ready',
|
||||||
content: {
|
content: {
|
||||||
...meta_resource.content.content,
|
...meta_resource.content.content,
|
||||||
|
|
@ -33,34 +33,27 @@ const mapMetaDetailsStateWithCtx = (meta_details, ctx) => {
|
||||||
:
|
:
|
||||||
NaN
|
NaN
|
||||||
),
|
),
|
||||||
|
upcoming: Date.parse(video.released) > Date.now(),
|
||||||
// TODO add watched and progress
|
// TODO add watched and progress
|
||||||
deepLinks: deepLinking.withVideo({
|
deepLinks: deepLinking.withVideo({
|
||||||
video,
|
video,
|
||||||
metaTransportUrl: meta_resource.request.base,
|
metaTransportUrl: meta_resource.request.base,
|
||||||
metaItem: meta_resource.content.content
|
metaItem: meta_resource.content.content
|
||||||
})
|
})
|
||||||
}))
|
})),
|
||||||
|
metaExtensions: meta_resource.content.content.links.filter((link) => link.category === CONSTANTS.META_LINK_CATEGORY)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addon: ctx.profile.addons.reduce((result, addon) => {
|
||||||
|
if (addon.transportUrl === meta_resource.request.base) {
|
||||||
|
return addon;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
:
|
|
||||||
meta_resource.content,
|
|
||||||
deepLinks: meta_details.selected !== null ?
|
|
||||||
deepLinking.withMetaResource({
|
|
||||||
metaResource: meta_resource,
|
|
||||||
type: meta_details.selected.meta_resource_ref.type_name,
|
|
||||||
id: meta_details.selected.meta_resource_ref.id,
|
|
||||||
videoId: meta_details.selected.streams_resource_ref !== null ? meta_details.selected.streams_resource_ref.id : null
|
|
||||||
})
|
|
||||||
:
|
|
||||||
null,
|
|
||||||
addon: ctx.profile.addons.reduce((result, addon) => {
|
|
||||||
if (addon.transportUrl === meta_resource.request.base) {
|
|
||||||
return addon;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, null)
|
}, null)
|
||||||
};
|
}
|
||||||
|
:
|
||||||
|
meta_resource;
|
||||||
});
|
});
|
||||||
const streams_resources = meta_details.streams_resources.map((stream_resource) => {
|
const streams_resources = meta_details.streams_resources.map((stream_resource) => {
|
||||||
return stream_resource.content.type === 'Ready' ?
|
return stream_resource.content.type === 'Ready' ?
|
||||||
|
|
|
||||||
29
src/routes/MetaDetails/useMetaExtensions.js
Normal file
29
src/routes/MetaDetails/useMetaExtensions.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright (C) 2017-2020 Smart code 203358507
|
||||||
|
|
||||||
|
const React = require('react');
|
||||||
|
|
||||||
|
const useMetaExtensions = (metaResources) => {
|
||||||
|
const tabs = React.useMemo(() => {
|
||||||
|
return metaResources.filter((metaResource) => metaResource.content.type === 'Ready' && metaResource.content.content.metaExtensions.length > 0 && metaResource.addon !== null)
|
||||||
|
.map((metaResource) => {
|
||||||
|
return metaResource.content.content.metaExtensions.map((metaExtension) => (
|
||||||
|
{
|
||||||
|
id: metaExtension.url,
|
||||||
|
label: metaResource.addon.manifest.name,
|
||||||
|
logo: metaResource.addon.manifest.logo,
|
||||||
|
icon: 'ic_addons',
|
||||||
|
onClick: () => { setSelectedMetaExtension(metaExtension); }
|
||||||
|
}
|
||||||
|
));
|
||||||
|
})
|
||||||
|
.flat(2)
|
||||||
|
.filter((tab, index, tabs) => tabs.findIndex((_tab) => _tab.id === tab.id) === index);
|
||||||
|
}, [metaResources]);
|
||||||
|
const [selectedMetaExtension, setSelectedMetaExtension] = React.useState(null);
|
||||||
|
const clearSelectedMetaExtension = React.useCallback(() => {
|
||||||
|
setSelectedMetaExtension(null);
|
||||||
|
}, []);
|
||||||
|
return { tabs, selectedMetaExtension, clearSelectedMetaExtension };
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = useMetaExtensions;
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
// Copyright (C) 2017-2020 Smart code 203358507
|
|
||||||
|
|
||||||
const { renderHook, act } = require('@testing-library/react-hooks');
|
|
||||||
const useSelectableSeasons = require('../src/routes/MetaDetails/VideosList/useSelectableSeasons');
|
|
||||||
|
|
||||||
const videos = [{ 'season': 4 }, { 'season': 5 }, { 'season': 4 }, { 'season': 7 }];
|
|
||||||
|
|
||||||
describe('hooks tests', () => {
|
|
||||||
describe('useSelectableSeasons hook', () => {
|
|
||||||
it('match 4', async () => {
|
|
||||||
const { result } = renderHook(() => useSelectableSeasons(videos));
|
|
||||||
const [, selectedSeason] = result.current;
|
|
||||||
expect(selectedSeason).toBe(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('match 5', async () => {
|
|
||||||
const { result } = renderHook(() => useSelectableSeasons(videos));
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
const [, , , selectSeason] = result.current;
|
|
||||||
selectSeason(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
const [, selectedSeason] = result.current;
|
|
||||||
expect(selectedSeason).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('not match 6', async () => {
|
|
||||||
const { result } = renderHook(() => useSelectableSeasons(videos));
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
const [, , , selectSeason] = result.current;
|
|
||||||
selectSeason(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
const [, selectedSeason] = result.current;
|
|
||||||
expect(selectedSeason).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('not match $', async () => {
|
|
||||||
const { result } = renderHook(() => useSelectableSeasons(videos));
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
const [, , , selectSeason] = result.current;
|
|
||||||
selectSeason('$');
|
|
||||||
});
|
|
||||||
|
|
||||||
const [, selectedSeason] = result.current;
|
|
||||||
expect(selectedSeason).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in a new issue