Discover major refactor

This commit is contained in:
NikolaBorislavovHristov 2019-06-03 19:36:22 +03:00
parent 8cc0d76792
commit 29677ec69b
4 changed files with 196 additions and 183 deletions

View file

@ -1,134 +1,43 @@
const React = require('react');
const classnames = require('classnames');
const { Input } = require('stremio-navigation');
const Icon = require('stremio-icons/dom');
const { MainNavBar, MetaItem, MetaPreview, Popup, useBinaryState } = require('stremio-common');
const { MainNavBar, MetaItem, MetaPreview } = require('stremio-common');
const PickerMenu = require('./PickerMenu');
const useCatalog = require('./useCatalog');
const styles = require('./styles');
// TODO impl refocus to left of the scroll view
const Discover = ({ urlParams }) => {
const catalog = useCatalog(urlParams);
const [typePickerOpen, typePickerOnOpen, typePickerOnClose] = useBinaryState(false);
const [catalogPickerOpen, catalogPickerOnOpen, catalogPickerOnClose] = useBinaryState(false);
const [categoryPickerOpen, categoryPickerOnOpen, categoryPickerOnClose] = useBinaryState(false);
React.useEffect(() => {
if (typeof urlParams.type !== 'string' || typeof urlParams.catalog !== 'string') {
const type = urlParams.type || 'movie';
const catalog = urlParams.catalog || 'com.linvo.cinemeta:top';
const category = urlParams.category || '';
window.location.replace(`#/discover/${type}/${catalog}/${category}`);
}
}, [urlParams.type, urlParams.catalog]);
const Discover = ({ urlParams, queryParams }) => {
const [pickers, metaItems] = useCatalog(urlParams, queryParams);
const [selectedItem, setSelectedItem] = React.useState(metaItems[0]);
const changeMetaItem = React.useCallback((event) => {
const metaItem = metaItems.find(({ id }) => id === event.currentTarget.dataset.metaItemId);
setSelectedItem(metaItem);
}, [metaItems]);
return (
<div className={styles['discover-container']}>
<MainNavBar className={styles['nav-bar']} />
{
typeof urlParams.type === 'string' || typeof urlParams.catalog === 'string' ?
<div className={styles['discover-content']}>
<div className={styles['pickers-container']}>
<Popup onOpen={typePickerOnOpen} onClose={typePickerOnClose}>
<Popup.Label>
<Input className={classnames(styles['picker-button'], styles['types-picker-button'], { 'active': typePickerOpen }, 'focusable-with-border')} type={'button'}>
<div className={styles['picker-label']}>{urlParams.type}</div>
<Icon className={styles['picker-icon']} icon={'ic_arrow_down'} />
</Input>
</Popup.Label>
<Popup.Menu className={styles['menu-layer']}>
<div className={classnames(styles['menu-items-container'], styles['menu-types-container'])}>
<Input className={classnames(styles['menu-item-container'], 'focusable-with-border')} type={'button'}>
<div className={styles['menu-label']}>movie</div>
</Input>
<Input className={classnames(styles['menu-item-container'], 'focusable-with-border')} type={'button'}>
<div className={styles['menu-label']}>series</div>
</Input>
<Input className={classnames(styles['menu-item-container'], 'focusable-with-border')} type={'button'}>
<div className={styles['menu-label']}>channel</div>
</Input>
<Input className={classnames(styles['menu-item-container'], 'focusable-with-border')} type={'button'}>
<div className={styles['menu-label']}>TV channels</div>
</Input>
</div>
</Popup.Menu>
</Popup>
<Popup onOpen={catalogPickerOnOpen} onClose={catalogPickerOnClose}>
<Popup.Label>
<Input className={classnames(styles['picker-button'], { 'active': catalogPickerOpen }, 'focusable-with-border')} type={'button'}>
<div className={styles['picker-label']}>{urlParams.catalog}</div>
<Icon className={styles['picker-icon']} icon={'ic_arrow_down'} />
</Input>
</Popup.Label>
<Popup.Menu className={styles['menu-layer']}>
<div className={styles['menu-items-container']}>
<Input className={classnames(styles['menu-item-container'], 'focusable-with-border')} type={'button'}>
<div className={styles['menu-label']}>catalog1</div>
</Input>
<Input className={classnames(styles['menu-item-container'], 'focusable-with-border')} type={'button'}>
<div className={styles['menu-label']}>catalog2</div>
</Input>
</div>
</Popup.Menu>
</Popup>
<Popup onOpen={categoryPickerOnOpen} onClose={categoryPickerOnClose}>
<Popup.Label>
<Input className={classnames(styles['picker-button'], { 'active': categoryPickerOpen }, 'focusable-with-border')} type={'button'}>
<div className={styles['picker-label']}>{urlParams.category !== null ? urlParams.category : 'Select category'}</div>
<Icon className={styles['picker-icon']} icon={'ic_arrow_down'} />
</Input>
</Popup.Label>
<Popup.Menu className={styles['menu-layer']}>
<div className={styles['menu-items-container']}>
<Input className={classnames(styles['menu-item-container'], 'focusable-with-border')} type={'button'}>
<div className={styles['menu-label']}>category1</div>
</Input>
<Input className={classnames(styles['menu-item-container'], 'focusable-with-border')} type={'button'}>
<div className={styles['menu-label']}>category2</div>
</Input>
</div>
</Popup.Menu>
</Popup>
<div className={styles['discover-content']}>
<div className={styles['pickers-container']}>
{pickers.map((picker) => (
<PickerMenu {...picker} key={picker.name} />
))}
</div>
<div className={styles['meta-items-container']} tabIndex={-1}>
{metaItems.map((metaItem) => (
<div key={metaItem.id} className={styles['meta-item-container']}>
<MetaItem
{...metaItem}
className={styles['meta-item']}
onClick={changeMetaItem}
/>
</div>
<div className={styles['meta-items-container']} tabIndex={-1}>
{catalog.map(({ id, type, name, posterShape }) => (
<div key={id} className={styles['meta-item-container']}>
<MetaItem
className={styles['meta-item']}
id={id}
type={type}
name={name}
posterShape={posterShape}
/>
</div>
))}
</div>
<MetaPreview
className={styles['meta-preview-container']}
compact={true}
id={'tt0117951'}
type={'movie'}
name={'Trainspotting'}
logo={'https://s3.dexerto.com/thumbnails/_thumbnailLarge/Pewdiepie-overtaken-by-t-series.jpg'}
logo={'https://images.metahub.space/logo/medium/tt0117951/img'}
background={'https://www.bfi.org.uk/sites/bfi.org.uk/files/styles/full/public/image/trainspotting-1996-008-ewan-bremner-ewan-mcgregor-robert-carlyle-00m-m63.jpg?itok=tmpxRcqP'}
duration={'93 min'}
releaseInfo={'1996'}
released={'1996-08-09T00:00:00.000Z'}
description={'Renton, deeply immersed in the Edinburgh drug scene, tries to clean up and get out, despite the allure of the drugs and influence of friends. gg'}
genres={['action', 'drama', 'drama', 'drama', 'drama', 'drama', 'drama', 'drama', 'drama']}
writers={['Ewan McGregor', 'Ewen Bremner', 'Jonny Lee Miller', 'Kevin McKidd', 'Kevin McKidd', 'Kevin McKidd', 'Kevin McKidd', 'Kevin McKidd', 'Kevin McKidd']}
directors={['Ewan McGregor', 'Ewen Bremner', 'Jonny Lee Miller', 'Kevin McKidd', 'Kevin McKidd', 'Kevin McKidd', 'Kevin McKidd', 'Kevin McKidd', 'Kevin McKidd']}
cast={['Ewan McGregor', 'Ewen Bremner', 'Jonny Lee Miller', 'Kevin McKidd', 'Kevin McKidd', 'Kevin McKidd', 'Kevin McKidd', 'Kevin McKidd', 'Kevin McKidd']}
imdbId={'tt0117951'}
imdbRating={'8.2'}
trailer={'encodedStream'}
inLibrary={true}
share={'share_url'}
toggleIsInLibrary={() => { }}
/>
</div>
:
null
}
))}
</div>
<MetaPreview
className={styles['meta-preview-container']}
compact={true}
{...selectedItem}
/>
</div>
</div>
);
};

View file

@ -0,0 +1,49 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const Icon = require('stremio-icons/dom');
const { Input } = require('stremio-navigation');
const { Popup, useBinaryState } = require('stremio-common');
const styles = require('./styles');
// TODO support optionsLimit
const PickerMenu = ({ name, value, options, toggle }) => {
const [open, onOpen, onClose] = useBinaryState(false);
const label = typeof value === 'string' ? value : name;
return (
<Popup onOpen={onOpen} onClose={onClose}>
<Popup.Label>
<Input className={classnames(styles['picker-button'], { 'active': open }, 'focusable-with-border')} title={label} type={'button'}>
<div className={classnames(styles['picker-label'], { [styles['capitalized']]: name === 'type' })}>{label}</div>
<Icon className={styles['picker-icon']} icon={'ic_arrow_down'} />
</Input>
</Popup.Label>
<Popup.Menu className={styles['menu-layer']}>
<div className={styles['menu-container']}>
{
Array.isArray(options) ?
options.map(({ value, label }) => (
<Input key={value} className={classnames(styles['menu-item-container'], 'focusable-with-border')} title={label} data-name={name} data-value={value} type={'button'} onClick={toggle}>
<div className={classnames(styles['menu-label'], { [styles['capitalized']]: name === 'type' })}>{label}</div>
</Input>
))
:
null
}
</div>
</Popup.Menu>
</Popup>
);
};
PickerMenu.propTypes = {
name: PropTypes.string,
value: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.string,
label: PropTypes.string
})),
toggle: PropTypes.func
};
module.exports = PickerMenu;

