board ui finalized

This commit is contained in:
NikolaBorislavovHristov 2019-10-14 15:09:20 +03:00
parent e6724a701d
commit 443db842fb
11 changed files with 225 additions and 243 deletions

View file

@ -0,0 +1,129 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const Icon = require('stremio-icons/dom');
const Button = require('stremio/common/Button');
const Image = require('stremio/common/Image');
const Multiselect = require('stremio/common/Multiselect');
const useBinaryState = require('stremio/common/useBinaryState');
const useDataset = require('stremio/common/useDataset');
const PlayIconCircleCentered = require('./PlayIconCircleCentered');
const styles = require('./styles');
const ICON_FOR_TYPE = Object.assign(Object.create(null), {
'movie': 'ic_movies',
'series': 'ic_series',
'channel': 'ic_channels',
'tv': 'ic_tv',
'other': 'ic_movies'
});
const MetaItem = React.memo(({ className, type, name, poster, posterShape, playIcon, progress, menuOptions, onSelect, menuOptionOnSelect, ...props }) => {
const dataset = useDataset(props);
const [menuOpen, onMenuOpen, onMenuClose] = useBinaryState(false);
const metaItemOnClick = React.useCallback((event) => {
if (!event.nativeEvent.selectMetaItemPrevented && typeof onSelect === 'function') {
onSelect({
type: 'select',
dataset: dataset,
reactEvent: event,
nativeEvent: event.nativeEvent
});
}
}, [onSelect, dataset]);
const multiselectOnClick = React.useCallback((event) => {
event.nativeEvent.selectMetaItemPrevented = true;
}, []);
const multiselectOnSelect = React.useCallback((event) => {
if (typeof menuOptionOnSelect === 'function') {
menuOptionOnSelect({
type: 'select-option',
dataset: dataset,
reactEvent: event.reactEvent,
nativeEvent: event.nativeEvent
});
}
}, [menuOptionOnSelect, dataset]);
return (
<Button className={classnames(className, styles['meta-item-container'], styles['poster-shape-poster'], styles[`poster-shape-${posterShape}`], { 'active': menuOpen })} title={name} onClick={metaItemOnClick}>
<div className={styles['poster-container']}>
<div className={styles['poster-image-layer']}>
<Image
className={styles['poster-image']}
src={poster}
alt={' '}
renderFallback={() => (
<Icon
className={styles['placeholder-icon']}
icon={typeof ICON_FOR_TYPE[type] === 'string' ? ICON_FOR_TYPE[type] : ICON_FOR_TYPE['other']}
/>
)}
/>
</div>
{
playIcon ?
<div className={styles['play-icon-layer']}>
<PlayIconCircleCentered className={styles['play-icon']} />
</div>
:
null
}
{
progress > 0 ?
<div className={styles['progress-bar-layer']}>
<div className={styles['progress-bar']} style={{ width: `${Math.min(progress, 1) * 100}%` }} />
</div>
:
null
}
</div>
{
(typeof name === 'string' && name.length > 0) || (Array.isArray(menuOptions) && menuOptions.length > 0) ?
<div className={styles['title-bar-container']}>
{
typeof name === 'string' && name.length > 0 ?
<div className={styles['title-label']}>{name}</div>
:
null
}
{
Array.isArray(menuOptions) && menuOptions.length > 0 ?
<div className={styles['multiselect-container']} onClick={multiselectOnClick}>
<Multiselect
className={styles['multiselect-label-container']}
renderLabelContent={() => (
<Icon className={styles['icon']} icon={'ic_more'} />
)}
options={menuOptions}
onOpen={onMenuOpen}
onClose={onMenuClose}
onSelect={multiselectOnSelect}
/>
</div>
:
null
}
</div>
:
null
}
</Button>
);
});
MetaItem.displayName = 'MetaItem';
MetaItem.propTypes = {
className: PropTypes.string,
type: PropTypes.string,
name: PropTypes.string,
poster: PropTypes.string,
posterShape: PropTypes.oneOf(['poster', 'landscape', 'square']),
playIcon: PropTypes.bool,
progress: PropTypes.number,
menuOptions: PropTypes.array,
onSelect: PropTypes.func,
menuOptionOnSelect: PropTypes.func
};
module.exports = MetaItem;

View file

