mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
Merge branch 'board' of github.com:Stremio/stremio-web
This commit is contained in:
commit
413ca7d8ca
47 changed files with 1341 additions and 997 deletions
40
package.json
40
package.json
|
|
@ -17,45 +17,37 @@
|
|||
"hat": "0.0.3",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"prop-types": "15.6.2",
|
||||
"react": "16.6.3",
|
||||
"react-dom": "16.6.3",
|
||||
"react-router": "4.3.1",
|
||||
"react-router-dom": "4.3.1",
|
||||
"stremio-addon-client": "git+ssh://git@github.com/Stremio/stremio-addon-client.git#v1.5.1",
|
||||
"stremio-addons": "git+ssh://git@github.com/Stremio/stremio-addons.git#v2.8.14",
|
||||
"stremio-aggregators": "git+ssh://git@github.com/Stremio/stremio-aggregators.git#v1.4.1",
|
||||
"stremio-api-client": "git+ssh://git@github.com/Stremio/stremio-api-client.git#e8459d01fdd3507113b13b02aab628d24e20515e",
|
||||
"react": "16.8.0-alpha.1",
|
||||
"react-dom": "16.8.0-alpha.1",
|
||||
"stremio-colors": "git+ssh://git@github.com/Stremio/stremio-colors.git#v2.0.3",
|
||||
"stremio-icons": "git+ssh://git@github.com/Stremio/stremio-icons.git#v1.0.2",
|
||||
"stremio-icons": "git+ssh://git@github.com/Stremio/stremio-icons.git#v1.0.4",
|
||||
"stremio-json-data": "git+ssh://git@github.com/stremio/stremio-json-data.git#v1.2.3",
|
||||
"stremio-models": "git+ssh://git@github.com/stremio/stremio-models.git#v1.51.5",
|
||||
"stremio-official-addons": "git+ssh://git@github.com/Stremio/stremio-official-addons.git#v1.1.1",
|
||||
"stremio-translations": "git+ssh://git@github.com/Stremio/stremio-translations.git#v1.41.0",
|
||||
"vtt.js": "0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.2.2",
|
||||
"@babel/plugin-proposal-class-properties": "7.2.1",
|
||||
"@babel/plugin-proposal-class-properties": "7.2.3",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.2.0",
|
||||
"@babel/preset-env": "7.2.0",
|
||||
"@babel/preset-env": "7.2.3",
|
||||
"@babel/preset-react": "7.0.0",
|
||||
"@babel/runtime": "7.2.0",
|
||||
"@storybook/addon-actions": "4.1.2",
|
||||
"@storybook/addon-links": "4.1.2",
|
||||
"@storybook/addons": "4.1.2",
|
||||
"@storybook/addon-actions": "4.1.6",
|
||||
"@storybook/addon-links": "4.1.6",
|
||||
"@storybook/addons": "4.1.6",
|
||||
"@storybook/react": "4.1.2",
|
||||
"autoprefixer": "9.4.3",
|
||||
"babel-loader": "8.0.4",
|
||||
"autoprefixer": "9.4.5",
|
||||
"babel-loader": "8.0.5",
|
||||
"copy-webpack-plugin": "4.6.0",
|
||||
"css-loader": "2.0.1",
|
||||
"css-loader": "2.1.0",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"less": "3.9.0",
|
||||
"less-loader": "4.1.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"style-loader": "0.23.1",
|
||||
"terser-webpack-plugin": "1.1.0",
|
||||
"webpack": "4.27.1",
|
||||
"webpack-cli": "3.1.2",
|
||||
"webpack-dev-server": "3.1.10"
|
||||
"terser-webpack-plugin": "1.2.1",
|
||||
"webpack": "4.28.4",
|
||||
"webpack-cli": "3.2.1",
|
||||
"webpack-dev-server": "3.1.14"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/App/App.js
Normal file
12
src/App/App.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React, { StrictMode } from 'react';
|
||||
import { Router } from 'stremio-common';
|
||||
import routerConfig from './routerConfig';
|
||||
import styles from './styles';
|
||||
|
||||
const App = () => (
|
||||
<StrictMode>
|
||||
<Router className={styles['router']} config={routerConfig} />
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
export default App;
|
||||
3
src/App/index.js
Normal file
3
src/App/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import App from './App';
|
||||
|
||||
export default App;
|
||||
|
|
@ -44,63 +44,48 @@
|
|||
|
||||
:root {
|
||||
--scroll-bar-width: 8px;
|
||||
--landscape-shape-ratio: 0.5625;
|
||||
--poster-shape-ratio: 1.464;
|
||||
--window-min-width: 1000px;
|
||||
--window-min-height: 650px;
|
||||
}
|
||||
|
||||
:global {
|
||||
* {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
border: none;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
html, body, #app, .modal-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-width: 1000px;
|
||||
min-height: 650px;
|
||||
font-family: 'Roboto', 'sans-serif';
|
||||
line-height: 1;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: var(--scroll-bar-width);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-secondarylighter80);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--color-backgroundlight);
|
||||
}
|
||||
}
|
||||
|
||||
#app {
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.route-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
html, body, :global(#app) {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-width: var(--window-min-width);
|
||||
min-height: var(--window-min-height);
|
||||
font-family: 'Roboto', 'sans-serif';
|
||||
line-height: 1;
|
||||
background-color: var(--color-background);
|
||||
|
||||
.router {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: var(--scroll-bar-width);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-secondarylighter80);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--color-backgroundlight);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import React, { PureComponent, StrictMode } from 'react';
|
||||
import { Router } from 'stremio-common';
|
||||
import routerConfig from './routerConfig';
|
||||
import styles from './styles';
|
||||
|
||||
class App extends PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<StrictMode>
|
||||
<Router
|
||||
routeContainerClassName={styles['route-container']}
|
||||
config={routerConfig}
|
||||
/>
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import App from './app';
|
||||
|
||||
export default App;
|
||||
55
src/common/Button/Button.js
Normal file
55
src/common/Button/Button.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withFocusable } from 'stremio-common';
|
||||
|
||||
class Button extends PureComponent {
|
||||
onClick = (event) => {
|
||||
if (this.props.stopPropagation) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (typeof this.props.onClick === 'function') {
|
||||
this.props.onClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
onKeyUp = (event) => {
|
||||
if (event.which === 13) { // Enter key code
|
||||
this.onClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { forwardedRef, focusable, stopPropagation, ...props } = this.props;
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
tabIndex={focusable ? 0 : -1}
|
||||
onClick={this.onClick}
|
||||
onKeyUp={this.onKeyUp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Button.propTypes = {
|
||||
focusable: PropTypes.bool.isRequired,
|
||||
stopPropagation: PropTypes.bool.isRequired
|
||||
};
|
||||
Button.defaultProps = {
|
||||
focusable: false,
|
||||
stopPropagation: true
|
||||
};
|
||||
|
||||
const ButtonWithFocusable = withFocusable(Button);
|
||||
|
||||
ButtonWithFocusable.displayName = 'ButtonWithFocusable';
|
||||
|
||||
const ButtonWithForwardedRef = React.forwardRef((props, ref) => (
|
||||
<ButtonWithFocusable {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
ButtonWithForwardedRef.displayName = 'ButtonWithForwardedRef';
|
||||
|
||||
export default ButtonWithForwardedRef;
|
||||
3
src/common/Button/index.js
Normal file
3
src/common/Button/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Button from './Button';
|
||||
|
||||
export default Button;
|
||||
7
src/common/Focusable/FocusableContext.js
Normal file
7
src/common/Focusable/FocusableContext.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
const FocusableContext = React.createContext(false);
|
||||
|
||||
FocusableContext.displayName = 'FocusableContext';
|
||||
|
||||
export default FocusableContext;
|
||||
7
src/common/Focusable/index.js
Normal file
7
src/common/Focusable/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import FocusableContext from './FocusableContext';
|
||||
import withFocusable from './withFocusable';
|
||||
|
||||
export {
|
||||
FocusableContext,
|
||||
withFocusable
|
||||
};
|
||||
14
src/common/Focusable/withFocusable.js
Normal file
14
src/common/Focusable/withFocusable.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import FocusableContext from './FocusableContext';
|
||||
|
||||
const withFocusable = (Component) => {
|
||||
return function withFocusable(props) {
|
||||
return (
|
||||
<FocusableContext.Consumer>
|
||||
{focusable => <Component {...props} focusable={focusable} />}
|
||||
</FocusableContext.Consumer>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default withFocusable;
|
||||
|
|
@ -1,173 +1,154 @@
|
|||
import React from 'react';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon, { dataUrl as iconDataUrl } from 'stremio-icons/dom';
|
||||
import colors from 'stremio-colors';
|
||||
import { RELATIVE_POSTER_SIZE } from './constants';
|
||||
import classnames from 'classnames';
|
||||
import { Popup, Button } from 'stremio-common';
|
||||
import Icon from 'stremio-icons/dom';
|
||||
import styles from './styles';
|
||||
|
||||
const getShapeSize = (posterShape, progress) => {
|
||||
switch (posterShape) {
|
||||
case 'poster':
|
||||
return {
|
||||
width: RELATIVE_POSTER_SIZE
|
||||
};
|
||||
case 'landscape':
|
||||
return {
|
||||
width: RELATIVE_POSTER_SIZE / 0.5625
|
||||
};
|
||||
default:
|
||||
if (progress) {
|
||||
return {
|
||||
width: RELATIVE_POSTER_SIZE * 1.464
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: RELATIVE_POSTER_SIZE
|
||||
};
|
||||
}
|
||||
}
|
||||
class MetaItem extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const getPlaceholderIcon = (type) => {
|
||||
switch (type) {
|
||||
case 'tv':
|
||||
return 'ic_tv';
|
||||
case 'series':
|
||||
return 'ic_series';
|
||||
case 'channel':
|
||||
return 'ic_channels';
|
||||
default:
|
||||
return 'ic_movies';
|
||||
}
|
||||
}
|
||||
|
||||
const renderProgress = (progress) => {
|
||||
if (progress <= 0) {
|
||||
return null;
|
||||
this.state = {
|
||||
menuPopupOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles['progress-container']}>
|
||||
<div style={{ width: progress + '%' }} className={styles['progress']} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderEpisode = (episode) => {
|
||||
if (episode.length === 0) {
|
||||
return null;
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextState.menuPopupOpen !== this.state.menuPopupOpen ||
|
||||
nextProps.className !== this.props.className ||
|
||||
nextProps.popupClassName !== this.props.popupClassName ||
|
||||
nextProps.id !== this.props.id ||
|
||||
nextProps.type !== this.props.type ||
|
||||
nextProps.relativeSize !== this.props.relativeSize ||
|
||||
nextProps.posterShape !== this.props.posterShape ||
|
||||
nextProps.poster !== this.props.poster ||
|
||||
nextProps.title !== this.props.title ||
|
||||
nextProps.subtitle !== this.props.subtitle ||
|
||||
nextProps.progress !== this.props.progress ||
|
||||
nextProps.released !== this.props.released ||
|
||||
nextProps.menu !== this.props.menu;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles['episode']}>{episode}</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderTitle = (title) => {
|
||||
if (title.length === 0) {
|
||||
return null;
|
||||
onMenuPopupOpen = () => {
|
||||
this.setState({ menuPopupOpen: true });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles['title']}>{title}</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderReleaseInfo = (releaseInfo) => {
|
||||
if (releaseInfo.length === 0) {
|
||||
return null;
|
||||
onMenuPopupClose = () => {
|
||||
this.setState({ menuPopupOpen: false });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles['year']}>{releaseInfo}</div>
|
||||
);
|
||||
}
|
||||
onClick = (event) => {
|
||||
if (typeof this.props.onClick === 'function') {
|
||||
this.props.onClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
const renderPopupIcon = (onItemClicked, popup) => {
|
||||
if (!popup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div onClick={onItemClicked} className={styles['popup-icon-container']}>
|
||||
<Icon className={styles['popup-icon']} icon={'ic_more'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
renderProgress() {
|
||||
if (this.props.progress <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getClassName = (progress, posterShape, title, releaseInfo, episode) => {
|
||||
if ((progress > 0) && (title.length > 0 || releaseInfo.length > 0 || episode.length > 0)) {
|
||||
if (posterShape === 'landscape') return 'progress-info-landscape-shape';
|
||||
if (posterShape === 'square') return 'progress-info-square-shape';
|
||||
return 'progress-info-poster-shape';
|
||||
return (
|
||||
<div className={styles['progress-bar-container']}>
|
||||
<div className={styles['progress']} style={{ width: `${this.props.progress * 100}%` }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if ((progress > 0) && (title.length === 0 && releaseInfo.length === 0 && episode.length === 0)) {
|
||||
if (posterShape === 'landscape') return 'progress-landscape-shape';
|
||||
if (posterShape === 'square') return 'progress-square-shape';
|
||||
return 'progress-poster-shape';
|
||||
}
|
||||
if (!progress && (title.length > 0 || releaseInfo.length > 0 || episode.length > 0)) {
|
||||
if (posterShape === 'landscape') return 'info-landscape-shape';
|
||||
if (posterShape === 'square') return 'info-square-shape';
|
||||
return 'info-poster-shape';
|
||||
}
|
||||
if (!progress && (title.length === 0 && releaseInfo.length === 0 && episode.length === 0)) {
|
||||
if (posterShape === 'landscape') return 'landscape-shape';
|
||||
if (posterShape === 'square') return 'square-shape';
|
||||
return 'poster-shape';
|
||||
}
|
||||
return 'meta-item';
|
||||
}
|
||||
|
||||
const MetaItem = (props) => {
|
||||
const posterSize = getShapeSize(props.posterShape, props.progress);
|
||||
const contentContainerStyle = {
|
||||
width: posterSize.width
|
||||
};
|
||||
const placeholderIcon = getPlaceholderIcon(props.type);
|
||||
const placeholderIconUrl = iconDataUrl({ icon: placeholderIcon, fill: colors.accent, width: Math.round(RELATIVE_POSTER_SIZE / 2.2), height: Math.round(RELATIVE_POSTER_SIZE / 2.2) });
|
||||
const imageStyle = {
|
||||
backgroundImage: `url(${props.poster}), url('${placeholderIconUrl}')`
|
||||
};
|
||||
renderPoster() {
|
||||
const placeholderIcon = this.props.type === 'tv' ? 'ic_tv'
|
||||
: this.props.type === 'series' ? 'ic_series'
|
||||
: this.props.type === 'channel' ? 'ic_channels'
|
||||
: 'ic_movies';
|
||||
return (
|
||||
<div className={styles['poster-image-container']}>
|
||||
<Icon className={styles['placeholder-image']} icon={placeholderIcon} />
|
||||
<div className={styles['poster-image']} style={{ backgroundImage: `url('${this.props.poster}')` }} />
|
||||
{this.renderProgress()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={contentContainerStyle} className={styles[getClassName(props.progress, props.posterShape, props.title, props.releaseInfo, props.episode)]}>
|
||||
<div style={imageStyle} className={styles['poster']}>
|
||||
<div onClick={props.play} style={props.progress ? { visibility: 'visible' } : null} className={styles['play-container']}>
|
||||
<Icon className={styles['play']} icon={'ic_play'} />
|
||||
renderInfoBar() {
|
||||
if (this.props.title.length === 0 && this.props.subtitle.length === 0 && this.props.menu.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles['title-bar-container']}>
|
||||
<div className={styles['title']}>{this.props.title}</div>
|
||||
{
|
||||
this.props.menu.length > 0 ?
|
||||
<Popup className={classnames(styles['menu-popup-container'], this.props.popupClassName)} onOpen={this.onMenuPopupOpen} onClose={this.onMenuPopupClose}>
|
||||
<Popup.Label>
|
||||
<Icon
|
||||
className={classnames(styles['menu-icon'], { 'active': this.state.menuPopupOpen })}
|
||||
icon={'ic_more'}
|
||||
/>
|
||||
</Popup.Label>
|
||||
<Popup.Menu>
|
||||
<div className={styles['menu-items-container']}>
|
||||
{this.props.menu.map(({ label, onSelect }) => (
|
||||
<Button key={label} className={styles['menu-item']} onClick={onSelect}>{label}</Button>
|
||||
))}
|
||||
</div>
|
||||
</Popup.Menu>
|
||||
</Popup>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{renderProgress(props.progress)}
|
||||
<div className={styles['info']}>
|
||||
{renderEpisode(props.episode)}
|
||||
{renderTitle(props.title)}
|
||||
{renderReleaseInfo(props.releaseInfo)}
|
||||
</div>
|
||||
{renderPopupIcon(props.onItemClicked, props.popup)}
|
||||
</div>
|
||||
);
|
||||
{
|
||||
this.props.subtitle.length > 0 ?
|
||||
<div className={styles['title-bar-container']}>
|
||||
<div className={styles['title']}>{this.props.subtitle}</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button className={classnames(styles['meta-item-container'], styles[`relative-size-${this.props.relativeSize}`], styles[`poster-shape-${this.props.posterShape}`], this.props.className)} data-meta-item-id={this.props.id} onClick={this.onClick}>
|
||||
{this.renderPoster()}
|
||||
{this.renderInfoBar()}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MetaItem.propTypes = {
|
||||
type: PropTypes.oneOf(['movie', 'series', 'channel', 'tv', 'other']).isRequired,
|
||||
poster: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
popupClassName: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
relativeSize: PropTypes.oneOf(['auto', 'height']).isRequired,
|
||||
posterShape: PropTypes.oneOf(['poster', 'landscape', 'square']).isRequired,
|
||||
progress: PropTypes.number.isRequired,
|
||||
episode: PropTypes.string.isRequired,
|
||||
poster: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
releaseInfo: PropTypes.string.isRequired,
|
||||
popup: PropTypes.bool.isRequired,
|
||||
play: PropTypes.func,
|
||||
onItemClicked: PropTypes.func
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
progress: PropTypes.number.isRequired,
|
||||
released: PropTypes.instanceOf(Date).isRequired,
|
||||
menu: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
onSelect: PropTypes.func.isRequired
|
||||
})).isRequired
|
||||
};
|
||||
MetaItem.defaultProps = {
|
||||
type: 'other',
|
||||
relativeSize: 'auto',
|
||||
posterShape: 'square',
|
||||
poster: '',
|
||||
posterShape: 'poster',
|
||||
progress: 0,
|
||||
episode: '',
|
||||
title: '',
|
||||
releaseInfo: '',
|
||||
popup: false
|
||||
subtitle: '',
|
||||
progress: 0,
|
||||
released: new Date(NaN),
|
||||
menu: Object.freeze([])
|
||||
};
|
||||
|
||||
export default MetaItem;
|
||||
export default MetaItem;
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"RELATIVE_POSTER_SIZE": 138
|
||||
}
|
||||
|
|
@ -1,153 +1,152 @@
|
|||
.meta-item, .progress-poster-shape, .progress-landscape-shape, .progress-square-shape, .progress-info-poster-shape, .progress-info-landscape-shape, .progress-info-square-shape, .poster-shape, .landscape-shape, .square-shape, .info-poster-shape, .info-landscape-shape, .info-square-shape {
|
||||
display: grid;
|
||||
color: var(--color-surfacelighter);
|
||||
.poster {
|
||||
grid-area: poster;
|
||||
.meta-item-container {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-backgroundlight);
|
||||
border: calc(var(--progress-bar-size) * 0.5) solid transparent;
|
||||
cursor: pointer;
|
||||
|
||||
.poster-image-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
background-position: center;
|
||||
background-size: cover, auto;
|
||||
background-repeat: no-repeat;
|
||||
background-color: var(--color-backgrounddark);
|
||||
.play-container {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
visibility: hidden;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-surfacelighter);
|
||||
.play {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin: auto;
|
||||
margin-left: 26px;
|
||||
fill: var(--color-primary);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
|
||||
.placeholder-image {
|
||||
width: calc(var(--poster-relative-size) * 0.5);
|
||||
height: calc(var(--poster-relative-size) * 0.5);
|
||||
fill: var(--color-surfacelighter);
|
||||
}
|
||||
|
||||
.poster-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-origin: border-box;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
height: var(--progress-bar-size);
|
||||
background-color: var(--color-backgroundlighter);
|
||||
box-shadow: 0 0 calc(var(--progress-bar-size) * 4) calc(var(--progress-bar-size) * 1.5) var(--color-backgroundlight);
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.progress-container {
|
||||
grid-area: progress;
|
||||
background-color: var(--color-surface);
|
||||
.progress {
|
||||
height: 4px;
|
||||
background-color: var(--color-primarylight);
|
||||
}
|
||||
}
|
||||
.info {
|
||||
grid-area: info;
|
||||
.title, .year, .episode {
|
||||
grid-area: text;
|
||||
color: var(--color-surfacelighter60);
|
||||
}
|
||||
:first-child {
|
||||
|
||||
.title-bar-container {
|
||||
height: 3em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
font-size: 1.4em;
|
||||
padding: 0 0.5em;
|
||||
color: var(--color-surfacelighter);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 3em;
|
||||
height: 3em;
|
||||
padding: 0.8em;
|
||||
fill: var(--color-surfacelighter);
|
||||
|
||||
&:hover, &:global(.active) {
|
||||
background-color: var(--color-backgroundlighter);
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup-icon-container {
|
||||
grid-area: popupIcon;
|
||||
cursor: pointer;
|
||||
fill: var(--color-surfacelighter);
|
||||
.popup-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
&.poster-shape-square {
|
||||
.poster-image-container {
|
||||
width: var(--poster-relative-size);
|
||||
height: var(--poster-relative-size);
|
||||
}
|
||||
|
||||
.title-bar-container {
|
||||
width: var(--poster-relative-size);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: var(--color-backgrounddarker);
|
||||
background-color: var(--color-surfacelighter);
|
||||
outline: 2px solid var(--color-surfacelighter);
|
||||
.play-container {
|
||||
|
||||
&.poster-shape-landscape {
|
||||
.poster-image-container {
|
||||
width: calc(var(--poster-relative-size) / var(--landscape-shape-ratio));
|
||||
height: var(--poster-relative-size);
|
||||
}
|
||||
|
||||
.title-bar-container {
|
||||
width: calc(var(--poster-relative-size) / var(--landscape-shape-ratio));
|
||||
}
|
||||
}
|
||||
|
||||
&.poster-shape-poster {
|
||||
&.relative-size-auto {
|
||||
.poster-image-container {
|
||||
width: var(--poster-relative-size);
|
||||
height: calc(var(--poster-relative-size) * var(--poster-shape-ratio));
|
||||
}
|
||||
|
||||
.title-bar-container {
|
||||
width: var(--poster-relative-size);
|
||||
}
|
||||
}
|
||||
|
||||
&.relative-size-height {
|
||||
.poster-image-container {
|
||||
width: calc(var(--poster-relative-size) / var(--poster-shape-ratio));
|
||||
height: var(--poster-relative-size);
|
||||
}
|
||||
|
||||
.title-bar-container {
|
||||
width: calc(var(--poster-relative-size) / var(--poster-shape-ratio));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: var(--color-surfacelighter);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-popup-container {
|
||||
--box-shadow: 0 0 2em .15em var(--color-background);
|
||||
|
||||
.menu-items-container {
|
||||
background-color: var(--color-surfacelighter);
|
||||
min-width: calc(var(--poster-relative-size) * 0.6);
|
||||
max-width: var(--poster-relative-size);
|
||||
|
||||
.menu-item {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
color: var(--color-backgrounddarker);
|
||||
font-size: 1.2em;
|
||||
padding: 0.5em;
|
||||
cursor: pointer;
|
||||
visibility: visible;
|
||||
}
|
||||
.info {
|
||||
.title, .year {
|
||||
color: var(--color-backgrounddarker60);
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: var(--color-surfacelight);
|
||||
}
|
||||
:first-child {
|
||||
color: var(--color-backgrounddarker);
|
||||
}
|
||||
}
|
||||
.popup-icon {
|
||||
fill: var(--color-backgrounddarker40);
|
||||
}
|
||||
}
|
||||
}
|
||||
.progress-poster-shape, .progress-landscape-shape, .progress-square-shape {
|
||||
grid-template-areas:
|
||||
"poster poster"
|
||||
"progress progress"
|
||||
". popupIcon";
|
||||
}
|
||||
.progress-poster-shape {
|
||||
grid-template-columns: 128px 10px;
|
||||
grid-template-rows: 202.03px 4px;
|
||||
}
|
||||
.progress-landscape-shape {
|
||||
grid-template-columns: 235.33px 10px;
|
||||
grid-template-rows: 202.03px 4px;
|
||||
}
|
||||
.progress-square-shape {
|
||||
grid-template-columns: 192.33px 10px;
|
||||
grid-template-rows: 202.03px 4px;
|
||||
}
|
||||
.progress-info-poster-shape, .progress-info-landscape-shape, .progress-info-square-shape {
|
||||
grid-template-areas:
|
||||
"poster poster"
|
||||
"progress progress"
|
||||
". ."
|
||||
"info popupIcon";
|
||||
}
|
||||
.progress-info-poster-shape {
|
||||
grid-template-columns: 128px 10px;
|
||||
grid-template-rows: 202.03px 4px 6px 77.97px;
|
||||
}
|
||||
.progress-info-landscape-shape {
|
||||
grid-template-columns: 235.33px 10px;
|
||||
grid-template-rows: 202.03px 4px 6px 77.97px;
|
||||
}
|
||||
.progress-info-square-shape {
|
||||
grid-template-columns: 192.33px 10px;
|
||||
grid-template-rows: 202.03px 4px 6px 77.97px;
|
||||
}
|
||||
.poster-shape, .landscape-shape, .square-shape {
|
||||
grid-template-areas:
|
||||
"poster poster"
|
||||
". popupIcon";
|
||||
}
|
||||
.poster-shape {
|
||||
grid-template-columns: 128px 10px;
|
||||
grid-template-rows: 202.03px;
|
||||
}
|
||||
.landscape-shape {
|
||||
grid-template-columns: 235.33px 10px;
|
||||
grid-template-rows: 138px;
|
||||
}
|
||||
.square-shape {
|
||||
grid-template-columns: 128px 10px;
|
||||
grid-template-rows: 138px;
|
||||
}
|
||||
.info-poster-shape, .info-landscape-shape, .info-square-shape {
|
||||
grid-template-areas:
|
||||
"poster poster"
|
||||
". ."
|
||||
"info popupIcon";
|
||||
}
|
||||
.info-poster-shape {
|
||||
grid-template-columns: 128px 10px;
|
||||
grid-template-rows: 202.03px 6px 81.97px;
|
||||
}
|
||||
.info-landscape-shape {
|
||||
grid-template-columns: 235.33px 10px;
|
||||
grid-template-rows: 138px 6px 64.97px;
|
||||
}
|
||||
.info-square-shape {
|
||||
grid-template-columns: 128px 10px;
|
||||
grid-template-rows: 138px 6px 64.97px;
|
||||
}
|
||||
.meta-item {
|
||||
grid-template-columns: 138px;
|
||||
grid-template-rows: 202.03px;
|
||||
grid-template-areas:
|
||||
"poster";
|
||||
}
|
||||
|
|
@ -1,6 +1,30 @@
|
|||
import React from 'react';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { FocusableContext } from 'stremio-common';
|
||||
import withModalsContainer from './withModalsContainer';
|
||||
|
||||
const Modal = (props) => ReactDOM.createPortal(<div {...props} />, document.body);
|
||||
const Modal = ({ modalsContainer, children }) => {
|
||||
const modalContainerRef = useRef(null);
|
||||
const [focusable, setFocusable] = useState(false);
|
||||
useEffect(() => {
|
||||
const nextFocusable = modalsContainer.lastElementChild === modalContainerRef.current;
|
||||
if (nextFocusable !== focusable) {
|
||||
setFocusable(nextFocusable);
|
||||
}
|
||||
});
|
||||
|
||||
export default Modal;
|
||||
return ReactDOM.createPortal(
|
||||
<FocusableContext.Provider value={focusable}>
|
||||
<div ref={modalContainerRef} className={'modal-container'}>
|
||||
{children}
|
||||
</div>
|
||||
</FocusableContext.Provider>,
|
||||
modalsContainer
|
||||
);
|
||||
};
|
||||
|
||||
const ModalWithModalsContainer = withModalsContainer(Modal);
|
||||
|
||||
ModalWithModalsContainer.displayName = 'ModalWithModalsContainer';
|
||||
|
||||
export default ModalWithModalsContainer;
|
||||
|
|
|
|||
8
src/common/Modal/ModalsContainerContext.js
Normal file
8
src/common/Modal/ModalsContainerContext.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
const ModalsContainerContext = React.createContext(null);
|
||||
|
||||
ModalsContainerContext.displayName = 'ModalsContainerContext';
|
||||
|
||||
export default ModalsContainerContext;
|
||||
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
import ModalsContainerContext from './ModalsContainerContext';
|
||||
import Modal from './Modal';
|
||||
import withModalsContainer from './withModalsContainer';
|
||||
|
||||
export default Modal;
|
||||
export {
|
||||
ModalsContainerContext,
|
||||
Modal,
|
||||
withModalsContainer
|
||||
};
|
||||
|
|
|
|||
14
src/common/Modal/withModalsContainer.js
Normal file
14
src/common/Modal/withModalsContainer.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import ModalsContainerContext from './ModalsContainerContext';
|
||||
|
||||
const withModalsContainer = (Component) => {
|
||||
return function withModalsContainer(props) {
|
||||
return (
|
||||
<ModalsContainerContext.Consumer>
|
||||
{modalsContainer => <Component {...props} modalsContainer={modalsContainer} />}
|
||||
</ModalsContainerContext.Consumer>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default withModalsContainer;
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import Icon from 'stremio-icons/dom';
|
||||
import styles from './styles';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React, { Component } from 'react';
|
||||
import { matchPath, withRouter } from 'react-router-dom';
|
||||
import Icon from 'stremio-icons/dom';
|
||||
import classnames from 'classnames';
|
||||
import styles from './styles';
|
||||
|
|
@ -86,4 +85,4 @@ class SearchInput extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default withRouter(SearchInput);
|
||||
export default SearchInput;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
const Label = ({ children, ...props }, ref) => {
|
||||
const Label = React.forwardRef(({ children, ...props }, ref) => {
|
||||
return React.cloneElement(React.Children.only(children), { ...props, ref });
|
||||
};
|
||||
});
|
||||
|
||||
export default React.forwardRef(Label);
|
||||
Label.displayName = 'Popup.Label';
|
||||
|
||||
export default Label;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
const Menu = ({ children }) => {
|
||||
return React.Children.only(children);
|
||||
};
|
||||
const Menu = React.forwardRef(({ children }, ref) => (
|
||||
<div ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
|
||||
Menu.displayName = 'Popup.Menu';
|
||||
|
||||
export default Menu;
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ class Popup extends Component {
|
|||
this.popupMutationObserver.observe(document.documentElement, {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
subtree: true
|
||||
});
|
||||
if (typeof this.props.onOpen === 'function') {
|
||||
|
|
@ -107,15 +108,15 @@ class Popup extends Component {
|
|||
this.labelBorderLeftRef.current.removeAttribute('style');
|
||||
|
||||
const menuDirections = {};
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const documentRect = document.documentElement.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,
|
||||
top: labelRect.y - bodyRect.y,
|
||||
right: (bodyRect.width + bodyRect.x) - (labelRect.x + labelRect.width),
|
||||
bottom: (bodyRect.height + bodyRect.y) - (labelRect.y + labelRect.height)
|
||||
left: labelRect.x - documentRect.x,
|
||||
top: labelRect.y - documentRect.y,
|
||||
right: (documentRect.width + documentRect.x) - (labelRect.x + labelRect.width),
|
||||
bottom: (documentRect.height + documentRect.y) - (labelRect.y + labelRect.height)
|
||||
};
|
||||
|
||||
if (menuChildredRect.height <= labelPosition.bottom) {
|
||||
|
|
@ -211,12 +212,22 @@ class Popup extends Component {
|
|||
this.setState({ open: false });
|
||||
}
|
||||
|
||||
labelOnClick = (event) => {
|
||||
event.stopPropagation();
|
||||
this.open();
|
||||
}
|
||||
|
||||
menuContainerOnClick = (event) => {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
modalBackgroundOnClick = (event) => {
|
||||
event.stopPropagation();
|
||||
this.close();
|
||||
}
|
||||
|
||||
renderLabel(children) {
|
||||
return React.cloneElement(children, { ref: this.labelRef, onClick: this.open });
|
||||
return React.cloneElement(children, { ref: this.labelRef, onClick: this.labelOnClick });
|
||||
}
|
||||
|
||||
renderMenu(children) {
|
||||
|
|
@ -225,23 +236,23 @@ class Popup extends Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<Modal className={classnames('modal-container', this.props.className)} onClick={this.close}>
|
||||
<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}
|
||||
<Modal>
|
||||
<div className={classnames(styles['modal-container'], this.props.className)} onClick={this.modalBackgroundOnClick}>
|
||||
<div ref={this.menuContainerRef} className={styles['menu-container']} onClick={this.menuContainerOnClick}>
|
||||
<div ref={this.menuScrollRef} className={styles['menu-scroll-container']}>
|
||||
{React.cloneElement(children, { ref: this.menuChildrenRef })}
|
||||
</div>
|
||||
<div ref={this.menuBorderTopRef} className={classnames(styles['border'], styles['border-top'])} />
|
||||
<div ref={this.menuBorderRightRef} className={classnames(styles['border'], styles['border-right'])} />
|
||||
<div ref={this.menuBorderBottomRef} className={classnames(styles['border'], styles['border-bottom'])} />
|
||||
<div ref={this.menuBorderLeftRef} className={classnames(styles['border'], styles['border-left'])} />
|
||||
</div>
|
||||
<div ref={this.menuBorderTopRef} className={classnames(styles['border'], styles['border-top'])} />
|
||||
<div ref={this.menuBorderRightRef} className={classnames(styles['border'], styles['border-right'])} />
|
||||
<div ref={this.menuBorderBottomRef} className={classnames(styles['border'], styles['border-bottom'])} />
|
||||
<div ref={this.menuBorderLeftRef} className={classnames(styles['border'], styles['border-left'])} />
|
||||
<div ref={this.labelBorderTopRef} className={classnames(styles['border'], styles['border-top'])} />
|
||||
<div ref={this.labelBorderRightRef} className={classnames(styles['border'], styles['border-right'])} />
|
||||
<div ref={this.labelBorderBottomRef} className={classnames(styles['border'], styles['border-bottom'])} />
|
||||
<div ref={this.labelBorderLeftRef} className={classnames(styles['border'], styles['border-left'])} />
|
||||
<div ref={this.hiddenBorderRef} className={classnames(styles['border'], styles['border-hidden'])} />
|
||||
</div>
|
||||
<div ref={this.labelBorderTopRef} className={classnames(styles['border'], styles['border-top'])} />
|
||||
<div ref={this.labelBorderRightRef} className={classnames(styles['border'], styles['border-right'])} />
|
||||
<div ref={this.labelBorderBottomRef} className={classnames(styles['border'], styles['border-bottom'])} />
|
||||
<div ref={this.labelBorderLeftRef} className={classnames(styles['border'], styles['border-left'])} />
|
||||
<div ref={this.hiddenBorderRef} className={classnames(styles['border'], styles['border-hidden'])} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,49 @@
|
|||
.menu-container {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
.modal-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.menu-scroll-container {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
.menu-container {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
|
||||
.border {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
background-color: var(--border-color);
|
||||
|
||||
&-top {
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
.menu-scroll-container {
|
||||
box-shadow: var(--box-shadow);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
.border {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
background-color: var(--border-color);
|
||||
|
||||
&-left {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
&-top {
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
&-bottom {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&-hidden {
|
||||
display: none;
|
||||
border: 1px solid;
|
||||
&-left {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&-hidden {
|
||||
display: none;
|
||||
border: 1px solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/common/Router/Route/ModalsContainerProvider.js
Normal file
38
src/common/Router/Route/ModalsContainerProvider.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ModalsContainerContext } from 'stremio-common';
|
||||
|
||||
class ModalsContainerProvider extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
modalsContainer: null
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextState.modalsContainer !== this.state.modalsContainer ||
|
||||
nextProps.modalsContainerClassName !== this.props.modalsContainerClassName ||
|
||||
nextProps.children !== this.props.children;
|
||||
}
|
||||
|
||||
modalsContainerRef = (modalsContainer) => {
|
||||
this.setState({ modalsContainer });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ModalsContainerContext.Provider value={this.state.modalsContainer}>
|
||||
{this.state.modalsContainer ? this.props.children : null}
|
||||
<div ref={this.modalsContainerRef} className={this.props.modalsContainerClassName} />
|
||||
</ModalsContainerContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ModalsContainerProvider.propTypes = {
|
||||
modalsContainerClassName: PropTypes.string
|
||||
};
|
||||
|
||||
export default ModalsContainerProvider;
|
||||
26
src/common/Router/Route/Route.js
Normal file
26
src/common/Router/Route/Route.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React, { Component } from 'react';
|
||||
import ModalsContainerProvider from './ModalsContainerProvider';
|
||||
import RouteFocusableProvider from './RouteFocusableProvider';
|
||||
import styles from './styles';
|
||||
|
||||
class Route extends Component {
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextProps.children !== this.props.children;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles['route']}>
|
||||
<ModalsContainerProvider modalsContainerClassName={styles['modals-container']}>
|
||||
<RouteFocusableProvider>
|
||||
<div className={styles['route-content']}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</RouteFocusableProvider>
|
||||
</ModalsContainerProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Route;
|
||||
58
src/common/Router/Route/RouteFocusableProvider.js
Normal file
58
src/common/Router/Route/RouteFocusableProvider.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import React, { Component } from 'react';
|
||||
import { FocusableContext, withModalsContainer } from 'stremio-common';
|
||||
|
||||
class RouteFocusableProvider extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.routeContentRef = React.createRef();
|
||||
this.modalsContainerDomTreeObserver = new MutationObserver(this.onModalsContainerDomTreeChange);
|
||||
this.state = {
|
||||
focusable: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.onModalsContainerDomTreeChange();
|
||||
this.modalsContainerDomTreeObserver.observe(this.props.modalsContainer, {
|
||||
childList: true
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.modalsContainerDomTreeObserver.disconnect();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextState.focusable !== this.state.focusable ||
|
||||
nextProps.modalsContainer !== this.props.modalsContainer ||
|
||||
nextProps.children !== this.props.children;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.focusable && !this.state.focusable) {
|
||||
const focusedElement = this.routeContentRef.current.querySelector(':focus');
|
||||
if (focusedElement !== null) {
|
||||
focusedElement.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onModalsContainerDomTreeChange = () => {
|
||||
this.setState({ focusable: this.props.modalsContainer.childElementCount === 0 });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FocusableContext.Provider value={this.state.focusable}>
|
||||
{React.cloneElement(React.Children.only(this.props.children), { ref: this.routeContentRef })}
|
||||
</FocusableContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const RouteFocusableProviderWithModalsContainer = withModalsContainer(RouteFocusableProvider);
|
||||
|
||||
RouteFocusableProviderWithModalsContainer.displayName = 'RouteFocusableProviderWithModalsContainer';
|
||||
|
||||
export default RouteFocusableProviderWithModalsContainer;
|
||||
3
src/common/Router/Route/index.js
Normal file
3
src/common/Router/Route/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Route from './Route';
|
||||
|
||||
export default Route;
|
||||
30
src/common/Router/Route/styles.less
Normal file
30
src/common/Router/Route/styles.less
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
.route {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.route-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modals-container {
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
>:global(.modal-container) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import pathToRegexp from 'path-to-regexp';
|
||||
import PathUtils from 'path';
|
||||
import UrlUtils from 'url';
|
||||
import Route from './Route';
|
||||
|
||||
class Router extends Component {
|
||||
constructor(props) {
|
||||
|
|
@ -44,7 +45,8 @@ class Router extends Component {
|
|||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextState.views !== this.state.views;
|
||||
return nextState.views !== this.state.views ||
|
||||
nextProps.className !== this.props.className;
|
||||
}
|
||||
|
||||
onLocationChanged = () => {
|
||||
|
|
@ -95,13 +97,15 @@ class Router extends Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={this.props.className}>
|
||||
{
|
||||
this.state.views
|
||||
.filter(({ element }) => React.isValidElement(element))
|
||||
.map(({ path, element }) => <div key={path} className={this.props.routeContainerClassName}>{element}</div>)
|
||||
.map(({ path, element }) => (
|
||||
<Route key={path}>{element}</Route>
|
||||
))
|
||||
}
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Checkbox from './Checkbox';
|
||||
import Popup from './Popup';
|
||||
import NavBar from './NavBar';
|
||||
import Modal from './Modal';
|
||||
import { ModalsContainerContext, Modal, withModalsContainer } from './Modal';
|
||||
import MetadataItem from './MetadataItem';
|
||||
import Router from './Router';
|
||||
import LibraryItemList from './LibraryItemList';
|
||||
|
|
@ -9,17 +9,24 @@ import MetaItem from './MetaItem';
|
|||
import ShareAddon from './ShareAddon';
|
||||
import UserPanel from './UserPanel';
|
||||
import Slider from './Slider';
|
||||
import { FocusableContext, withFocusable } from './Focusable';
|
||||
import Button from './Button';
|
||||
|
||||
export {
|
||||
Checkbox,
|
||||
Popup,
|
||||
NavBar,
|
||||
ModalsContainerContext,
|
||||
Modal,
|
||||
withModalsContainer,
|
||||
MetadataItem,
|
||||
Router,
|
||||
LibraryItemList,
|
||||
MetaItem,
|
||||
ShareAddon,
|
||||
UserPanel,
|
||||
Slider
|
||||
Slider,
|
||||
FocusableContext,
|
||||
withFocusable,
|
||||
Button
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './app';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('app'));
|
||||
|
|
|
|||
|
|
@ -1,10 +1,68 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import { MetaItem } from 'stremio-common';
|
||||
import styles from './styles';
|
||||
|
||||
class Board extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.items = {
|
||||
cw: [
|
||||
{
|
||||
id: 'cw1',
|
||||
posterShape: 'poster',
|
||||
type: 'movie',
|
||||
progress: 0.4,
|
||||
title: 'Movie title'
|
||||
}
|
||||
]
|
||||
};
|
||||
this.cwMenu = [
|
||||
{
|
||||
label: 'Play',
|
||||
onSelect: (event) => {
|
||||
console.log('Play', {
|
||||
defaultPrevented: event.isDefaultPrevented(),
|
||||
propagationStopped: event.isPropagationStopped()
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Dismiss',
|
||||
onSelect: (event) => {
|
||||
console.log('Dismiss', {
|
||||
defaultPrevented: event.isDefaultPrevented(),
|
||||
propagationStopped: event.isPropagationStopped()
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
onClick = (event) => {
|
||||
console.log('onClick', {
|
||||
id: event.currentTarget.dataset.metaItemId,
|
||||
defaultPrevented: event.isDefaultPrevented(),
|
||||
propagationStopped: event.isPropagationStopped()
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{ paddingTop: 40, color: 'yellow' }}>
|
||||
Board
|
||||
<div className={styles['board-container']}>
|
||||
<div className={styles['continue-watching-row']}>
|
||||
{this.items.cw.map((props) => (
|
||||
<MetaItem
|
||||
key={props.id}
|
||||
className={styles['meta-item-container']}
|
||||
popupClassName={styles['meta-item-popup-container']}
|
||||
relativeSize={'height'}
|
||||
menu={this.cwMenu}
|
||||
onClick={this.onClick}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
30
src/routes/Board/styles.less
Normal file
30
src/routes/Board/styles.less
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
.board-container, .meta-item-popup-container {
|
||||
--poster-relative-size: 180px;
|
||||
--progress-bar-size: 6px;
|
||||
font-size: 12px;
|
||||
|
||||
.continue-watching-row {
|
||||
--poster-relative-size: calc(180px * var(--poster-shape-ratio));
|
||||
}
|
||||
|
||||
.search-row {
|
||||
--poster-relative-size: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
.board-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
.continue-watching-row, .search-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.meta-item-container {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { NavBar } from 'stremio-common';
|
||||
import { Board, Discover, Library, Calendar, Search } from 'stremio-routes';
|
||||
import styles from './styles';
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ const ControlBarButton = React.forwardRef(({ icon, active, disabled, onClick },
|
|||
</div>
|
||||
));
|
||||
|
||||
ControlBarButton.displayName = 'ControlBarButton';
|
||||
|
||||
class ControlBar extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
@ -27,6 +29,7 @@ class ControlBar extends Component {
|
|||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextProps.className !== this.props.className ||
|
||||
nextProps.popupClassName !== this.props.popupClassName ||
|
||||
nextProps.paused !== this.props.paused ||
|
||||
nextProps.time !== this.props.time ||
|
||||
nextProps.duration !== this.props.duration ||
|
||||
|
|
@ -94,7 +97,7 @@ class ControlBar extends Component {
|
|||
|
||||
renderShareButton() {
|
||||
return (
|
||||
<Popup className={'player-popup-container'} border={true} onOpen={this.onSharePopupOpen} onClose={this.onSharePopupClose}>
|
||||
<Popup className={classnames(styles['popup-container'], this.props.popupClassName)} border={true} onOpen={this.onSharePopupOpen} onClose={this.onSharePopupClose}>
|
||||
<Popup.Label>
|
||||
<ControlBarButton
|
||||
icon={'ic_share'}
|
||||
|
|
@ -110,7 +113,7 @@ class ControlBar extends Component {
|
|||
|
||||
renderSubtitlesButton() {
|
||||
return (
|
||||
<Popup className={'player-popup-container'} border={true} onOpen={this.onSubtitlesPopupOpen} onClose={this.onSubtitlesPopupClose}>
|
||||
<Popup className={classnames(styles['popup-container'], this.props.popupClassName)} border={true} onOpen={this.onSubtitlesPopupOpen} onClose={this.onSubtitlesPopupClose}>
|
||||
<Popup.Label>
|
||||
<ControlBarButton
|
||||
icon={'ic_sub'}
|
||||
|
|
@ -151,6 +154,7 @@ class ControlBar extends Component {
|
|||
|
||||
ControlBar.propTypes = {
|
||||
className: PropTypes.string,
|
||||
popupClassName: PropTypes.string,
|
||||
paused: PropTypes.bool,
|
||||
time: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
:global(.player-popup-container) {
|
||||
.popup-container {
|
||||
--border-color: var(--color-surfacelighter);
|
||||
|
||||
.popup-content {
|
||||
|
|
|
|||
|
|
@ -40,13 +40,13 @@ class Player extends Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatch('command', 'load', this.props.stream, {});
|
||||
this.dispatch('command', 'addSubtitleTracks', [{
|
||||
url: 'https://raw.githubusercontent.com/caitp/ng-media/master/example/assets/captions/bunny-en.vtt',
|
||||
origin: 'Github',
|
||||
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 = () => {
|
||||
|
|
@ -98,6 +98,7 @@ class Player extends Component {
|
|||
return (
|
||||
<ControlBar
|
||||
className={classnames(styles['layer'], styles['control-bar-layer'])}
|
||||
popupClassName={styles['control-bar-popup-container']}
|
||||
paused={this.state.paused}
|
||||
time={this.state.time}
|
||||
duration={this.state.duration}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.player-container, :global(.player-popup-container) {
|
||||
.player-container, .control-bar-popup-container {
|
||||
--control-bar-button-size: 60px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
import StremioAPI from 'stremio-api-client';
|
||||
import storage from './storage';
|
||||
|
||||
// const API = new StremioAPI({ storage });
|
||||
|
||||
export default {};
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import API from './API';
|
||||
|
||||
export default API;
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
const storage = {
|
||||
getUser: function() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('user'));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
setUser: function(user) {
|
||||
try {
|
||||
if (user === null) {
|
||||
localStorage.removeItem('user');
|
||||
} else {
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default storage;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { AddonCollection } from 'stremio-addon-client';
|
||||
import officialAddons from 'stremio-official-addons';
|
||||
|
||||
// const addons = new AddonCollection();
|
||||
// addons.load(officialAddons);
|
||||
|
||||
export default {};
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import addons from './addons';
|
||||
|
||||
export default addons;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import addons from './addons';
|
||||
import API from './API';
|
||||
|
||||
export {
|
||||
addons,
|
||||
API
|
||||
};
|
||||
Loading…
Reference in a new issue