View file

@ -20,29 +20,23 @@
align-self: stretch;
display: grid;
grid-template-columns: 1fr 26em;
grid-template-rows: 4.6em 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"picker-area meta-preview-area"
"pickers-area meta-preview-area"
"meta-items-area meta-preview-area";
overflow: hidden;
.pickers-container {
grid-area: picker-area;
display: flex;
flex-direction: row;
align-items: stretch;
padding: 0.8em;
grid-area: pickers-area;
padding: 0.4em;
.picker-button {
flex-grow: 0;
flex-shrink: 1;
flex-basis: 16em;
display: flex;
width: 16em;
height: 3em;
margin: 0.4em;
padding: 0 0.8em;
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-right: 0.8em;
padding: 0 0.8em;
background-color: var(--color-backgroundlighter);
cursor: pointer;
@ -62,20 +56,16 @@
}
}
&.types-picker-button {
.picker-label {
text-transform: capitalize;
}
}
.picker-label {
flex: 1;
font-size: 1.1em;
line-height: 1.1em;
max-height: 2.2em;
line-height: 1.2em;
max-height: 2.4em;
color: var(--color-surfacelighter);
word-break: break-all;
word-break: break-word;
&.capitalized {
text-transform: capitalize;
}
}
.picker-icon {
@ -90,31 +80,17 @@
.meta-items-container {
grid-area: meta-items-area;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-content: flex-start;
flex-wrap: wrap;
padding: 0 0.4em;
overflow-x: hidden;
overflow-y: auto;
&::after {
width: 100%;
height: 0.4em;
display: block;
content: "";
}
.meta-item-container {
flex-grow: 0;
flex-shrink: 0;
flex-basis: calc(100% / var(--items-per-row));
display: inline-block;
width: calc(100% / var(--items-per-row));
padding: 0.4em;
.meta-item {
width: 100%;
height: 100%;
}
}
}
@ -130,22 +106,11 @@
--border-color: var(--color-backgroundlighter40);
--box-shadow: -0.6em 0.6em 0.5em -0.1em var(--color-backgrounddark40);
.menu-items-container {
.menu-container {
width: 16em;
max-height: 18em;
overflow-y: auto;
background-color: var(--color-backgroundlighter);
&.menu-types-container {
.menu-item-container {
.menu-label {
text-transform: capitalize;
}
}
}
.menu-item-container {
width: 100%;
padding: 0.8em;
cursor: pointer;
@ -154,13 +119,14 @@
}
.menu-label {
line-height: 1.1em;
max-height: 2.2em;
line-height: 1.2em;
max-height: 2.4em;
color: var(--color-surfacelighter);
word-break: break-all;
word-break: break-word;
&.capitalized {
text-transform: capitalize;
}
}
}
}
}