@ -1,34 +1,35 @@
:import('~stremio/common/Image/styles.less') {
poster-image: image;
:import('~stremio/common/Popup/styles.less') {
popup-menu-container: menu-container;
}
:import('~stremio/common/PlayIconCircleCentered/styles.less') {
:import('~stremio/common/Multiselect/styles.less') {
multiselect-menu-container: menu-container;
multiselect-option-container: option-container;
multiselect-option-label: label;
}
:import('./PlayIconCircleCentered/styles.less') {
play-icon-circle-centered-background: background;
play-icon-circle-centered-icon: icon;
}
:import('~stremio/common/Dropdown/styles.less') {
dropdown-option-container: dropdown-option-container;
dropdown-option-label: label;
}
.meta-item-container {
position: relative;
z-index: 0;
background-color: var(--color-backgroundlight);
overflow: visible;
&:hover, &:focus, &:global(.active) {
outline-style: solid;
background-color: var(--color-surfacelight);
outline-offset: 0;
.title-bar-container {
background-color: var(--color-surfacelight);
.title-label {
color: var(--color-backgrounddarker);
}
.dropdown-menu-container {
.menu-label-container {
.menu-icon {
.multiselect-container {
.multiselect-label-container {
.icon {
fill: var(--color-backgrounddarker);
}
}
@ -56,7 +57,7 @@
.poster-container {
position: relative;
z-index: -1;
z-index: 0;
background-color: var(--color-backgroundlight);
.poster-image-layer {
@ -65,26 +66,25 @@
right: 0;
bottom: 0;
left: 0;
z-index: 1;
z-index: -3;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.poster-image-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.poster-image {
flex: none;
width: 100%;
height: 100%;
object-position: center;
object-fit: cover;
}
.poster-image {
object-fit: cover;
}
.placeholder-icon {
flex: none;
height: 50%;
width: 80%;
fill: var(--color-surfacelight20);
}
.placeholder-icon {
flex: none;
width: 80%;
height: 50%;
fill: var(--color-surfacelight20);
}
}
@ -94,7 +94,7 @@
right: 0;
bottom: 30%;
left: 0;
z-index: 2;
z-index: -2;
overflow: visible;
.play-icon {
@ -118,7 +118,7 @@
right: 0;
bottom: 0;
left: 0;
z-index: 3;
z-index: -1;
background-color: var(--color-backgroundlighter);
.progress-bar {
@ -134,6 +134,8 @@
align-items: center;
justify-content: flex-end;
height: 2.8rem;
background-color: var(--color-backgroundlight);
overflow: visible;
.title-label {
flex: 1;
@ -146,52 +148,54 @@
}
}
.dropdown-menu-container {
.multiselect-container {
flex: none;
width: 2.8rem;
height: 2.8rem;
overflow: visible;
.menu-label-container {
.multiselect-label-container {
width: 100%;
height: 100%;
padding: 0.75rem;
background-color: transparent;
&:hover, &:global(.active) {
filter: none;
background-color: var(--color-surface80);
.menu-icon {
fill: var(--color-backgrounddarker);
}
}
.menu-icon {
.icon {
display: block;
width: 100%;
height: 100%;
fill: var(--color-surfacelighter);
}
.popup-menu-container {
width: auto;
right: initial;
left: 0;
.multiselect-menu-container {
min-width: 8rem;
max-width: 12rem;
.multiselect-option-container {
padding: 0.5rem;
background-color: var(--color-surfacelighter);
&:hover, &:focus {
outline: none;
background-color: var(--color-surfacelight);
}
.multiselect-option-label {
color: var(--color-backgrounddarker);
}
}
}
}
}
}
}
}
.menu-container {
min-width: 8rem;
max-width: 12rem;
.dropdown-option-container {
padding: 0.5rem;
background-color: var(--color-surfacelighter);
&:hover, &:focus {
outline: none;
background-color: var(--color-surfacelight);
}
.dropdown-option-label {
color: var(--color-backgrounddarker);
}
}
}

View file

@ -1,137 +0,0 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const Icon = require('stremio-icons/dom');
const Button = require('stremio/common/Button');
const Image = require('stremio/common/Image');
const Dropdown = require('stremio/common/Dropdown');
const PlayIconCircleCentered = require('./PlayIconCircleCentered');
const styles = require('./styles');
const ICON_FOR_TYPE = Object.assign(Object.create(null), {
'movie': 'ic_movies',
'series': 'ic_series',
'channel': 'ic_channels',
'tv': 'ic_tv',
'other': 'ic_movies'
});
const MetaItem = React.memo(({ className, id, type, name, posterShape, poster, title, subtitle, progress, playIcon, menuOptions, onSelect, menuOptionOnSelect }) => {
const [menuOpen, setMenuOpen] = React.useState(false);
const onOpen = React.useCallback(() => {
setMenuOpen(true);
}, []);
const onClose = React.useCallback(() => {
setMenuOpen(false);
}, []);
const metaItemOnClick = React.useCallback((event) => {
if (!event.nativeEvent.selectMetaItemPrevented && typeof onSelect === 'function') {
onSelect(event);
}
}, [onSelect]);
const menuOnClick = React.useCallback((event) => {
event.nativeEvent.selectMetaItemPrevented = true;
}, []);
return (
<Button className={classnames(className, styles['meta-item-container'], styles['poster-shape-poster'], styles[`poster-shape-${posterShape}`], { 'active': menuOpen })} title={name} data-id={id} onClick={metaItemOnClick}>
<div className={styles['poster-container']}>
<div className={styles['poster-image-layer']}>
<Image
className={styles['poster-image-container']}
src={poster}
alt={' '}
id={id}
renderFallback={() => (
<Icon
className={styles['placeholder-icon']}
icon={typeof ICON_FOR_TYPE[type] === 'string' ? ICON_FOR_TYPE[type] : ICON_FOR_TYPE['other']}
/>
)}
/>
</div>
{
playIcon ?
<div className={styles['play-icon-layer']}>
<PlayIconCircleCentered className={styles['play-icon']} />
</div>
:
null
}
{
progress > 0 ?
<div className={styles['progress-bar-layer']}>
<div className={styles['progress-bar']} style={{ width: `${Math.min(progress, 1) * 100}%` }} />
</div>
:
null
}
</div>
{
(typeof title === 'string' && title.length > 0) || (typeof subtitle === 'string' && subtitle.length > 0) || (Array.isArray(menuOptions) && menuOptions.length > 0) ?
<React.Fragment>
<div className={styles['title-bar-container']}>
{
typeof title === 'string' && title.length > 0 ?
<div className={styles['title-label']}>{title}</div>
:
null
}
{
Array.isArray(menuOptions) && menuOptions.length > 0 ?
<div className={styles['dropdown-menu-container']} onClick={menuOnClick}>
<Dropdown
className={styles['menu-label-container']}
menuClassName={styles['menu-container']}
menuMatchLabelWidth={false}
renderLabel={() => (
<Icon className={styles['menu-icon']} icon={'ic_more'} />
)}
options={menuOptions}
tabIndex={-1}
onOpen={onOpen}
onClose={onClose}
onSelect={menuOptionOnSelect}
/>
</div>
:
null
}
</div>
{
typeof subtitle === 'string' && subtitle.length > 0 ?
<div className={styles['title-bar-container']}>
<div className={styles['title-label']}>{subtitle}</div>
</div>
:
null
}
</React.Fragment>
:
null
}
</Button>
);
});
MetaItem.displayName = 'MetaItem';
MetaItem.propTypes = {
className: PropTypes.string,
id: PropTypes.string,
type: PropTypes.string,
name: PropTypes.string,
posterShape: PropTypes.oneOf(['poster', 'landscape', 'square']),
poster: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
progress: PropTypes.number,
playIcon: PropTypes.bool,
menuOptions: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
})),
onSelect: PropTypes.func,
menuOptionOnSelect: PropTypes.func
};
module.exports = MetaItem;

View file

@ -3,70 +3,62 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const Icon = require('stremio-icons/dom');
const Button = require('stremio/common/Button');
const MetaItem = require('./MetaItem');
const MetaItem = require('stremio/common/MetaItem');
const MetaRowPlaceholder = require('./MetaRowPlaceholder');
const styles = require('./styles');
const MetaRow = ({ className, title, message, items, itemMenuOptions }) => {
const MetaRow = ({ className, title, message, items, maximumItemsCount, itemMenuOptions }) => {
maximumItemsCount = maximumItemsCount !== null && isFinite(maximumItemsCount) ? maximumItemsCount : 20;
items = Array.isArray(items) ? items.slice(0, maximumItemsCount) : [];
return (
<div className={classnames(className, styles['meta-row-container'])}>
{
typeof title === 'string' && title.length > 0 ?
<div className={styles['title-container']}>{title}</div>
<div className={styles['title-container']} title={title}>{title}</div>
:
null
}
{
typeof message === 'string' && message.length > 0 ?
<div className={styles['message-container']}>{message}</div>
<div className={styles['message-container']} title={message}>{message}</div>
:
null
}
{
Array.isArray(items) && items.length > 0 ?
<React.Fragment>
<div className={styles['meta-items-container']}>
{items.map((item) => (
{items.map((item, index) => (
<MetaItem
{...item}
key={item.id}
key={index}
data-id={item.id}
data-type={item.type}
className={classnames(styles['meta-item'], styles['poster-shape-poster'], styles[`poster-shape-${item.posterShape}`])}
title={item.name}
menuOptions={itemMenuOptions}
/>
))}
<div className={classnames(styles['meta-item'], styles['poster-shape-poster'], styles[`poster-shape-${items[0].posterShape}`])} />
<div className={classnames(styles['meta-item'], styles['poster-shape-poster'], styles[`poster-shape-${items[0].posterShape}`])} />
<div className={classnames(styles['meta-item'], styles['poster-shape-poster'], styles[`poster-shape-${items[0].posterShape}`])} />
<div className={classnames(styles['meta-item'], styles['poster-shape-poster'], styles[`poster-shape-${items[0].posterShape}`])} />
<div className={classnames(styles['meta-item'], styles['poster-shape-poster'], styles[`poster-shape-${items[0].posterShape}`])} />
<div className={classnames(styles['meta-item'], styles['poster-shape-poster'], styles[`poster-shape-${items[0].posterShape}`])} />
<div className={classnames(styles['meta-item'], styles['poster-shape-poster'], styles[`poster-shape-${items[0].posterShape}`])} />
<div className={classnames(styles['meta-item'], styles['poster-shape-poster'], styles[`poster-shape-${items[0].posterShape}`])} />
<div className={classnames(styles['meta-item'], styles['poster-shape-poster'], styles[`poster-shape-${items[0].posterShape}`])} />
<div className={classnames(styles['meta-item'], styles['poster-shape-poster'], styles[`poster-shape-${items[0].posterShape}`])} />
{Array(Math.max(maximumItemsCount - items.length, 0)).fill(null).map((_, index) => (
<div key={index} className={classnames(styles['meta-item'], styles['poster-shape-poster'])} />
))}
</div>
<Button className={styles['see-all-container']} title={'SEE ALL'}>
<div className={styles['see-all-label']}>SEE ALL</div>
<div className={styles['label']}>SEE ALL</div>
<Icon className={styles['icon']} icon={'ic_arrow_thin_right'} />
</Button>
</React.Fragment>
:
null
}
</div>
);
};
MetaRow.Placeholder = MetaRowPlaceholder;
MetaRow.propTypes = {
className: PropTypes.string,
title: PropTypes.string,
message: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
posterShape: PropTypes.string
})),
itemMenuOptions: PropTypes.array
maximumItemsCount: PropTypes.number,
itemMenuOptions: PropTypes.any
};
module.exports = MetaRow;

View file

@ -5,6 +5,7 @@
"title-area title-area"
"message-area message-area"
"meta-items-area see-all-area";
overflow: visible;
.title-container {
grid-area: title-area;
@ -19,10 +20,6 @@
max-height: 3.6em;
font-size: 1.3rem;
color: var(--color-surfacelighter);
&~.meta-items-container, &~.see-all-container {
display: none;
}
}
.meta-items-container {
@ -30,6 +27,7 @@
display: flex;
flex-direction: row;
align-items: stretch;
overflow: visible;
.meta-item {
margin-right: 2rem;
@ -59,21 +57,16 @@
&:hover, &:focus {
background-color: var(--color-backgroundlighter);
.see-all-label {
color: var(--color-surfacelighter);
}
.icon {
fill: var(--color-surfacelighter);
}
}
.see-all-label {
.label {
flex-grow: 0;
flex-shrink: 1;
flex-basis: auto;
max-height: 2.4em;
font-size: 1.2rem;
text-align: center;
color: var(--color-surfacelight);
color: var(--color-surfacelighter);
}
.icon {
@ -81,7 +74,7 @@
width: 1.3rem;
height: 1.3rem;
margin-left: 0.3rem;
fill: var(--color-surfacelight);
fill: var(--color-surfacelighter);
}
}
}

View file

@ -1,5 +1,5 @@
const React = require('react');
const { MainNavBar, MetaRow, MetaRowPlaceholder } = require('stremio/common');
const { MainNavBar, MetaRow } = require('stremio/common');
const useCatalogs = require('./useCatalogs');
const styles = require('./styles');
@ -42,9 +42,10 @@ const Board = () => {
);
case 'Loading':
return (
<MetaRowPlaceholder
<MetaRow.Placeholder
key={`${index}${req.base}${content.type}`}
className={styles['board-row-placeholder']}
title={`${req.path.id} - ${req.path.type_name}`}
/>
);
}

View file

@ -4,7 +4,7 @@
meta-item: meta-item;
}
:import('~stremio/common/MetaRowPlaceholder/styles.less') {
:import('~stremio/common/MetaRow/MetaRowPlaceholder/styles.less') {
meta-item-placeholder: meta-item;
}