mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-15 08:26:04 +00:00
Merge branch 'master' of github.com:Stremio/stremio-web into addons-screen
This commit is contained in:
commit
a61697ce38
20 changed files with 736 additions and 143 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { Calendar, Discover, Addons, Settings, Board, Player } from 'stremio-routes';
|
||||
import { Calendar, Discover, Addons, Settings, Board, Player, Detail } from 'stremio-routes';
|
||||
|
||||
const config = {
|
||||
views: [
|
||||
|
|
@ -27,6 +27,10 @@ const config = {
|
|||
{
|
||||
path: '/settings',
|
||||
component: Settings
|
||||
},
|
||||
{
|
||||
path: '/detail',
|
||||
component: Detail
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,13 +15,15 @@ class Popup extends Component {
|
|||
this.labelBorderRightRef = React.createRef();
|
||||
this.labelBorderBottomRef = React.createRef();
|
||||
this.labelBorderLeftRef = React.createRef();
|
||||
this.menuRef = React.createRef();
|
||||
this.menuContainerRef = React.createRef();
|
||||
this.menuScrollRef = React.createRef();
|
||||
this.menuChildrenRef = React.createRef();
|
||||
this.menuBorderTopRef = React.createRef();
|
||||
this.menuBorderRightRef = React.createRef();
|
||||
this.menuBorderBottomRef = React.createRef();
|
||||
this.menuBorderLeftRef = React.createRef();
|
||||
this.hiddenBorderRef = React.createRef();
|
||||
this.popupMutationObserver = this.createPopupMutationObserver();
|
||||
|
||||
this.state = {
|
||||
open: false
|
||||
|
|
@ -38,6 +40,7 @@ class Popup extends Component {
|
|||
window.removeEventListener('blur', this.close);
|
||||
window.removeEventListener('resize', this.close);
|
||||
window.removeEventListener('keyup', this.onKeyUp);
|
||||
this.popupMutationObserver.disconnect();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
|
|
@ -48,19 +51,65 @@ class Popup extends Component {
|
|||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.state.open && !prevState.open) {
|
||||
this.updateStyles();
|
||||
this.popupMutationObserver.observe(document.documentElement, {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
subtree: true
|
||||
});
|
||||
if (typeof this.props.onOpen === 'function') {
|
||||
this.props.onOpen();
|
||||
}
|
||||
} else if (!this.state.open && prevState.open && typeof this.props.onClose === 'function') {
|
||||
this.props.onClose();
|
||||
} else if (!this.state.open && prevState.open) {
|
||||
this.popupMutationObserver.disconnect();
|
||||
if (typeof this.props.onClose === 'function') {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createPopupMutationObserver = () => {
|
||||
let prevLabelRect = {};
|
||||
let prevMenuChildrenRect = {};
|
||||
return new MutationObserver(() => {
|
||||
if (this.state.open) {
|
||||
const labelRect = this.labelRef.current.getBoundingClientRect();
|
||||
const menuChildrenRect = this.menuChildrenRef.current.getBoundingClientRect();
|
||||
if (labelRect.x !== prevLabelRect.x ||
|
||||
labelRect.y !== prevLabelRect.y ||
|
||||
labelRect.width !== prevLabelRect.width ||
|
||||
labelRect.height !== prevLabelRect.height ||
|
||||
menuChildrenRect.x !== prevMenuChildrenRect.x ||
|
||||
menuChildrenRect.y !== prevMenuChildrenRect.y ||
|
||||
menuChildrenRect.width !== prevMenuChildrenRect.width ||
|
||||
menuChildrenRect.height !== prevMenuChildrenRect.height) {
|
||||
this.updateStyles();
|
||||
}
|
||||
|
||||
prevLabelRect = labelRect;
|
||||
prevMenuChildrenRect = menuChildrenRect;
|
||||
} else {
|
||||
prevLabelRect = {};
|
||||
prevMenuChildrenRect = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateStyles = () => {
|
||||
this.menuContainerRef.current.removeAttribute('style');
|
||||
this.menuScrollRef.current.removeAttribute('style');
|
||||
this.menuBorderTopRef.current.removeAttribute('style');
|
||||
this.menuBorderRightRef.current.removeAttribute('style');
|
||||
this.menuBorderBottomRef.current.removeAttribute('style');
|
||||
this.menuBorderLeftRef.current.removeAttribute('style');
|
||||
this.labelBorderTopRef.current.removeAttribute('style');
|
||||
this.labelBorderRightRef.current.removeAttribute('style');
|
||||
this.labelBorderBottomRef.current.removeAttribute('style');
|
||||
this.labelBorderLeftRef.current.removeAttribute('style');
|
||||
|
||||
const menuDirections = {};
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const menuRect = this.menuRef.current.getBoundingClientRect();
|
||||
const labelRect = this.labelRef.current.getBoundingClientRect();
|
||||
const menuChildredRect = this.menuChildrenRef.current.getBoundingClientRect();
|
||||
const borderSize = parseFloat(window.getComputedStyle(this.hiddenBorderRef.current).getPropertyValue('border-top-width'));
|
||||
const labelPosition = {
|
||||
left: labelRect.x - bodyRect.x,
|
||||
|
|
@ -69,38 +118,38 @@ class Popup extends Component {
|
|||
bottom: (bodyRect.height + bodyRect.y) - (labelRect.y + labelRect.height)
|
||||
};
|
||||
|
||||
if (menuRect.height <= labelPosition.bottom) {
|
||||
this.menuRef.current.style.top = `${labelPosition.top + labelRect.height}px`;
|
||||
if (menuChildredRect.height <= labelPosition.bottom) {
|
||||
this.menuContainerRef.current.style.top = `${labelPosition.top + labelRect.height}px`;
|
||||
this.menuScrollRef.current.style.maxHeight = `${labelPosition.bottom}px`;
|
||||
menuDirections.bottom = true;
|
||||
} else if (menuRect.height <= labelPosition.top) {
|
||||
this.menuRef.current.style.bottom = `${labelPosition.bottom + labelRect.height}px`;
|
||||
} else if (menuChildredRect.height <= labelPosition.top) {
|
||||
this.menuContainerRef.current.style.bottom = `${labelPosition.bottom + labelRect.height}px`;
|
||||
this.menuScrollRef.current.style.maxHeight = `${labelPosition.top}px`;
|
||||
menuDirections.top = true;
|
||||
} else if (labelPosition.bottom >= labelPosition.top) {
|
||||
this.menuRef.current.style.top = `${labelPosition.top + labelRect.height}px`;
|
||||
this.menuContainerRef.current.style.top = `${labelPosition.top + labelRect.height}px`;
|
||||
this.menuScrollRef.current.style.maxHeight = `${labelPosition.bottom}px`;
|
||||
menuDirections.bottom = true;
|
||||
} else {
|
||||
this.menuRef.current.style.bottom = `${labelPosition.bottom + labelRect.height}px`;
|
||||
this.menuContainerRef.current.style.bottom = `${labelPosition.bottom + labelRect.height}px`;
|
||||
this.menuScrollRef.current.style.maxHeight = `${labelPosition.top}px`;
|
||||
menuDirections.top = true;
|
||||
}
|
||||
|
||||
if (menuRect.width <= (labelPosition.right + labelRect.width)) {
|
||||
this.menuRef.current.style.left = `${labelPosition.left}px`;
|
||||
if (menuChildredRect.width <= (labelPosition.right + labelRect.width)) {
|
||||
this.menuContainerRef.current.style.left = `${labelPosition.left}px`;
|
||||
this.menuScrollRef.current.style.maxWidth = `${labelPosition.right + labelRect.width}px`;
|
||||
menuDirections.right = true;
|
||||
} else if (menuRect.width <= (labelPosition.left + labelRect.width)) {
|
||||
this.menuRef.current.style.right = `${labelPosition.right}px`;
|
||||
} else if (menuChildredRect.width <= (labelPosition.left + labelRect.width)) {
|
||||
this.menuContainerRef.current.style.right = `${labelPosition.right}px`;
|
||||
this.menuScrollRef.current.style.maxWidth = `${labelPosition.left + labelRect.width}px`;
|
||||
menuDirections.left = true;
|
||||
} else if (labelPosition.right > labelPosition.left) {
|
||||
this.menuRef.current.style.left = `${labelPosition.left}px`;
|
||||
this.menuContainerRef.current.style.left = `${labelPosition.left}px`;
|
||||
this.menuScrollRef.current.style.maxWidth = `${labelPosition.right + labelRect.width}px`;
|
||||
menuDirections.right = true;
|
||||
} else {
|
||||
this.menuRef.current.style.right = `${labelPosition.right}px`;
|
||||
this.menuContainerRef.current.style.right = `${labelPosition.right}px`;
|
||||
this.menuScrollRef.current.style.maxWidth = `${labelPosition.left + labelRect.width}px`;
|
||||
menuDirections.left = true;
|
||||
}
|
||||
|
|
@ -128,14 +177,14 @@ class Popup extends Component {
|
|||
this.labelBorderLeftRef.current.style.left = `${labelPosition.left}px`;
|
||||
|
||||
if (menuDirections.top) {
|
||||
this.labelBorderTopRef.current.style.display = 'none';
|
||||
this.labelBorderTopRef.current.style.left = `${labelPosition.left + menuChildredRect.width}px`;
|
||||
if (menuDirections.left) {
|
||||
this.menuBorderBottomRef.current.style.right = `${labelRect.width - borderSize}px`;
|
||||
} else {
|
||||
this.menuBorderBottomRef.current.style.left = `${labelRect.width - borderSize}px`;
|
||||
}
|
||||
} else {
|
||||
this.labelBorderBottomRef.current.style.display = 'none';
|
||||
this.labelBorderBottomRef.current.style.left = `${labelPosition.left + menuChildredRect.width}px`;
|
||||
if (menuDirections.left) {
|
||||
this.menuBorderTopRef.current.style.right = `${labelRect.width - borderSize}px`;
|
||||
} else {
|
||||
|
|
@ -144,7 +193,7 @@ class Popup extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
this.menuRef.current.style.visibility = 'visible';
|
||||
this.menuContainerRef.current.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
onKeyUp = (event) => {
|
||||
|
|
@ -177,9 +226,11 @@ class Popup extends Component {
|
|||
|
||||
return (
|
||||
<Modal className={classnames('modal-container', this.props.className)} onClick={this.close}>
|
||||
<div ref={this.menuRef} className={styles['menu-container']} onClick={this.menuContainerOnClick}>
|
||||
<div ref={this.menuScrollRef} className={styles['scroll-container']}>
|
||||
{children}
|
||||
<div ref={this.menuContainerRef} className={styles['menu-container']} onClick={this.menuContainerOnClick}>
|
||||
<div ref={this.menuScrollRef} className={styles['menu-scroll-container']}>
|
||||
<div ref={this.menuChildrenRef}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<div ref={this.menuBorderTopRef} className={classnames(styles['border'], styles['border-top'])} />
|
||||
<div ref={this.menuBorderRightRef} className={classnames(styles['border'], styles['border-right'])} />
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
position: absolute;
|
||||
visibility: hidden;
|
||||
|
||||
.scroll-container {
|
||||
.menu-scroll-container {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,10 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import { Catalogs } from 'stremio-aggregators';
|
||||
import { addons } from 'stremio-services';
|
||||
import { Stream } from 'stremio-common';
|
||||
import { Video } from 'stremio-common';
|
||||
import { LibraryItemList } from 'stremio-common';
|
||||
import { MetaItem } from 'stremio-common';
|
||||
import { Addon } from 'stremio-common';
|
||||
import { ShareAddon } from 'stremio-common';
|
||||
import { UserPanel } from 'stremio-common';
|
||||
|
||||
class Board extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// this.aggregator = new Catalogs(addons.addons);
|
||||
|
||||
this.state = {
|
||||
catalogs: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// this.aggregator.evs.addListener('updated', this.onCatalogsUpdated);
|
||||
// this.aggregator.run();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// this.aggregator.evs.removeListener('updated', this.onCatalogsUpdated);
|
||||
}
|
||||
|
||||
onCatalogsUpdated = () => {
|
||||
// this.setState({ catalogs: this.aggregator.results.slice() });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{ paddingTop: 40, color: 'yellow' }}>
|
||||
<UserPanel photo={'https://image.freepik.com/free-vector/wild-animals-cartoon_1196-361.jpg'} email={'animals@mail.com'}></UserPanel>
|
||||
Board
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,161 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import VideosList from './VideosList';
|
||||
import Icon from 'stremio-icons/dom';
|
||||
import styles from './styles';
|
||||
|
||||
class Detail extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
logoLoaded: true
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextState.logoLoaded !== this.state.logoLoaded;
|
||||
}
|
||||
|
||||
renderSection({ title, links }) {
|
||||
return (
|
||||
<div className={styles['section-container']}>
|
||||
{
|
||||
title ?
|
||||
<div className={styles['title']}>{title}</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{links.map(link => <a key={link} className={styles['link']}>{link}</a>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
return (
|
||||
<div style={this.props.metaItem.background.length > 0 ? { backgroundImage: 'url(' + this.props.metaItem.background + ')' } : { backgroundColor: colors.backgrounddarker }} className={styles['detail-container']}>
|
||||
<div className={styles['overlay-container']} />
|
||||
<div className={styles['info-container']}>
|
||||
{
|
||||
this.state.logoLoaded ?
|
||||
<img className={styles['logo']} src={this.props.metaItem.logo} onError={() => this.setState({ logoLoaded: false })} />
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['duration']}>{this.props.metaItem.duration}</div>
|
||||
<div className={styles['release-info']}>
|
||||
{
|
||||
this.props.metaItem.releaseInfo.length > 0 ?
|
||||
this.props.metaItem.releaseInfo
|
||||
:
|
||||
this.props.metaItem.released.getFullYear()
|
||||
}
|
||||
</div>
|
||||
<div className={styles['name']}>{this.props.metaItem.name}</div>
|
||||
<div className={styles['description']}>{this.props.metaItem.description}</div>
|
||||
{this.renderSection({ title: 'GENRES', links: this.props.metaItem.genres })}
|
||||
{this.renderSection({ title: 'WRITTEN BY', links: this.props.metaItem.writers })}
|
||||
{this.renderSection({ title: 'DIRECTED BY', links: this.props.metaItem.directors })}
|
||||
{this.renderSection({ title: 'CAST', links: this.props.metaItem.cast })}
|
||||
<div className={styles['action-buttons-container']}>
|
||||
<a href={this.props.metaItem.links.youtube} className={styles['action-button-container']}>
|
||||
<Icon className={styles['icon']} icon={'ic_movies'} />
|
||||
<div className={styles['label']}>Trailer</div>
|
||||
</a>
|
||||
<a href={this.props.metaItem.links.imdb} target={'_blank'} className={styles['action-button-container']}>
|
||||
<Icon className={styles['icon']} icon={'ic_imdb'} />
|
||||
<div className={styles['label']}>{this.props.metaItem.imdbRating} / 10</div>
|
||||
</a>
|
||||
<div className={styles['action-button-container']} onClick={this.props.toggleLibraryButton}>
|
||||
<Icon className={styles['icon']} icon={this.props.inLibrary ? 'ic_removelib' : 'ic_addlib'} />
|
||||
<div className={styles['label']}>{this.props.inLibrary ? 'Remove from Library' : 'Add to library'}</div>
|
||||
</div>
|
||||
<div className={styles['action-button-container']}>
|
||||
<Icon className={styles['icon']} icon={'ic_share'} />
|
||||
<div className={styles['label']}>Share</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<VideosList className={styles['videos-list']} videos={this.props.metaItem.videos}></VideosList>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Detail.propTypes = {
|
||||
inLibrary: PropTypes.bool.isRequired,
|
||||
metaItem: PropTypes.object.isRequired,
|
||||
toggleLibraryButton: PropTypes.func
|
||||
};
|
||||
Detail.defaultProps = {
|
||||
inLibrary: false,
|
||||
metaItem: {
|
||||
logo: 'https://images.metahub.space/logo/medium/tt4123430/img',
|
||||
background: 'https://images.metahub.space/background/medium/tt4123430/img',
|
||||
duration: '134 min',
|
||||
releaseInfo: '2018',
|
||||
released: new Date(2018, 4, 23),
|
||||
imdbRating: '7.4',
|
||||
name: 'Fantastic Beasts and Where to Find Them: The Original Screenplay',
|
||||
description: 'In an effort to thwart Grindelwald' + 's plans ofraisingpurebloodwizardstoansofraisingpurebloodwizardstoansofraisingpurebloodwizardstoansofraising pure-blood wizards toans of raising pure-blood wizards toans of raising pure-blood wizards toans of raising pure-blood wizards toans of raising pure-blood wizards toans of raising pure-blood wizards toans of raising pure-blood wizards toans of raising pure-blood wizards toans of raising pure-blood wizards toans of raising pure-blood wizards toans of raising pure-blood wizards toans of raising pure-blood wizards toans of raising pure-blood wizards toans of raising pure-blood wizards to rule over all non-magical beings, Albus Dumbledore enlists his former student Newt Scamander, who agrees to help, unaware of the dangers that lie ahead. Lines are drawn as love and loyalty are tested, even among the truest friends and family, in an increasingly divided wizarding world.',
|
||||
genres: ['Adventure', 'Family', 'Fantasy'],
|
||||
writers: ['J. K. Rowling'],
|
||||
directors: ['David Yates'],
|
||||
cast: ['Johny Depp', 'Kevin Guthrie', 'Carmen Ejogo', 'Wolf Roth'],
|
||||
videos: [
|
||||
{ id: '1', poster: 'https://www.stremio.com/websiste/home-stremio.png', episode: 1, name: 'The Bing BranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, isWatched: true, season: 1 },
|
||||
{ id: '2', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 2, name: 'The Bing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isWatched: true, season: 3 },
|
||||
{ id: '3', episode: 4, name: 'The Luminous Fish Effect', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 5 },
|
||||
{ id: '4', poster: 'https://www.stremiocom/website/home-stremio.png', episode: 5, name: 'The Dumpling Paradox', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 4 },
|
||||
{ id: '5', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 5 },
|
||||
{ id: '6', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 1, name: 'The Bing BranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, isWatched: true, season: 1 },
|
||||
{ id: '7', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 2, name: 'The Bing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isWatched: true, season: 1 },
|
||||
{ id: '8', episode: 4, name: 'The Luminous Fish Effect', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 3 },
|
||||
{ id: '9', poster: 'https://www.stremiocom/website/home-stremio.png', episode: 5, name: 'The Dumpling Paradox', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 2 },
|
||||
{ id: '10', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 5 },
|
||||
{ id: '11', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 1 },
|
||||
{ id: '12', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 1, name: 'The Bing BranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, isWatched: true, season: 1 },
|
||||
{ id: '13', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 2, name: 'The Bing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isWatched: true, season: 1 },
|
||||
{ id: '14', episode: 4, name: 'The Luminous Fish Effect', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 3 },
|
||||
{ id: '15', poster: 'https://www.stremiocom/website/home-stremio.png', episode: 5, name: 'The Dumpling Paradox', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 2 },
|
||||
{ id: '16', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 5 },
|
||||
{ id: '17', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 1 },
|
||||
{ id: '18', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 1, name: 'The Bing BranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, isWatched: true, season: 1 },
|
||||
{ id: '19', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 2, name: 'The Bing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isWatched: true, season: 1 },
|
||||
{ id: '20', episode: 4, name: 'The Luminous Fish Effect', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 3 },
|
||||
{ id: '21', poster: 'https://www.stremiocom/wsebsite/home-stremio.png', episode: 5, name: 'The Dumpling Paradox', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 2 },
|
||||
{ id: '22', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 5 },
|
||||
{ id: '23', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 1 },
|
||||
{ id: '24', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 1, name: 'The Bing BranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, isWatched: true, season: 1 },
|
||||
{ id: '25', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 2, name: 'The Bing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isWatched: true, season: 1 },
|
||||
{ id: '26', episode: 4, name: 'The Luminous Fish Effect', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 3 },
|
||||
{ id: '27', poster: 'https://www.stremiocom/website/home-stremio.png', episode: 5, name: 'The Dumpling Paradox', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 2 },
|
||||
{ id: '28', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 5 },
|
||||
{ id: '29', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 1 },
|
||||
{ id: '30', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 1, name: 'The Bing BranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, isWatched: true, season: 1 },
|
||||
{ id: '31', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 2, name: 'The Bing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isWatched: true, season: 1 },
|
||||
{ id: '32', episode: 4, name: 'The Luminous Fish Effect', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 1 },
|
||||
{ id: '33', poster: 'https://www.stremiocom/website/home-stremio.png', episode: 5, name: 'The Dumpling Paradox', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 2 },
|
||||
{ id: '34', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 1 },
|
||||
{ id: '35', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 1 },
|
||||
{ id: '36', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 1, name: 'The Bing BranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, isWatched: true, season: 1 },
|
||||
{ id: '37', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 2, name: 'The Bing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isWatched: true, season: 1 },
|
||||
{ id: '38', episode: 4, name: 'The Luminous Fish Effect', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 1 },
|
||||
{ id: '39', poster: 'https://www.stremiocom/website/home-stremio.png', episode: 5, name: 'The Dumpling Paradox', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 2 },
|
||||
{ id: '40', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 1 },
|
||||
{ id: '41', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 1 },
|
||||
{ id: '42', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 1, name: 'The Bing BranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBranHypothesiingBran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesiing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, isWatched: true, season: 1 },
|
||||
{ id: '43', poster: 'https://www.stremio.com/website/home-stremio.png', episode: 2, name: 'The Bing Bran Hypothesis', description: 'dasdasda', released: new Date(2018, 4, 23), isWatched: true, season: 1 },
|
||||
{ id: '44', episode: 4, name: 'The Luminous Fish Effect', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 1 },
|
||||
{ id: '45', poster: 'https://www.stremiocom/website/home-stremio.png', episode: 5, name: 'The Dumpling Paradox', description: 'dasdasda', released: new Date(2018, 4, 23), progress: 50, season: 2 },
|
||||
{ id: '46', episode: 8, name: 'The Loobendfeld Decay', description: 'dasdasda', released: new Date(2018, 4, 23), isUpcoming: true, season: 1 }
|
||||
],
|
||||
links: {
|
||||
share: '',
|
||||
imdb: 'https://www.imdb.com/title/tt4123430/?ref_=fn_al_tt_3',
|
||||
youtube: '#/player'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default Detail;
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ const renderProgress = (progress) => {
|
|||
|
||||
const Video = (props) => {
|
||||
return (
|
||||
<div onClick={props.onVideoClicked} className={classnames(styles['video-container'], props.className)}>
|
||||
<div className={classnames(styles['video-container'], props.className)} data-video-id={props.id} onClick={props.onClick}>
|
||||
<div className={styles['flex-row-container']}>
|
||||
{renderPoster(props.poster)}
|
||||
<div className={styles['info-container']}>
|
||||
|
|
@ -106,14 +106,16 @@ const Video = (props) => {
|
|||
|
||||
Video.propTypes = {
|
||||
className: PropTypes.string,
|
||||
id: PropTypes.string.isRequired,
|
||||
poster: PropTypes.string.isRequired,
|
||||
episode: PropTypes.number.isRequired,
|
||||
season: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
released: PropTypes.instanceOf(Date).isRequired,
|
||||
isWatched: PropTypes.bool.isRequired,
|
||||
isUpcoming: PropTypes.bool.isRequired,
|
||||
progress: PropTypes.number.isRequired,
|
||||
onVideoClicked: PropTypes.func
|
||||
onClick: PropTypes.func
|
||||
};
|
||||
Video.defaultProps = {
|
||||
poster: '',
|
||||
|
|
|
|||
|
|
@ -1,15 +1,6 @@
|
|||
.video-container {
|
||||
--video-width: 360px;
|
||||
--spacing: 8px;
|
||||
--title-font-size: 12px;
|
||||
--released-date-font-size: 11px;
|
||||
--label-font-size: 10px;
|
||||
--label-border-width: 2px;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
width: var(--video-width);
|
||||
background-color: var(--color-backgroundlight);
|
||||
background-color: var(--color-surfacedarker60);
|
||||
|
||||
.flex-row-container {
|
||||
display: flex;
|
||||
|
|
@ -40,21 +31,21 @@
|
|||
|
||||
.info-container {
|
||||
flex: 3;
|
||||
min-height: calc(0.2 * var(--video-width));
|
||||
min-height: calc(var(--video-width) * 0.2);
|
||||
padding: var(--spacing);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.title {
|
||||
font-size: var(--title-font-size);
|
||||
color: var(--color-surfacelighter);
|
||||
line-height: 1.2em;
|
||||
color: var(--color-surfacelight);
|
||||
word-break: break-all; //Firefox doesn't support { break-word }
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.released-date {
|
||||
font-size: var(--released-date-font-size);
|
||||
font-size: 0.9em;
|
||||
color: var(--color-surface);
|
||||
}
|
||||
|
||||
|
|
@ -62,13 +53,13 @@
|
|||
display: flex;
|
||||
|
||||
.upcoming-label, .watched-label {
|
||||
font-size: var(--label-font-size);
|
||||
font-weight: 600;
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
border-width: var(--label-border-width);
|
||||
border-width: calc(var(--spacing) * 0.25);
|
||||
border-style: solid;
|
||||
padding: 0 0.6em;
|
||||
color: var(--color-surfacelighter);
|
||||
color: var(--color-surfacelight);
|
||||
}
|
||||
|
||||
.upcoming-label {
|
||||
|
|
@ -82,25 +73,25 @@
|
|||
}
|
||||
|
||||
>:not(:last-child) {
|
||||
margin-bottom: calc(0.5 * var(--spacing));
|
||||
margin-bottom: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-container {
|
||||
width: calc(0.07 * var(--video-width));
|
||||
width: calc(var(--video-width) * 0.07);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing) var(--spacing) var(--spacing) 0;
|
||||
|
||||
.arrow {
|
||||
width: 100%;
|
||||
fill: var(--color-surfacelighter);
|
||||
fill: var(--color-surfacelight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
height: calc(0.5 * var(--spacing));
|
||||
height: calc(var(--spacing) * 0.5);
|
||||
background-color: var(--color-primarydark);
|
||||
|
||||
.progress {
|
||||
|
|
@ -111,6 +102,24 @@
|
|||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--color-surfacelighter20);
|
||||
background-color: var(--color-surfacedarker);
|
||||
|
||||
.info-container {
|
||||
.title {
|
||||
color: var(--color-surfacelighter);
|
||||
}
|
||||
|
||||
.label-container {
|
||||
.upcoming-label, .watched-label {
|
||||
color: var(--color-surfacelighter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-container {
|
||||
.arrow {
|
||||
fill: var(--color-surfacelighter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import Icon from 'stremio-icons/dom';
|
||||
import { Popup } from 'stremio-common';
|
||||
import Video from './Video';
|
||||
import styles from './styles';
|
||||
|
||||
|
|
@ -8,20 +10,25 @@ class VideosList extends Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.seasonsPopupRef = React.createRef();
|
||||
this.seasons = this.props.videos.map((video) => video.season)
|
||||
.filter((season, index, seasons) => seasons.indexOf(season) === index);
|
||||
|
||||
this.state = {
|
||||
selectedSeason: this.seasons[0]
|
||||
selectedSeason: this.seasons[0],
|
||||
selectedVideoId: 0,
|
||||
seasonsPopupOpen: false
|
||||
}
|
||||
}
|
||||
|
||||
changeSeason = (event) => {
|
||||
this.setState({ selectedSeason: parseInt(event.target.value) });
|
||||
this.setState({ selectedSeason: parseInt(event.currentTarget.dataset.season) });
|
||||
this.seasonsPopupRef.current && this.seasonsPopupRef.current.close();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextState.selectedSeason !== this.state.selectedSeason;
|
||||
return nextState.selectedSeason !== this.state.selectedSeason ||
|
||||
nextState.seasonsPopupOpen !== this.state.seasonsPopupOpen;
|
||||
}
|
||||
|
||||
onPrevButtonClicked = () => {
|
||||
|
|
@ -34,20 +41,45 @@ class VideosList extends Component {
|
|||
this.setState({ selectedSeason: this.seasons[nextSeasonIndex] });
|
||||
}
|
||||
|
||||
onSeasonsPopupOpen = () => {
|
||||
this.setState({ seasonsPopupOpen: true });
|
||||
}
|
||||
|
||||
onSeasonsPopupClose = () => {
|
||||
this.setState({ seasonsPopupOpen: false });
|
||||
}
|
||||
|
||||
onClick = (event) => {
|
||||
this.setState({ selectedVideoId: event.currentTarget.dataset.videoId });
|
||||
console.log(event.currentTarget.dataset.videoId);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles['videos-list-container']}>
|
||||
<div className={classnames(styles['videos-list-container'], this.props.className)}>
|
||||
<div className={styles['seasons-bar']}>
|
||||
<div className={styles['button-container']} onClick={this.onPrevButtonClicked}>
|
||||
<Icon className={styles['button-icon']} icon={'ic_arrow_left'} />
|
||||
</div>
|
||||
<select value={this.state.selectedSeason} onChange={this.changeSeason}>
|
||||
{this.seasons.map((season) =>
|
||||
<option key={season} value={season}>
|
||||
{season}
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
<Popup ref={this.seasonsPopupRef} className={'detail-popup-container'} onOpen={this.onSeasonsPopupOpen} onClose={this.onSeasonsPopupClose}>
|
||||
<Popup.Label>
|
||||
<div className={classnames(styles['season-bar-button'], { 'active': this.state.seasonsPopupOpen })}>
|
||||
<div className={styles['season-label']}>Season</div>
|
||||
<div className={styles['season-number']}>{this.state.selectedSeason}</div>
|
||||
<Icon className={styles['icon']} icon={'ic_arrow_down'} />
|
||||
</div>
|
||||
</Popup.Label>
|
||||
<Popup.Menu>
|
||||
<div className={styles['popup-content']}>
|
||||
{this.seasons.map((season) =>
|
||||
<div className={classnames(styles['season'], { [styles['selected-season']]: this.state.selectedSeason === season })} key={season} data-season={season} onClick={this.changeSeason}>
|
||||
<div className={styles['season-label']}>Season</div>
|
||||
<div className={styles['season-number']}>{season}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popup.Menu>
|
||||
</Popup>
|
||||
<div className={styles['button-container']} onClick={this.onNextButtonClicked} >
|
||||
<Icon className={styles['button-icon']} icon={'ic_arrow_left'} />
|
||||
</div>
|
||||
|
|
@ -58,13 +90,16 @@ class VideosList extends Component {
|
|||
.map((video) =>
|
||||
<Video key={video.id}
|
||||
className={styles['video']}
|
||||
id={video.id}
|
||||
poster={video.poster}
|
||||
episode={video.episode}
|
||||
season={video.season}
|
||||
title={video.name}
|
||||
released={video.released}
|
||||
isWatched={video.isWatched}
|
||||
isUpcoming={video.isUpcoming}
|
||||
progress={video.progress}
|
||||
onClick={this.onClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -74,6 +109,7 @@ class VideosList extends Component {
|
|||
}
|
||||
|
||||
VideosList.propTypes = {
|
||||
className: PropTypes.string,
|
||||
videos: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
VideosList.defaultProps = {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,69 @@
|
|||
.videos-list-container {
|
||||
--scroll-container-width: 392px;
|
||||
--seasons-bar-height: 50px;
|
||||
--spacing: 8px;
|
||||
}
|
||||
|
||||
.videos-list-container {
|
||||
height: 100%;
|
||||
display: inline-flex;
|
||||
width: calc(var(--video-width) + var(--spacing) * 6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-background);
|
||||
align-items: center;
|
||||
background: var(--color-backgrounddarker40);
|
||||
|
||||
.seasons-bar {
|
||||
height: var(--seasons-bar-height);
|
||||
height: calc(var(--video-width) * 0.14);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing);
|
||||
|
||||
.button-container {
|
||||
width: calc(1.5 * var(--seasons-bar-height));
|
||||
.season-bar-button {
|
||||
cursor: pointer;
|
||||
width: calc(var(--video-width) * 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing);
|
||||
color: var(--color-surfacelight);
|
||||
|
||||
.season-label {
|
||||
max-width: 8em;
|
||||
max-height: 2.4em;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.2em;
|
||||
text-align: end;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.season-number {
|
||||
margin: 0 calc(var(--spacing) * 1.5) 0 var(--spacing);
|
||||
font-size: 1.2em;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: calc(var(--video-width) * 0.04);
|
||||
height: calc(var(--video-width) * 0.04);
|
||||
fill: var(--color-surfacelight);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-surfacelighter);
|
||||
background-color: var(--color-surfacedarker60);
|
||||
|
||||
.icon {
|
||||
fill: var(--color-surfacelighter);
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.active) {
|
||||
color: var(--color-backgrounddarker);
|
||||
background-color: var(--color-surfacelighter);
|
||||
|
||||
.icon {
|
||||
fill: var(--color-backgrounddarker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-container {
|
||||
cursor: pointer;
|
||||
width: calc(var(--video-width) * 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -26,18 +71,22 @@
|
|||
.button-icon {
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
fill: var(--color-surfacelighter);
|
||||
fill: var(--color-surfacelight);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-surfacelighter20);
|
||||
background-color: var(--color-surfacedarker60);
|
||||
|
||||
.button-icon {
|
||||
fill: var(--color-surfacelighter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
flex: 1;
|
||||
width: var(--scroll-container-width);
|
||||
width: calc(var(--video-width) + var(--spacing) * 4);
|
||||
padding: 0 calc(2 * var(--spacing));
|
||||
margin: 0 var(--spacing);
|
||||
overflow-y: auto;
|
||||
|
|
@ -49,14 +98,54 @@
|
|||
}
|
||||
|
||||
.scroll-container::-webkit-scrollbar {
|
||||
width: var(--spacing);
|
||||
width: var(--spacing) !important;
|
||||
}
|
||||
|
||||
.scroll-container::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-secondarylighter80);
|
||||
background-color: var(--color-secondarylighter);
|
||||
}
|
||||
|
||||
.scroll-container::-webkit-scrollbar-track {
|
||||
background-color: var(--color-backgroundlight);
|
||||
background-color: var(--color-backgroundlighter);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.detail-popup-container) {
|
||||
--border-color: var(--color-backgrounddarker80);
|
||||
|
||||
.popup-content {
|
||||
width: calc(var(--video-width) * 0.6);
|
||||
background-color: var(--color-surfacelighter);
|
||||
|
||||
.season {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: calc(var(--spacing) * 1.5);
|
||||
font-size: 1.2em;
|
||||
color: var(--color-backgrounddark);
|
||||
|
||||
.season-label {
|
||||
max-height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.season-number {
|
||||
margin-left: var(--spacing);
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
&.selected-season {
|
||||
color: var(--color-surfacelighter);
|
||||
background-color: var(--color-primarydark);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-surfacelighter);
|
||||
background-color: var(--color-primarylight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
175
src/routes/Detail/styles.less
Normal file
175
src/routes/Detail/styles.less
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
.detail-container, :global(.detail-popup-container) {
|
||||
--spacing: 8px;
|
||||
--action-button-width: 80px;
|
||||
--video-width: 360px;
|
||||
--stream-width: 360px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
.overlay-container {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--color-backgrounddarker60);
|
||||
}
|
||||
|
||||
.info-container {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 40%;
|
||||
bottom: 0;
|
||||
padding: calc(var(--spacing) * 3);
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
height: calc(var(--action-button-width) * 1.2);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logo-error {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.duration {
|
||||
display: inline-block;
|
||||
max-width: 45%;
|
||||
margin-right: 1.2em;
|
||||
font-size: 1.15em;
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--color-surfacelight);
|
||||
}
|
||||
|
||||
.release-info {
|
||||
display: inline-block;
|
||||
max-width: 45%;
|
||||
font-size: 1.15em;
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--color-surfacelight);
|
||||
}
|
||||
|
||||
.name {
|
||||
max-height: 3em;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5;
|
||||
color: var(--color-surfacelight);
|
||||
}
|
||||
|
||||
.description {
|
||||
max-height: 10.5em;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
line-height: 1.5;
|
||||
color: var(--color-surfacelight);
|
||||
}
|
||||
|
||||
.section-container {
|
||||
max-height: 3.2em;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
margin-bottom: 0.3em;
|
||||
font-size: 1.15em;
|
||||
color: var(--color-surface);
|
||||
}
|
||||
|
||||
.link {
|
||||
display: none;
|
||||
max-width: 100%;
|
||||
padding: 0.3em 0.6em;
|
||||
font-size: 1.15em;
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--color-surfacelight);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-surfacelighter);
|
||||
background-color: var(--color-surface40);
|
||||
}
|
||||
|
||||
&:nth-child(-n+6) {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons-container {
|
||||
position: absolute;
|
||||
left: calc(var(--spacing) * 3);
|
||||
bottom: calc(var(--spacing) * 3);
|
||||
|
||||
.action-button-container {
|
||||
cursor: pointer;
|
||||
width: var(--action-button-width);
|
||||
height: var(--action-button-width);
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
.icon {
|
||||
height: 30%;
|
||||
margin: 10% 0;
|
||||
fill: var(--color-surfacelight);
|
||||
}
|
||||
|
||||
.label {
|
||||
height: 2.4em;
|
||||
padding: 0 1em;
|
||||
overflow-wrap: break-word;
|
||||
overflow: hidden;
|
||||
font-size: 1.05em;
|
||||
line-height: 1.2em;
|
||||
color: var(--color-surfacelight);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-surfacedarker60);
|
||||
|
||||
.icon {
|
||||
fill: var(--color-surfacelighter);
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--color-surfacelighter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>:not(:last-child) {
|
||||
margin-bottom: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
}
|
||||
|
||||
.videos-list {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: calc(3 * var(--spacing)) 0;
|
||||
}
|
||||
}
|
||||
25
src/routes/Player/BufferingLoader/BufferingLoader.js
Normal file
25
src/routes/Player/BufferingLoader/BufferingLoader.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import styles from './styles';
|
||||
|
||||
class BufferingLoader extends PureComponent {
|
||||
render() {
|
||||
if (!this.props.buffering) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classnames(this.props.className, styles['buffering-loader-container'])}>
|
||||
<div className={styles['bufferring-loader']} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BufferingLoader.propTypes = {
|
||||
className: PropTypes.string,
|
||||
buffering: PropTypes.bool
|
||||
};
|
||||
|
||||
export default BufferingLoader;
|
||||
3
src/routes/Player/BufferingLoader/index.js
Normal file
3
src/routes/Player/BufferingLoader/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import BufferingLoader from './BufferingLoader';
|
||||
|
||||
export default BufferingLoader;
|
||||
24
src/routes/Player/BufferingLoader/styles.less
Normal file
24
src/routes/Player/BufferingLoader/styles.less
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.buffering-loader-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.bufferring-loader {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: 50%;
|
||||
border: 10px solid #f3f3f3;
|
||||
border-top: 10px solid #3498db;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,8 +9,8 @@ import VolumeBar from './VolumeBar';
|
|||
import SubtitlesPicker from './SubtitlesPicker';
|
||||
import styles from './styles';
|
||||
|
||||
const ControlBarButton = React.forwardRef(({ icon, active, onClick }, ref) => (
|
||||
<div ref={ref} className={classnames(styles['control-bar-button'], { 'active': active })} onClick={onClick}>
|
||||
const ControlBarButton = React.forwardRef(({ icon, active, disabled, onClick }, ref) => (
|
||||
<div ref={ref} className={classnames(styles['control-bar-button'], { 'active': active }, { 'disabled': disabled })} onClick={!disabled ? onClick : null}>
|
||||
<Icon className={styles['icon']} icon={icon} />
|
||||
</div>
|
||||
));
|
||||
|
|
@ -109,15 +109,12 @@ class ControlBar extends Component {
|
|||
}
|
||||
|
||||
renderSubtitlesButton() {
|
||||
if (this.props.subtitleTracks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popup className={'player-popup-container'} border={true} onOpen={this.onSubtitlesPopupOpen} onClose={this.onSubtitlesPopupClose}>
|
||||
<Popup.Label>
|
||||
<ControlBarButton
|
||||
icon={'ic_sub'}
|
||||
disabled={this.props.subtitleTracks.length === 0}
|
||||
active={this.state.subtitlesPopupOpen}
|
||||
/>
|
||||
</Popup.Label>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class PlayPauseButton extends Component {
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
|
|
@ -20,4 +21,14 @@ class PlayPauseButton extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
PlayPauseButton.propTypes = {
|
||||
paused: PropTypes.bool,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
toggleButtonComponent: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.string,
|
||||
PropTypes.shape({ render: PropTypes.func.isRequired }),
|
||||
]).isRequired
|
||||
};
|
||||
|
||||
export default PlayPauseButton;
|
||||
|
|
|
|||
|
|
@ -81,7 +81,11 @@ class VolumeBar extends Component {
|
|||
VolumeBar.propTypes = {
|
||||
className: PropTypes.string,
|
||||
volume: PropTypes.number,
|
||||
toggleButtonComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
|
||||
toggleButtonComponent: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.string,
|
||||
PropTypes.shape({ render: PropTypes.func.isRequired }),
|
||||
]).isRequired,
|
||||
dispatch: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:global(.disabled) {
|
||||
cursor: default;
|
||||
|
||||
.icon {
|
||||
fill: var(--color-surfacedark);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(:global(.disabled)) {
|
||||
.icon {
|
||||
fill: var(--color-surfacelighter);
|
||||
}
|
||||
|
|
@ -80,7 +88,7 @@
|
|||
}
|
||||
|
||||
:global(.player-popup-container) {
|
||||
--border-color: var(--color-primarylight);
|
||||
--border-color: var(--color-surfacelighter);
|
||||
|
||||
.popup-content {
|
||||
background-color: var(--color-backgrounddark);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import Video from './Video';
|
||||
import BufferingLoader from './BufferingLoader';
|
||||
import ControlBar from './ControlBar';
|
||||
import styles from './styles';
|
||||
|
||||
|
|
@ -15,6 +16,7 @@ class Player extends Component {
|
|||
paused: null,
|
||||
time: null,
|
||||
duration: null,
|
||||
buffering: null,
|
||||
volume: null,
|
||||
subtitleTracks: [],
|
||||
selectedSubtitleTrackId: null,
|
||||
|
|
@ -28,6 +30,7 @@ class Player extends Component {
|
|||
return nextState.paused !== this.state.paused ||
|
||||
nextState.time !== this.state.time ||
|
||||
nextState.duration !== this.state.duration ||
|
||||
nextState.buffering !== this.state.buffering ||
|
||||
nextState.volume !== this.state.volume ||
|
||||
nextState.subtitleTracks !== this.state.subtitleTracks ||
|
||||
nextState.selectedSubtitleTrackId !== this.state.selectedSubtitleTrackId ||
|
||||
|
|
@ -43,6 +46,7 @@ class Player extends Component {
|
|||
label: 'English'
|
||||
}]);
|
||||
this.dispatch('setProp', 'selectedSubtitleTrackId', 'https://raw.githubusercontent.com/caitp/ng-media/master/example/assets/captions/bunny-en.vtt');
|
||||
this.dispatch('command', 'load', this.props.stream, {});
|
||||
}
|
||||
|
||||
onEnded = () => {
|
||||
|
|
@ -71,7 +75,6 @@ class Player extends Component {
|
|||
<Video
|
||||
ref={this.videoRef}
|
||||
className={styles['layer']}
|
||||
stream={this.props.stream}
|
||||
onEnded={this.onEnded}
|
||||
onError={this.onError}
|
||||
onPropValue={this.onPropValue}
|
||||
|
|
@ -82,6 +85,15 @@ class Player extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
renderBufferingLoader() {
|
||||
return (
|
||||
<BufferingLoader
|
||||
className={styles['layer']}
|
||||
buffering={this.state.buffering}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderControlBar() {
|
||||
return (
|
||||
<ControlBar
|
||||
|
|
@ -104,6 +116,7 @@ class Player extends Component {
|
|||
return (
|
||||
<div className={styles['player-container']}>
|
||||
{this.renderVideo()}
|
||||
{this.renderBufferingLoader()}
|
||||
{this.renderControlBar()}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,19 +13,6 @@ class Video extends Component {
|
|||
this.video = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const Video = this.selectVideoImplementation();
|
||||
this.video = new Video(this.containerRef.current);
|
||||
this.video.on('ended', this.props.onEnded);
|
||||
this.video.on('error', this.props.onError);
|
||||
this.video.on('propValue', this.props.onPropValue);
|
||||
this.video.on('propChanged', this.props.onPropChanged);
|
||||
this.video.constructor.manifest.props.forEach((propName) => {
|
||||
this.dispatch('observeProp', propName);
|
||||
});
|
||||
this.dispatch('command', 'load', this.props.stream, this.props.extra);
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -34,8 +21,8 @@ class Video extends Component {
|
|||
this.dispatch('command', 'destroy');
|
||||
}
|
||||
|
||||
selectVideoImplementation = () => {
|
||||
if (this.props.stream.ytId) {
|
||||
selectVideoImplementation = (stream, extra) => {
|
||||
if (stream.ytId) {
|
||||
return YouTubeVideo;
|
||||
} else {
|
||||
return HTMLVideo;
|
||||
|
|
@ -43,6 +30,21 @@ class Video extends Component {
|
|||
}
|
||||
|
||||
dispatch = (...args) => {
|
||||
if (args[0] === 'command' && args[1] === 'load') {
|
||||
const Video = this.selectVideoImplementation(args[2], args[3]);
|
||||
if (this.video === null || this.video.constructor !== Video) {
|
||||
this.dispatch('command', 'destroy');
|
||||
this.video = new Video(this.containerRef.current);
|
||||
this.video.on('ended', this.props.onEnded);
|
||||
this.video.on('error', this.props.onError);
|
||||
this.video.on('propValue', this.props.onPropValue);
|
||||
this.video.on('propChanged', this.props.onPropChanged);
|
||||
this.video.constructor.manifest.props.forEach((propName) => {
|
||||
this.dispatch('observeProp', propName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.video && this.video.dispatch(...args);
|
||||
} catch (e) {
|
||||
|
|
@ -59,15 +61,10 @@ class Video extends Component {
|
|||
|
||||
Video.propTypes = {
|
||||
className: PropTypes.string,
|
||||
stream: PropTypes.object.isRequired,
|
||||
extra: PropTypes.object.isRequired,
|
||||
onEnded: PropTypes.func.isRequired,
|
||||
onError: PropTypes.func.isRequired,
|
||||
onPropValue: PropTypes.func.isRequired,
|
||||
onPropChanged: PropTypes.func.isRequired
|
||||
};
|
||||
Video.defaultProps = {
|
||||
extra: Object.freeze({})
|
||||
};
|
||||
|
||||
export default Video;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,13 @@ var HTMLVideo = function(containerElement) {
|
|||
|
||||
return Math.floor(videoElement.duration * 1000);
|
||||
}
|
||||
function getBuffering() {
|
||||
if (!loaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return videoElement.readyState < videoElement.HAVE_FUTURE_DATA;
|
||||
}
|
||||
function getVolume() {
|
||||
if (destroyed) {
|
||||
return null;
|
||||
|
|
@ -140,6 +147,9 @@ var HTMLVideo = function(containerElement) {
|
|||
function onDurationChanged() {
|
||||
events.emit('propChanged', 'duration', getDuration());
|
||||
}
|
||||
function onBufferingChanged() {
|
||||
events.emit('propChanged', 'buffering', getBuffering());
|
||||
}
|
||||
function onVolumeChanged() {
|
||||
events.emit('propChanged', 'volume', getVolume());
|
||||
}
|
||||
|
|
@ -216,6 +226,15 @@ var HTMLVideo = function(containerElement) {
|
|||
videoElement.removeEventListener('durationchange', onDurationChanged);
|
||||
videoElement.addEventListener('durationchange', onDurationChanged);
|
||||
return;
|
||||
case 'buffering':
|
||||
events.emit('propValue', 'buffering', getBuffering());
|
||||
videoElement.removeEventListener('waiting', onBufferingChanged);
|
||||
videoElement.addEventListener('waiting', onBufferingChanged);
|
||||
videoElement.removeEventListener('playing', onBufferingChanged);
|
||||
videoElement.addEventListener('playing', onBufferingChanged);
|
||||
videoElement.removeEventListener('loadeddata', onBufferingChanged);
|
||||
videoElement.addEventListener('loadeddata', onBufferingChanged);
|
||||
return;
|
||||
case 'volume':
|
||||
events.emit('propValue', 'volume', getVolume());
|
||||
videoElement.removeEventListener('volumechange', onVolumeChanged);
|
||||
|
|
@ -383,6 +402,7 @@ var HTMLVideo = function(containerElement) {
|
|||
onPausedChanged();
|
||||
onTimeChanged();
|
||||
onDurationChanged();
|
||||
onBufferingChanged();
|
||||
onSubtitleTracksChanged();
|
||||
onSelectedSubtitleTrackIdChanged();
|
||||
onSubtitleDelayChanged();
|
||||
|
|
@ -402,6 +422,8 @@ var HTMLVideo = function(containerElement) {
|
|||
onPausedChanged();
|
||||
onTimeChanged();
|
||||
onDurationChanged();
|
||||
onBufferingChanged();
|
||||
onSubtitleDelayChanged();
|
||||
updateSubtitleText();
|
||||
flushArgsQueue();
|
||||
return;
|
||||
|
|
@ -417,6 +439,9 @@ var HTMLVideo = function(containerElement) {
|
|||
videoElement.removeEventListener('timeupdate', onTimeChanged);
|
||||
videoElement.removeEventListener('durationchange', onDurationChanged);
|
||||
videoElement.removeEventListener('volumechange', onVolumeChanged);
|
||||
videoElement.removeEventListener('waiting', onBufferingChanged);
|
||||
videoElement.removeEventListener('playing', onBufferingChanged);
|
||||
videoElement.removeEventListener('loadeddata', onBufferingChanged);
|
||||
containerElement.removeChild(videoElement);
|
||||
containerElement.removeChild(stylesElement);
|
||||
containerElement.removeChild(subtitlesElement);
|
||||
|
|
@ -438,7 +463,7 @@ var HTMLVideo = function(containerElement) {
|
|||
HTMLVideo.manifest = {
|
||||
name: 'HTMLVideo',
|
||||
embedded: true,
|
||||
props: ['paused', 'time', 'duration', 'volume', 'subtitleTracks', 'selectedSubtitleTrackId', 'subtitleSize', 'subtitleDelay', 'subtitleDarkBackground']
|
||||
props: ['paused', 'time', 'duration', 'volume', 'buffering', 'subtitleTracks', 'selectedSubtitleTrackId', 'subtitleSize', 'subtitleDelay', 'subtitleDarkBackground']
|
||||
};
|
||||
|
||||
module.exports = HTMLVideo;
|
||||
|
|
|
|||
Loading…
Reference in a new issue