View file

@ -1,15 +1,104 @@
const React = require('react');
const useCatalog = (addonId, catalogId, extra) => {
return React.useMemo(() => {
const useCatalog = (urlParams, queryParams) => {
const query = new URLSearchParams(queryParams).toString();
const [addon, catalog] = React.useMemo(() => {
// TODO impl this logic to stremio-core for code-reuse:
// TODO use type if it is part of user's addons
// TODO fallback to first type from user's addons
// TODO find catalog for addonId, catalogId and type
// TODO fallback to first catalog for the type from user's catalogs
const addon = {
id: 'com.linvo.cinemeta',
version: '2.11.0',
name: 'Cinemeta'
};
const catalog = {
id: 'top',
type: 'movie',
name: 'Top',
extra: [
{
name: 'genre',
isRequired: false,
options: ['Action', 'drama', 'Boring']
},
{
name: 'year',
isRequired: false,
options: ['2017', '2016', '2015']
}
]
};
return [addon, catalog];
}, [urlParams.type, urlParams.catalog]);
const pickers = React.useMemo(() => {
const replaceType = (event) => {
const { value } = event.currentTarget.dataset;
const query = new URLSearchParams(queryParams);
window.location = `#/discover/${value}/${addon.id}:${catalog.id}?${query}`;
};
const replaceCatalog = (event) => {
const { value } = event.currentTarget.dataset;
const query = new URLSearchParams(queryParams);
window.location = `#/discover/${catalog.type}/${value}?${query}`;
};
const replaceQueryParam = (event) => {
const { name, value } = event.currentTarget.dataset;
const query = new URLSearchParams({ ...queryParams, [name]: value });
window.location = `#/discover/${catalog.type}/${addon.id}:${catalog.id}?${query}`;
};
const requiredPickers = [
{
name: 'type',
value: catalog.type,
options: [
{ value: 'movie', label: 'movie' },
{ value: 'series', label: 'series' },
{ value: 'channels', label: 'channels' },
{ value: 'games', label: 'games' }
],
toggle: replaceType
},
{
name: 'catalog',
value: catalog.name,
options: [
{ value: 'com.linvo.cinemeta:top', label: 'Top' },
{ value: 'com.linvo.cinemeta:year', label: 'By year' }
],
toggle: replaceCatalog
}
];
const extraPickers = catalog.extra
.filter((extra) => {
return extra.name !== 'skip' && extra.name !== 'search';
})
.map((extra) => ({
...extra,
toggle: replaceQueryParam,
options: extra.options.map((option) => ({ value: option, label: option })),
value: extra.options.includes(queryParams[extra.name]) ?
queryParams[extra.name]
:
extra.isRequired ?
extra.options[0]
:
null
}));
return requiredPickers.concat(extraPickers);
}, [addon, catalog, query]);
const items = React.useMemo(() => {
return Array(303).fill(null).map((_, index) => ({
id: `tt${index}`,
type: 'movie',
name: 'Stremio demo item',
name: `Stremio demo item${index}`,
poster: `https://dummyimage.com/300x400/000/0011ff.jpg&text=${index + 1}`,
logo: `https://dummyimage.com/300x400/000/0011ff.jpg&text=${index + 1}`,
posterShape: 'poster'
}));
}, []);
return [pickers, items];
};
module.exports = useCatalog;