MetaDetails adapted to changes in core

This commit is contained in:
nklhrstv 2020-10-27 14:52:30 +02:00
parent d32fe5123a
commit 07398b56cc
8 changed files with 207 additions and 202 deletions

View file

@ -2,43 +2,30 @@
const React = require('react');
const PropTypes = require('prop-types');
const { VerticalNavBar, HorizontalNavBar, MetaPreview, ModalDialog, Image, useInLibrary } = require('stremio/common');
const { useServices } = require('stremio/services');
const { VerticalNavBar, HorizontalNavBar, MetaPreview, ModalDialog, Image } = require('stremio/common');
const StreamsList = require('./StreamsList');
const VideosList = require('./VideosList');
const useMetaDetails = require('./useMetaDetails');
const useMetaExtensions = require('./useMetaExtensions');
const useSeason = require('./useSeason');
const useMetaExtensionTabs = require('./useMetaExtensionTabs');
const styles = require('./styles');
const MetaDetails = ({ urlParams, queryParams }) => {
const { core } = useServices();
const metaDetails = useMetaDetails(urlParams);
const { tabs, selectedMetaExtension, clearSelectedMetaExtension } = useMetaExtensions(metaDetails.meta_resources);
const metaResourceRef = React.useMemo(() => {
return metaDetails.selected !== null ? metaDetails.selected.meta_resources_ref : null;
}, [metaDetails.selected]);
const selectedMetaResource = React.useMemo(() => {
return metaDetails.meta_resources.reduceRight((result, metaResource) => {
if (metaResource.content.type === 'Ready') {
return metaResource;
}
return result;
}, null);
}, [metaDetails]);
const streamsResourceRef = metaDetails.selected !== null ? metaDetails.selected.streams_resource_ref : null;
const streamsResources = metaDetails.streams_resources;
const seasonQueryParam = React.useMemo(() => {
return queryParams.has('season') && !isNaN(queryParams.get('season')) ?
parseInt(queryParams.get('season'))
const [season, setSeason] = useSeason(urlParams, queryParams);
const [tabs, metaExtension, clearMetaExtension] = useMetaExtensionTabs(metaDetails.metaExtensions);
const [metaPath, streamsPath] = React.useMemo(() => {
return metaDetails.selected !== null ?
[metaDetails.selected.metaPath, metaDetails.selected.streamsPath]
:
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(() => {
return streamsResourceRef !== null && selectedMetaResource !== null ?
selectedMetaResource.content.content.videos.reduce((result, video) => {
if (video.id === streamsResourceRef.id) {
[null, null];
}, [metaDetails.selected]);
const video = React.useMemo(() => {
return streamsPath !== null && metaDetails.metaCatalog !== null && metaDetails.metaCatalog.content.type === 'Ready' ?
metaDetails.metaCatalog.content.content.videos.reduce((result, video) => {
if (video.id === streamsPath.id) {
return video;
}
@ -46,14 +33,42 @@ const MetaDetails = ({ urlParams, queryParams }) => {
}, null)
:
null;
}, [selectedMetaResource, streamsResourceRef]);
const [inLibrary, toggleInLibrary] = useInLibrary(selectedMetaResource !== null ? selectedMetaResource.content.content : null);
}, [metaDetails.metaCatalog, streamsPath]);
const addToLibrary = React.useCallback(() => {
if (metaDetails.metaCatalog === null || metaDetails.metaCatalog.content.type !== 'Ready') {
return;
}
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'AddToLibrary',
args: metaDetails.metaCatalog.content.content
}
});
}, [metaDetails]);
const removeFromLibrary = React.useCallback(() => {
if (metaDetails.metaCatalog === null || metaDetails.metaCatalog.content.type !== 'Ready') {
return;
}
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'RemoveFromLibrary',
args: metaDetails.metaCatalog.content.content.id
}
});
}, [metaDetails]);
const seasonOnSelect = React.useCallback((event) => {
setSeason(event.value);
}, [setSeason]);
return (
<div className={styles['metadetails-container']}>
<HorizontalNavBar
className={styles['nav-bar']}
backButton={true}
title={selectedMetaResource !== null ? selectedMetaResource.content.content.name : null}
title={metaDetails.metaCatalog !== null && metaDetails.metaCatalog.content.type === 'Ready' ? metaDetails.metaCatalog.content.content.name : null}
/>
<div className={styles['metadetails-content']}>
{
@ -61,39 +76,41 @@ const MetaDetails = ({ urlParams, queryParams }) => {
<VerticalNavBar
className={styles['vertical-nav-bar']}
tabs={tabs}
selected={selectedMetaExtension !== null ? selectedMetaExtension.url : null}
selected={metaExtension !== null ? metaExtension.url : null}
/>
:
null
}
{
metaResourceRef === null ?
metaPath === null ?
<div className={styles['meta-message-container']}>
<Image className={styles['image']} src={'/images/empty.png'} alt={' '} />
<div className={styles['message-label']}>No meta was selected!</div>
</div>
:
metaDetails.meta_resources.length === 0 ?
metaDetails.metaCatalog === null ?
<div className={styles['meta-message-container']}>
<Image className={styles['image']} src={'/images/empty.png'} alt={' '} />
<div className={styles['message-label']}>No addons ware requested for this meta!</div>
</div>
:
metaDetails.meta_resources.every((metaResource) => metaResource.content.type === 'Err') ?
metaDetails.metaCatalog.content.type === 'Err' ?
<div className={styles['meta-message-container']}>
<Image className={styles['image']} src={'/images/empty.png'} alt={' '} />
<div className={styles['message-label']}>No metadata was found!</div>
</div>
:
selectedMetaResource !== null ?
metaDetails.metaCatalog.content.type === 'Loading' ?
<MetaPreview.Placeholder className={styles['meta-preview']} />
:
<React.Fragment>
{
typeof selectedMetaResource.content.content.background === 'string' &&
selectedMetaResource.content.content.background.length > 0 ?
typeof metaDetails.metaCatalog.content.content.background === 'string' &&
metaDetails.metaCatalog.content.content.background.length > 0 ?
<div className={styles['background-image-layer']}>
<Image
className={styles['background-image']}
src={selectedMetaResource.content.content.background}
src={metaDetails.metaCatalog.content.content.background}
alt={' '}
/>
</div>
@ -102,39 +119,37 @@ const MetaDetails = ({ urlParams, queryParams }) => {
}
<MetaPreview
className={styles['meta-preview']}
name={selectedMetaResource.content.content.name + (selectedVideo !== null && typeof selectedVideo.title === 'string' ? ` - ${selectedVideo.title}` : '')}
logo={selectedMetaResource.content.content.logo}
runtime={selectedMetaResource.content.content.runtime}
releaseInfo={selectedMetaResource.content.content.releaseInfo}
released={selectedMetaResource.content.content.released}
name={metaDetails.metaCatalog.content.content.name + (video !== null && typeof video.title === 'string' && video.title.length > 0 ? ` - ${video.title}` : '')}
logo={metaDetails.metaCatalog.content.content.logo}
runtime={metaDetails.metaCatalog.content.content.runtime}
releaseInfo={metaDetails.metaCatalog.content.content.releaseInfo}
released={metaDetails.metaCatalog.content.content.released}
description={
selectedVideo !== null && typeof selectedVideo.overview === 'string' && selectedVideo.overview.length > 0 ?
selectedVideo.overview
video !== null && typeof video.overview === 'string' && video.overview.length > 0 ?
video.overview
:
selectedMetaResource.content.content.description
metaDetails.metaCatalog.content.content.description
}
links={selectedMetaResource.content.content.links}
trailerStreams={selectedMetaResource.content.content.trailerStreams}
inLibrary={inLibrary}
toggleInLibrary={toggleInLibrary}
links={metaDetails.metaCatalog.content.content.links}
trailerStreams={metaDetails.metaCatalog.content.content.trailerStreams}
inLibrary={metaDetails.metaCatalog.content.content.inLibrary}
toggleInLibrary={metaDetails.metaCatalog.content.content.inLibrary ? removeFromLibrary : addToLibrary}
/>
</React.Fragment>
:
<MetaPreview.Placeholder className={styles['meta-preview']} />
}
<div className={styles['spacing']} />
{
streamsResourceRef !== null ?
streamsPath !== null ?
<StreamsList
className={styles['streams-list']}
streamsResources={streamsResources}
streamsCatalogs={metaDetails.streamsCatalogs}
/>
:
metaResourceRef !== null ?
metaPath !== null ?
<VideosList
className={styles['videos-list']}
metaResource={selectedMetaResource}
season={seasonQueryParam}
metaCatalog={metaDetails.metaCatalog}
season={season}
seasonOnSelect={seasonOnSelect}
/>
:
@ -142,15 +157,15 @@ const MetaDetails = ({ urlParams, queryParams }) => {
}
</div>
{
selectedMetaExtension !== null ?
metaExtension !== null ?
<ModalDialog
className={styles['meta-extension-modal-container']}
title={selectedMetaExtension.name}
onCloseRequest={clearSelectedMetaExtension}>
title={metaExtension.name}
onCloseRequest={clearMetaExtension}>
<iframe
className={styles['meta-extension-modal-iframe']}
sandbox={'allow-forms allow-scripts allow-same-origin'}
src={selectedMetaExtension.url}
src={metaExtension.url}
/>
</ModalDialog>
:

View file

@ -8,38 +8,48 @@ const { Button, Image } = require('stremio/common');
const Stream = require('./Stream');
const styles = require('./styles');
const StreamsList = ({ className, streamsResources }) => {
const StreamsList = ({ className, streamsCatalogs }) => {
const streams = React.useMemo(() => {
return streamsResources
.filter((streamsResource) => streamsResource.content.type === 'Ready')
.map((streamsResource) => streamsResource.content.content)
return streamsCatalogs
.filter((catalog) => catalog.content.type === 'Ready')
.map((catalog) => ({
...catalog.content.content,
addonName: catalog.addonName
}))
.flat(1);
}, [streamsResources]);
}, [streamsCatalogs]);
return (
<div className={classnames(className, styles['streams-list-container'])}>
{
streamsResources.length === 0 ?
streamsCatalogs.length === 0 ?
<div className={styles['message-container']}>
<Image className={styles['image']} src={'/images/empty.png'} alt={' '} />
<div className={styles['label']}>No addons were requested for streams!</div>
</div>
:
streamsResources.every((streamsResource) => streamsResource.content.type === 'Err') ?
streamsCatalogs.every((streamsResource) => streamsResource.content.type === 'Err') ?
<div className={styles['message-container']}>
<Image className={styles['image']} src={'/images/empty.png'} alt={' '} />
<div className={styles['label']}>No streams were found!</div>
</div>
:
streams.length > 0 ?
streams.length === 0 ?
<div className={styles['streams-container']}>
{streams.map((stream, index) => (
<Stream {...stream} key={index} />
))}
<Stream.Placeholder />
<Stream.Placeholder />
</div>
:
<div className={styles['streams-container']}>
<Stream.Placeholder />
<Stream.Placeholder />
{streams.map((stream, index) => (
<Stream
key={index}
addonName={stream.addonName}
title={stream.title}
thumbnail={stream.thumbnail}
progress={stream.progress}
deepLinks={stream.deepLinks}
/>
))}
</div>
}
<Button className={styles['install-button-container']} title={'Install Addons'} href={'#/addons'}>
@ -52,7 +62,7 @@ const StreamsList = ({ className, streamsResources }) => {
StreamsList.propTypes = {
className: PropTypes.string,
streamsResources: PropTypes.arrayOf(PropTypes.object)
streamsCatalogs: PropTypes.arrayOf(PropTypes.object).isRequired
};
module.exports = StreamsList;

View file

@ -8,7 +8,7 @@ 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, deepLinks, ...props }) => {
const Video = ({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, scheduled, deepLinks, ...props }) => {
const href = React.useMemo(() => {
return deepLinks ?
typeof deepLinks.player === 'string' ?
@ -53,7 +53,12 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
{released.toLocaleString(undefined, { year: '2-digit', month: 'short', day: 'numeric' })}
</div>
:
null
scheduled ?
<div className={styles['released-container']} title={'To be announced'}>
TBA
</div>
:
null
}
<div className={styles['upcoming-watched-container']}>
{
@ -99,6 +104,7 @@ Video.propTypes = {
upcoming: PropTypes.bool,
watched: PropTypes.bool,
progress: PropTypes.number,
scheduled: PropTypes.bool,
deepLinks: PropTypes.shape({
meta_details_streams: PropTypes.string,
player: PropTypes.string

View file

@ -9,13 +9,13 @@ const SeasonsBar = require('./SeasonsBar');
const Video = require('./Video');
const styles = require('./styles');
const VideosList = ({ className, metaResource, season, seasonOnSelect }) => {
const VideosList = ({ className, metaCatalog, season, seasonOnSelect }) => {
const videos = React.useMemo(() => {
return metaResource && metaResource.content.type === 'Ready' ?
metaResource.content.content.videos
return metaCatalog && metaCatalog.content.type === 'Ready' ?
metaCatalog.content.content.videos
:
[];
}, [metaResource]);
}, [metaCatalog]);
const seasons = React.useMemo(() => {
return videos
.map(({ season }) => season)
@ -52,7 +52,7 @@ const VideosList = ({ className, metaResource, season, seasonOnSelect }) => {
return (
<div className={classnames(className, styles['videos-list-container'])}>
{
!metaResource || metaResource.content.type === 'Loading' ?
!metaCatalog || metaCatalog.content.type === 'Loading' ?
<React.Fragment>
<SeasonsBar.Placeholder className={styles['seasons-bar']} />
<SearchBar.Placeholder className={styles['search-bar']} title={'Search videos'} />
@ -65,7 +65,7 @@ const VideosList = ({ className, metaResource, season, seasonOnSelect }) => {
</div>
</React.Fragment>
:
metaResource.content.type === 'Err' || videosForSeason.length === 0 ?
metaCatalog.content.type === 'Err' || videosForSeason.length === 0 ?
<div className={styles['message-container']}>
<Image className={styles['image']} src={'/images/empty.png'} alt={' '} />
<div className={styles['label']}>No videos found for this meta!</div>
@ -96,11 +96,23 @@ const VideosList = ({ className, metaResource, season, seasonOnSelect }) => {
return search.length === 0 ||
(
(typeof video.title === 'string' && video.title.toLowerCase().includes(search.toLowerCase())) ||
(video.released.toLocaleString(undefined, { year: '2-digit', month: 'short', day: 'numeric' }).toLowerCase().includes(search.toLowerCase()))
(!isNaN(video.released.getTime()) && video.released.toLocaleString(undefined, { year: '2-digit', month: 'short', day: 'numeric' }).toLowerCase().includes(search.toLowerCase()))
);
})
.map((video, index) => (
<Video {...video} key={index} />
<Video
key={index}
id={video.id}
title={video.title}
thumbnail={video.thumbnail}
episode={video.episode}
released={video.released}
upcoming={video.upcoming}
watched={video.watched}
progress={video.progress}
deepLinks={video.deepLinks}
scheduled={video.scheduled}
/>
))
}
</div>
@ -112,7 +124,7 @@ const VideosList = ({ className, metaResource, season, seasonOnSelect }) => {
VideosList.propTypes = {
className: PropTypes.string,
metaResource: PropTypes.object,
metaCatalog: PropTypes.object,
season: PropTypes.number,
seasonOnSelect: PropTypes.func
};

View file

@ -1,111 +1,64 @@
// Copyright (C) 2017-2020 Smart code 203358507
const React = require('react');
const { CONSTANTS, deepLinking, useModelState } = require('stremio/common');
const { useModelState } = require('stremio/common');
const initMetaDetailsState = () => ({
const init = () => ({
selected: null,
meta_resources: [],
streams_resources: []
metaCatalog: null,
streamsCatalogs: [],
metaExtensions: []
});
const mapMetaDetailsStateWithCtx = (meta_details, ctx) => {
const selected = meta_details.selected;
const meta_resources = meta_details.meta_resources.map((meta_resource) => {
return meta_resource.content.type === 'Ready' ?
{
request: meta_resource.request,
const map = (metaDetails) => ({
...metaDetails,
metaCatalog: metaDetails.metaCatalog !== null && metaDetails.metaCatalog.content.type === 'Ready' ?
{
...metaDetails.metaCatalog,
content: {
...metaDetails.metaCatalog.content,
content: {
type: 'Ready',
content: {
...meta_resource.content.content,
...metaDetails.metaCatalog.content.content,
released: new Date(
typeof metaDetails.metaCatalog.content.content.released === 'string' ?
metaDetails.metaCatalog.content.content.released
:
NaN
),
videos: metaDetails.metaCatalog.content.content.videos.map((video) => ({
...video,
released: new Date(
typeof meta_resource.content.content.released === 'string' ?
meta_resource.content.content.released
typeof video.released === 'string' ?
video.released
:
NaN
),
videos: meta_resource.content.content.videos.map((video) => ({
...video,
released: new Date(
typeof video.released === 'string' ?
video.released
:
NaN
),
upcoming: Date.parse(video.released) > Date.now(),
// TODO add watched and progress
deepLinks: deepLinking.withVideo({
video,
metaTransportUrl: meta_resource.request.base,
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;
}
return result;
}, null)
}
:
meta_resource;
});
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 };
};
}
:
metaDetails.metaCatalog
});
const useMetaDetails = (urlParams) => {
const loadMetaDetailsAction = React.useMemo(() => {
const action = React.useMemo(() => {
if (typeof urlParams.type === 'string' && typeof urlParams.id === 'string') {
return {
action: 'Load',
args: {
model: 'MetaDetails',
args: {
meta_resource_ref: {
metaPath: {
resource: 'meta',
type_name: urlParams.type,
type: urlParams.type,
id: urlParams.id,
extra: []
},
streams_resource_ref: typeof urlParams.videoId === 'string' ?
streamsPath: typeof urlParams.videoId === 'string' ?
{
resource: 'stream',
type_name: urlParams.type,
type: urlParams.type,
id: urlParams.videoId,
extra: []
}
@ -120,12 +73,7 @@ const useMetaDetails = (urlParams) => {
};
}
}, [urlParams]);
return useModelState({
model: 'meta_details',
action: loadMetaDetailsAction,
mapWithCtx: mapMetaDetailsStateWithCtx,
init: initMetaDetailsState
});
return useModelState({ model: 'meta_details', action, map, init });
};
module.exports = useMetaDetails;

View file

@ -0,0 +1,23 @@
// Copyright (C) 2017-2020 Smart code 203358507
const React = require('react');
const useMetaExtensionTabs = (metaExtensions) => {
const tabs = React.useMemo(() => {
return metaExtensions
.map((extension) => ({
id: extension.url,
label: extension.addon.manifest.name,
logo: extension.addon.manifest.logo,
icon: 'ic_addons',
onClick: () => setSelected(extension)
}));
}, [metaExtensions]);
const [selected, setSelected] = React.useState(null);
const clear = React.useCallback(() => {
setSelected(null);
}, []);
return [tabs, selected, clear];
};
module.exports = useMetaExtensionTabs;

View file

@ -1,29 +0,0 @@
// 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;

View file

@ -0,0 +1,20 @@
// Copyright (C) 2017-2020 Smart code 203358507
const React = require('react');
const useSeason = (urlParams, queryParams) => {
const season = React.useMemo(() => {
return queryParams.has('season') && !isNaN(queryParams.get('season')) ?
parseInt(queryParams.get('season'))
:
null;
}, [queryParams]);
const setSeason = React.useCallback((season) => {
const nextQueryParams = new URLSearchParams(queryParams);
nextQueryParams.set('season', season);
window.location.replace(`#${urlParams.path}?${nextQueryParams}`);
}, [urlParams, queryParams]);
return [season, setSeason];
};
module.exports = useSeason;