mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-18 08:42:48 +00:00
Discover major refactor
This commit is contained in:
parent
8cc0d76792
commit
29677ec69b
4 changed files with 196 additions and 183 deletions
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
49
src/routes/Discover/PickerMenu.js
Normal file
49
src/routes/Discover/PickerMenu.js
Normal 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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue