Merge branch 'master' of github.com:Stremio/stremio-web into detail-page

This commit is contained in:
svetlagasheva 2019-01-17 10:45:13 +02:00
commit 71e1afc0b4
23 changed files with 651 additions and 389 deletions

View file

@ -42,6 +42,10 @@
font-style: normal;
}
:root {
--scroll-bar-width: 8px;
}
:global {
* {
margin: 0px;
@ -64,7 +68,7 @@
line-height: 1;
::-webkit-scrollbar {
width: 8px;
width: var(--scroll-bar-width);
}
::-webkit-scrollbar-thumb {

View file

@ -5,22 +5,33 @@ import Icon from 'stremio-icons/dom';
import styles from './styles';
class Checkbox extends Component {
shouldComponentUpdate(nextProps, nextState) {
shouldComponentUpdate(nextProps) {
return nextProps.checked !== this.props.checked ||
nextProps.enabled !== this.props.enabled;
nextProps.disabled !== this.props.disabled ||
nextProps.className !== this.props.className;
}
onClick = () => {
if (this.props.enabled && typeof this.props.onClick === 'function') {
onClick = (event) => {
event.preventDefault();
if (typeof this.props.onClick === 'function') {
this.props.onClick();
}
}
render() {
return (
<div className={classnames(styles['root'], this.props.className, { [styles['checkbox-checked']]: this.props.checked }, { [styles['checkbox-disabled']]: !this.props.enabled })}>
<Icon className={classnames(styles['icon'])} icon={this.props.checked ? 'ic_check' : 'ic_box_empty'} />
<input type={'checkbox'} className={styles['native-checkbox']} defaultChecked={this.props.checked} disabled={!this.props.enabled} onClick={this.onClick} />
<div className={classnames(this.props.className, styles['checkbox-container'], { 'checked': this.props.checked }, { 'disabled': this.props.disabled })}>
<Icon
className={styles['icon']}
icon={this.props.checked ? 'ic_check' : 'ic_box_empty'}
/>
<input
className={styles['native-checkbox']}
type={'checkbox'}
disabled={this.props.disabled}
defaultChecked={this.props.checked}
onClick={this.onClick}
/>
</div>
);
}
@ -28,13 +39,13 @@ class Checkbox extends Component {
Checkbox.propTypes = {
className: PropTypes.string,
enabled: PropTypes.bool.isRequired,
disabled: PropTypes.bool.isRequired,
checked: PropTypes.bool.isRequired,
onClick: PropTypes.func
};
Checkbox.defaultProps = {
enabled: true,
disabled: false,
checked: false
};

View file

@ -1,18 +1,14 @@
.root {
cursor: pointer;
.checkbox-container {
position: relative;
z-index: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--background-color);
&.checkbox-checked {
background-color: var(--color-primarylight);
.icon {
padding: 10%;
fill: var(--color-surfacelighter);
}
}
&.checkbox-disabled {
opacity: 0.5;
cursor: not-allowed;
.icon {
height: 100%;
fill: var(--icon-color);
}
.native-checkbox {
@ -20,14 +16,13 @@
opacity: 0;
height: 0;
width: 0;
top: -9999px;
left: -9999px;
top: -99999999px;
left: -99999999px;
}
.icon {
width: 100%;
height: 100%;
margin: auto;
fill: var(--color-surfacelighter60);
&:global(.checked) {
.icon {
height: 55%;
}
}
}

View file

@ -10,18 +10,20 @@ class Popup extends Component {
constructor(props) {
super(props);
this.hiddenBorderRef = React.createRef();
this.labelRef = React.createRef();
this.labelBorderTopRef = React.createRef();
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,20 +51,66 @@ 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 borderWidth = parseFloat(window.getComputedStyle(this.hiddenBorderRef.current).getPropertyValue('border-top-width'));
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,
@ -69,82 +118,82 @@ 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;
}
if (this.props.border) {
this.menuBorderTopRef.current.style.height = `${borderWidth}px`;
this.menuBorderRightRef.current.style.width = `${borderWidth}px`;
this.menuBorderBottomRef.current.style.height = `${borderWidth}px`;
this.menuBorderLeftRef.current.style.width = `${borderWidth}px`;
this.labelBorderTopRef.current.style.height = `${borderWidth}px`;
this.menuBorderTopRef.current.style.height = `${borderSize}px`;
this.menuBorderRightRef.current.style.width = `${borderSize}px`;
this.menuBorderBottomRef.current.style.height = `${borderSize}px`;
this.menuBorderLeftRef.current.style.width = `${borderSize}px`;
this.labelBorderTopRef.current.style.height = `${borderSize}px`;
this.labelBorderTopRef.current.style.top = `${labelPosition.top}px`;
this.labelBorderTopRef.current.style.right = `${labelPosition.right}px`;
this.labelBorderTopRef.current.style.left = `${labelPosition.left}px`;
this.labelBorderRightRef.current.style.width = `${borderWidth}px`;
this.labelBorderRightRef.current.style.width = `${borderSize}px`;
this.labelBorderRightRef.current.style.top = `${labelPosition.top}px`;
this.labelBorderRightRef.current.style.right = `${labelPosition.right}px`;
this.labelBorderRightRef.current.style.bottom = `${labelPosition.bottom}px`;
this.labelBorderBottomRef.current.style.height = `${borderWidth}px`;
this.labelBorderBottomRef.current.style.height = `${borderSize}px`;
this.labelBorderBottomRef.current.style.right = `${labelPosition.right}px`;
this.labelBorderBottomRef.current.style.bottom = `${labelPosition.bottom}px`;
this.labelBorderBottomRef.current.style.left = `${labelPosition.left}px`;
this.labelBorderLeftRef.current.style.width = `${borderWidth}px`;
this.labelBorderLeftRef.current.style.width = `${borderSize}px`;
this.labelBorderLeftRef.current.style.top = `${labelPosition.top}px`;
this.labelBorderLeftRef.current.style.bottom = `${labelPosition.bottom}px`;
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 - borderWidth}px`;
this.menuBorderBottomRef.current.style.right = `${labelRect.width - borderSize}px`;
} else {
this.menuBorderBottomRef.current.style.left = `${labelRect.width - borderWidth}px`;
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 - borderWidth}px`;
this.menuBorderTopRef.current.style.right = `${labelRect.width - borderSize}px`;
} else {
this.menuBorderTopRef.current.style.left = `${labelRect.width - borderWidth}px`;
this.menuBorderTopRef.current.style.left = `${labelRect.width - borderSize}px`;
}
}
}
this.menuRef.current.style.visibility = 'visible';
this.menuContainerRef.current.style.visibility = 'visible';
}
onKeyUp = (event) => {
@ -177,20 +226,22 @@ 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'])} />
<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.hiddenBorderRef} className={styles['hidden-border']} />
<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>
);
}

View file

@ -2,7 +2,7 @@
position: absolute;
visibility: hidden;
.scroll-container {
.menu-scroll-container {
overflow: auto;
}
}
@ -35,9 +35,9 @@
right: 0;
bottom: 0;
}
}
.hidden-border {
display: none;
border: 1px solid;
&-hidden {
display: none;
border: 1px solid;
}
}

View file

@ -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>
);
}

View 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;

View file

@ -0,0 +1,3 @@
import BufferingLoader from './BufferingLoader';
export default BufferingLoader;

View 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;
}
}

View file

@ -4,13 +4,13 @@ import classnames from 'classnames';
import Icon from 'stremio-icons/dom';
import { Popup } from 'stremio-common';
import SeekBar from './SeekBar';
import PlayPauseButton from './PlayPauseButton';
import VolumeBar from './VolumeBar';
import SubtitlesPicker from './SubtitlesPicker';
import styles from './styles';
//TODO move this in separate file
const ControlBarButton = React.forwardRef(({ active, icon, 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>
));
@ -34,36 +34,14 @@ class ControlBar extends Component {
nextProps.subtitleTracks !== this.props.subtitleTracks ||
nextProps.selectedSubtitleTrackId !== this.props.selectedSubtitleTrackId ||
nextProps.subtitleSize !== this.props.subtitleSize ||
nextProps.subtitleDelay !== this.props.subtitleDelay ||
nextProps.subtitleDarkBackground !== this.props.subtitleDarkBackground ||
nextState.sharePopupOpen !== this.state.sharePopupOpen ||
nextState.subtitlesPopupOpen !== this.state.subtitlesPopupOpen;
}
setTime = (time) => {
this.props.setTime(time);
}
setVolume = (volume) => {
this.props.setVolume(volume);
}
setSelectedSubtitleTrackId = (selectedSubtitleTrackId) => {
this.props.setSelectedSubtitleTrackId(selectedSubtitleTrackId);
}
setSubtitleSize = (size) => {
this.props.setSubtitleSize(size);
}
mute = () => {
this.props.mute();
}
unmute = () => {
this.props.unmute();
}
togglePaused = () => {
this.props.paused ? this.props.play() : this.props.pause();
dispatch = (...args) => {
this.props.dispatch(...args);
}
onSharePopupOpen = () => {
@ -88,21 +66,17 @@ class ControlBar extends Component {
className={styles['seek-bar']}
time={this.props.time}
duration={this.props.duration}
setTime={this.setTime}
dispatch={this.dispatch}
/>
);
}
renderPlayPauseButton() {
if (this.props.paused === null) {
return null;
}
const icon = this.props.paused ? 'ic_play' : 'ic_pause';
return (
<ControlBarButton
icon={icon}
onClick={this.togglePaused}
<PlayPauseButton
toggleButtonComponent={ControlBarButton}
paused={this.props.paused}
dispatch={this.dispatch}
/>
);
}
@ -113,18 +87,19 @@ class ControlBar extends Component {
className={styles['volume-bar']}
toggleButtonComponent={ControlBarButton}
volume={this.props.volume}
setVolume={this.setVolume}
mute={this.mute}
unmute={this.unmute}
dispatch={this.dispatch}
/>
);
}
renderShareButton() {
return (
<Popup className={styles['popup-container']} border={true} onOpen={this.onSharePopupOpen} onClose={this.onSharePopupClose}>
<Popup className={'player-popup-container'} border={true} onOpen={this.onSharePopupOpen} onClose={this.onSharePopupClose}>
<Popup.Label>
<ControlBarButton active={this.state.sharePopupOpen} icon={'ic_share'} />
<ControlBarButton
icon={'ic_share'}
active={this.state.sharePopupOpen}
/>
</Popup.Label>
<Popup.Menu>
<div className={classnames(styles['popup-content'], styles['share-popup-content'])} />
@ -134,23 +109,24 @@ class ControlBar extends Component {
}
renderSubtitlesButton() {
if (this.props.subtitleTracks.length === 0) {
return null;
}
return (
<Popup className={styles['popup-container']} border={true} onOpen={this.onSubtitlesPopupOpen} onClose={this.onSubtitlesPopupClose}>
<Popup className={'player-popup-container'} border={true} onOpen={this.onSubtitlesPopupOpen} onClose={this.onSubtitlesPopupClose}>
<Popup.Label>
<ControlBarButton active={this.state.subtitlesPopupOpen} icon={'ic_sub'} />
<ControlBarButton
icon={'ic_sub'}
disabled={this.props.subtitleTracks.length === 0}
active={this.state.subtitlesPopupOpen}
/>
</Popup.Label>
<Popup.Menu>
<SubtitlesPicker
className={classnames(styles['popup-content'], styles['subtitles-popup-content'])}
subtitleTracks={this.props.subtitleTracks}
subtitleSize={this.props.subtitleSize}
selectedSubtitleTrackId={this.props.selectedSubtitleTrackId}
setSelectedSubtitleTrackId={this.setSelectedSubtitleTrackId}
setSubtitleSize={this.setSubtitleSize}
subtitleSize={this.props.subtitleSize}
subtitleDelay={this.props.subtitleDelay}
subtitleDarkBackground={this.props.subtitleDarkBackground}
dispatch={this.dispatch}
/>
</Popup.Menu>
</Popup >
@ -164,7 +140,7 @@ class ControlBar extends Component {
<div className={styles['control-bar-buttons-container']}>
{this.renderPlayPauseButton()}
{this.renderVolumeBar()}
<div className={styles['flex-spacing']} />
<div className={styles['spacing']} />
{this.renderSubtitlesButton()}
{this.renderShareButton()}
</div>
@ -185,13 +161,10 @@ ControlBar.propTypes = {
origin: PropTypes.string.isRequired
})).isRequired,
selectedSubtitleTrackId: PropTypes.string,
play: PropTypes.func.isRequired,
pause: PropTypes.func.isRequired,
setTime: PropTypes.func.isRequired,
setVolume: PropTypes.func.isRequired,
setSelectedSubtitleTrackId: PropTypes.func.isRequired,
mute: PropTypes.func.isRequired,
unmute: PropTypes.func.isRequired
subtitleSize: PropTypes.number,
subtitleDelay: PropTypes.number,
subtitleDarkBackground: PropTypes.bool,
dispatch: PropTypes.func.isRequired
};
export default ControlBar;

View file

@ -0,0 +1,34 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class PlayPauseButton extends Component {
shouldComponentUpdate(nextProps, nextState) {
return nextProps.paused !== this.props.paused ||
nextProps.toggleButtonComponent !== this.props.toggleButtonComponent;
}
togglePaused = () => {
this.props.dispatch('setProp', 'paused', !this.props.paused);
}
render() {
if (this.props.paused === null) {
return null;
}
const icon = this.props.paused ? 'ic_play' : 'ic_pause';
return React.createElement(this.props.toggleButtonComponent, { icon, onClick: this.togglePaused }, null);
}
}
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;

View file

@ -0,0 +1,3 @@
import PlayPauseButton from './PlayPauseButton';
export default PlayPauseButton;

View file

@ -37,7 +37,7 @@ class SeekBar extends Component {
onComplete = (time) => {
this.resetTimeDebounced();
this.setState({ time });
this.props.setTime(time);
this.props.dispatch('setProp', 'time', time);
}
onCancel = () => {
@ -112,7 +112,7 @@ SeekBar.propTypes = {
className: PropTypes.string,
time: PropTypes.number,
duration: PropTypes.number,
setTime: PropTypes.func.isRequired
dispatch: PropTypes.func.isRequired
};
export default SeekBar;

View file

@ -4,7 +4,7 @@
align-items: center;
.label {
font-size: var(--seek-bar-font-size);
font-size: 1em;
color: var(--color-surfacelight);
}

View file

@ -1,7 +1,8 @@
import React, { PureComponent, Fragment } from 'react';
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Icon from 'stremio-icons/dom';
import { Checkbox } from 'stremio-common';
import styles from './styles';
const ORIGIN_PRIORITIES = {
@ -9,43 +10,81 @@ const ORIGIN_PRIORITIES = {
'EMBEDDED': 2
};
class SubtitlesPicker extends PureComponent {
subtitlesComparator = (PRIORITIES) => {
const NumberInput = ({ value, unit, delta, onChange }) => {
if (value === null) {
return null;
}
const fractionalDigits = delta.toString().split('.')[1];
const digitsCount = typeof fractionalDigits === 'string' ? fractionalDigits.length : 0;
return (
<div className={styles['number-input-container']}>
<div className={styles['number-input-button']} data-value={value - delta} onClick={onChange}>
<Icon className={styles['number-input-icon']} icon={'ic_minus'} />
</div>
<div className={styles['number-input-value']}>{value.toFixed(digitsCount)}{unit}</div>
<div className={styles['number-input-button']} data-value={value + delta} onClick={onChange}>
<Icon className={styles['number-input-icon']} icon={'ic_plus'} />
</div>
</div>
);
};
class SubtitlesPicker extends Component {
shouldComponentUpdate(nextProps, nextState) {
return nextProps.className !== this.props.className ||
nextProps.subtitleTracks !== this.props.subtitleTracks ||
nextProps.selectedSubtitleTrackId !== this.props.selectedSubtitleTrackId ||
nextProps.subtitleSize !== this.props.subtitleSize ||
nextProps.subtitleDelay !== this.props.subtitleDelay ||
nextProps.subtitleDarkBackground !== this.props.subtitleDarkBackground;
}
subtitlesComparator = (priorities) => {
return (a, b) => {
const valueA = PRIORITIES[a];
const valueB = PRIORITIES[b];
const valueA = priorities[a];
const valueB = priorities[b];
if (!isNaN(valueA) && !isNaN(valueB)) return valueA - valueB;
if (!isNaN(valueA)) return -1;
if (!isNaN(valueB)) return 1;
return a - b;
}
};
}
toggleOnClick = () => {
this.props.setSelectedSubtitleTrackId(this.props.selectedSubtitleTrackId === null ? this.props.subtitleTracks[0].id : null);
toggleSubtitleEnabled = () => {
const selectedSubtitleTrackId = this.props.selectedSubtitleTrackId === null ? this.props.subtitleTracks[0].id : null
this.props.dispatch('setProp', 'selectedSubtitleTrackId', selectedSubtitleTrackId);
}
labelOnClick = (event) => {
const selectedTrack = this.props.subtitleTracks.find(({ label, origin }) => {
const subtitleTrack = this.props.subtitleTracks.find(({ label, origin }) => {
return label === event.currentTarget.dataset.label &&
origin === event.currentTarget.dataset.origin;
});
if (selectedTrack) {
this.props.setSelectedSubtitleTrackId(selectedTrack.id);
if (subtitleTrack) {
this.props.dispatch('setProp', 'selectedSubtitleTrackId', subtitleTrack.id);
}
}
trackOnClick = (event) => {
this.props.setSelectedSubtitleTrackId(event.currentTarget.dataset.trackId);
variantOnClick = (event) => {
this.props.dispatch('setProp', 'selectedSubtitleTrackId', event.currentTarget.dataset.trackId);
}
setSubtitleSize = (event) => {
this.props.setSubtitleSize(event.currentTarget.dataset.value);
this.props.dispatch('setProp', 'subtitleSize', event.currentTarget.dataset.value);
}
setSubtitleDelay = (event) => {
this.props.dispatch('setProp', 'subtitleDelay', event.currentTarget.dataset.value * 1000);
}
toggleSubtitleDarkBackground = () => {
this.props.dispatch('setProp', 'subtitleDarkBackground', !this.props.subtitleDarkBackground);
}
renderToggleButton({ selectedTrack }) {
return (
<div className={styles['toggle-button-container']} onClick={this.toggleOnClick}>
<div className={styles['toggle-button-container']} onClick={this.toggleSubtitleEnabled}>
<div className={styles['toggle-label']}>ON</div>
<div className={styles['toggle-label']}>OFF</div>
<div className={classnames(styles['toggle-thumb'], { [styles['on']]: selectedTrack })} />
@ -97,9 +136,9 @@ class SubtitlesPicker extends PureComponent {
<div key={track.id}
className={classnames(styles['variant-button'], { [styles['selected']]: track.id === selectedTrack.id })}
title={track.id}
onClick={this.trackOnClick}
onClick={this.variantOnClick}
data-track-id={track.id}
children={index}
children={index + 1}
/>
);
})}
@ -107,23 +146,20 @@ class SubtitlesPicker extends PureComponent {
);
}
renderNumberInput({ value, unit, delta, onChange }) {
if (value === null) {
renderDarkBackgroundToggle() {
if (this.props.subtitleDarkBackground === null) {
return null;
}
const fractionalDigits = delta.toString().split('.')[1];
const digitsCount = typeof fractionalDigits === 'string' ? fractionalDigits.length : 0;
return (
<div className={styles['number-input-container']}>
<div className={styles['number-input-button']} data-value={value - delta} onClick={onChange}>
<Icon className={styles['number-input-icon']} icon={'ic_minus'} />
</div>
<div className={styles['number-input-value']}>{value.toFixed(digitsCount)}{unit}</div>
<div className={styles['number-input-button']} data-value={value + delta} onClick={onChange}>
<Icon className={styles['number-input-icon']} icon={'ic_plus'} />
</div>
</div>
<label className={styles['background-toggle-container']}>
<Checkbox
className={styles['background-toggle-checkbox']}
checked={this.props.subtitleDarkBackground}
onClick={this.toggleSubtitleDarkBackground}
/>
<div className={styles['background-toggle-label']}>Dark background</div>
</label>
);
}
@ -140,8 +176,19 @@ class SubtitlesPicker extends PureComponent {
<div className={styles['preferences-container']}>
<div className={styles['preferences-title']}>Preferences</div>
{this.renderVariantsList({ groupedTracks, selectedTrack })}
{this.renderNumberInput({ value: this.props.subtitleSize, unit: 'pt', delta: 0.5, onChange: this.setSubtitleSize })}
{this.renderNumberInput({ value: this.props.subtitleSize, unit: 'pt', delta: 0.5, onChange: this.setSubtitleSize })}
{this.renderDarkBackgroundToggle()}
<NumberInput
value={this.props.subtitleSize}
unit={'pt'}
delta={0.5}
onChange={this.setSubtitleSize}
/>
<NumberInput
value={this.props.subtitleDelay / 1000}
unit={'s'}
delta={0.2}
onChange={this.setSubtitleDelay}
/>
</div>
);
}
@ -167,14 +214,17 @@ class SubtitlesPicker extends PureComponent {
SubtitlesPicker.propTypes = {
className: PropTypes.string,
selectedSubtitleTrackId: PropTypes.string,
languagePriorities: PropTypes.objectOf(PropTypes.number).isRequired,
subtitleTracks: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
origin: PropTypes.string.isRequired
})).isRequired,
setSelectedSubtitleTrackId: PropTypes.func.isRequired
selectedSubtitleTrackId: PropTypes.string,
subtitleSize: PropTypes.number,
subtitleDelay: PropTypes.number,
subtitleDarkBackground: PropTypes.bool,
dispatch: PropTypes.func.isRequired
};
SubtitlesPicker.defaultProps = {
languagePriorities: Object.freeze({

View file

@ -1,12 +1,11 @@
.subtitles-picker-container {
--scroll-bar-width: 12px;
width: calc(var(--subtitles-picker-button-size) * 14);
height: calc(var(--subtitles-picker-button-size) * 9);
font-size: calc(var(--subtitles-picker-button-size) * 0.45);
padding: calc(var(--subtitles-picker-button-size) * 0.3);
gap: calc(var(--subtitles-picker-button-size) * 0.3);
display: grid;
grid-template-columns: auto calc(var(--subtitles-picker-button-size) * 8 + var(--scroll-bar-width));
grid-template-columns: auto calc(var(--subtitles-picker-button-size) * 8 + var(--scroll-bar-width) * 1.5);
grid-template-rows: var(--subtitles-picker-button-size) auto;
grid-template-areas:
"toggle-button preferences"
@ -64,7 +63,8 @@
display: flex;
flex-direction: column;
align-items: stretch;
overflow-y: auto;
padding-right: calc(var(--scroll-bar-width) * 0.5);
overflow-y: scroll;
overflow-x: hidden;
.track-origin {
@ -151,10 +151,44 @@
background-color: var(--color-primarylight);
}
}
}
&::-webkit-scrollbar {
// TODO reuse app scrollbar styles
width: var(--scroll-bar-width);
.background-toggle-container {
align-self: stretch;
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
.background-toggle-checkbox {
--icon-color: var(--color-surfacelighter);
--background-color: transparent;
width: calc(var(--subtitles-picker-button-size) * 0.6);
height: calc(var(--subtitles-picker-button-size) * 0.6);
&:global(.checked) {
--background-color: var(--color-primarydark);
}
}
.background-toggle-label {
flex: 1;
padding: 0 0.5em;
font-size: 1em;
line-height: 1.1em;
max-height: 2.2em;
word-break: break-all;
overflow: hidden;
color: var(--color-surfacelighter);
}
&:hover {
.background-toggle-checkbox {
&:global(.checked) {
--background-color: var(--color-primarylight);
}
}
}
}
@ -171,7 +205,7 @@
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-primary);
background-color: var(--color-primarydark);
cursor: pointer;
.number-input-icon {

View file

@ -16,8 +16,9 @@ class VolumeBar extends Component {
shouldComponentUpdate(nextProps, nextState) {
return nextState.volume !== this.state.volume ||
nextProps.className !== this.props.className ||
nextProps.volume !== this.props.volume ||
nextProps.className !== this.props.className;
nextProps.toggleButtonComponent !== this.props.toggleButtonComponent;
}
componentWillUnmount() {
@ -25,7 +26,8 @@ class VolumeBar extends Component {
}
toogleVolumeMute = () => {
this.props.volume === 0 ? this.props.unmute() : this.props.mute();
const command = this.props.volume > 0 ? 'mute' : 'unmute';
this.props.dispatch('command', command);
}
resetVolumeDebounced = debounce(() => {
@ -40,7 +42,7 @@ class VolumeBar extends Component {
onComplete = (volume) => {
this.resetVolumeDebounced();
this.setState({ volume });
this.props.setVolume(volume);
this.props.dispatch('setProp', 'volume', volume);
}
onCancel = () => {
@ -79,10 +81,12 @@ class VolumeBar extends Component {
VolumeBar.propTypes = {
className: PropTypes.string,
volume: PropTypes.number,
toggleButtonComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
setVolume: PropTypes.func.isRequired,
mute: PropTypes.func.isRequired,
unmute: PropTypes.func.isRequired
toggleButtonComponent: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string,
PropTypes.shape({ render: PropTypes.func.isRequired }),
]).isRequired,
dispatch: PropTypes.func.isRequired
};
export default VolumeBar;

View file

@ -1,31 +1,26 @@
.control-bar-container, .popup-container {
--control-bar-button-height: 60px;
}
.control-bar-container {
top: initial !important;
padding: 0 calc(var(--control-bar-button-height) * 0.4);
padding: 0 calc(var(--control-bar-button-size) * 0.4);
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: stretch;
.seek-bar {
--seek-bar-thumb-size: calc(var(--control-bar-button-height) * 0.40);
--seek-bar-track-size: calc(var(--control-bar-button-height) * 0.12);
--seek-bar-font-size: calc(var(--control-bar-button-height) * 0.35);
height: calc(var(--control-bar-button-height) * 0.6);
--seek-bar-thumb-size: calc(var(--control-bar-button-size) * 0.40);
--seek-bar-track-size: calc(var(--control-bar-button-size) * 0.12);
height: calc(var(--control-bar-button-size) * 0.6);
font-size: calc(var(--control-bar-button-size) * 0.35);
}
.control-bar-buttons-container {
height: var(--control-bar-button-height);
height: var(--control-bar-button-size);
display: flex;
flex-direction: row;
align-items: center;
.control-bar-button {
width: var(--control-bar-button-height);
height: var(--control-bar-button-height);
width: var(--control-bar-button-size);
height: var(--control-bar-button-size);
display: flex;
justify-content: center;
align-items: center;
@ -46,7 +41,15 @@
}
}
&:hover {
&:global(.disabled) {
cursor: default;
.icon {
fill: var(--color-surfacedark);
}
}
&:hover:not(:global(.disabled)) {
.icon {
fill: var(--color-surfacelighter);
}
@ -54,10 +57,10 @@
}
.volume-bar {
--volume-bar-thumb-size: calc(var(--control-bar-button-height) * 0.36);
--volume-bar-track-size: calc(var(--control-bar-button-height) * 0.10);
height: var(--control-bar-button-height);
width: calc(var(--control-bar-button-height) * 5);
--volume-bar-thumb-size: calc(var(--control-bar-button-size) * 0.36);
--volume-bar-track-size: calc(var(--control-bar-button-size) * 0.10);
height: var(--control-bar-button-size);
width: calc(var(--control-bar-button-size) * 5);
&:hover, &:global(.active) {
.control-bar-button {
@ -68,7 +71,7 @@
}
}
.flex-spacing {
.spacing {
flex: 1
}
}
@ -79,24 +82,24 @@
bottom: 0;
left: 0;
z-index: -1;
box-shadow: 0 0 calc(var(--control-bar-button-height) * 2) calc(var(--control-bar-button-height) * 2.3) var(--color-backgrounddarker);
box-shadow: 0 0 calc(var(--control-bar-button-size) * 2) calc(var(--control-bar-button-size) * 2.3) var(--color-backgrounddarker);
content: "";
}
}
.popup-container {
--border-color: var(--color-primarylight);
:global(.player-popup-container) {
--border-color: var(--color-surfacelighter);
.popup-content {
background-color: var(--color-backgrounddark);
&.share-popup-content {
width: calc(var(--control-bar-button-height) * 5);
height: calc(var(--control-bar-button-height) * 3);
width: calc(var(--control-bar-button-size) * 5);
height: calc(var(--control-bar-button-size) * 3);
}
&.subtitles-popup-content {
--subtitles-picker-button-size: calc(var(--control-bar-button-height) * 0.6);
--subtitles-picker-button-size: calc(var(--control-bar-button-size) * 0.6);
}
}
}

View file

@ -1,6 +1,8 @@
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';
@ -14,10 +16,13 @@ class Player extends Component {
paused: null,
time: null,
duration: null,
buffering: null,
volume: null,
subtitleTracks: [],
selectedSubtitleTrackId: null,
subtitleSize: null
subtitleSize: null,
subtitleDelay: null,
subtitleDarkBackground: null
};
}
@ -25,19 +30,23 @@ 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 ||
nextState.subtitleSize !== this.state.subtitleSize;
nextState.subtitleSize !== this.state.subtitleSize ||
nextState.subtitleDelay !== this.state.subtitleDelay ||
nextState.subtitleDarkBackground !== this.state.subtitleDarkBackground;
}
componentDidMount() {
this.addSubtitleTracks([{
this.dispatch('command', 'addSubtitleTracks', [{
url: 'https://raw.githubusercontent.com/caitp/ng-media/master/example/assets/captions/bunny-en.vtt',
origin: 'Github',
label: 'English'
}]);
this.setSelectedSubtitleTrackId('https://raw.githubusercontent.com/caitp/ng-media/master/example/assets/captions/bunny-en.vtt');
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 = () => {
@ -56,44 +65,8 @@ class Player extends Component {
this.setState({ [propName]: propValue });
}
play = () => {
this.videoRef.current && this.videoRef.current.dispatch('setProp', 'paused', false);
}
pause = () => {
this.videoRef.current && this.videoRef.current.dispatch('setProp', 'paused', true);
}
setTime = (time) => {
this.videoRef.current && this.videoRef.current.dispatch('setProp', 'time', time);
}
setVolume = (volume) => {
this.videoRef.current && this.videoRef.current.dispatch('setProp', 'volume', volume);
}
setSelectedSubtitleTrackId = (selectedSubtitleTrackId) => {
this.videoRef.current && this.videoRef.current.dispatch('setProp', 'selectedSubtitleTrackId', selectedSubtitleTrackId);
}
setSubtitleSize = (size) => {
this.videoRef.current && this.videoRef.current.dispatch('setProp', 'subtitleSize', size);
}
mute = () => {
this.videoRef.current && this.videoRef.current.dispatch('command', 'mute');
}
unmute = () => {
this.videoRef.current && this.videoRef.current.dispatch('command', 'unmute');
}
addSubtitleTracks = (subtitleTracks) => {
this.videoRef.current && this.videoRef.current.dispatch('command', 'addSubtitleTracks', subtitleTracks);
}
stop = () => {
this.videoRef.current && this.videoRef.current.dispatch('command', 'stop');
dispatch = (...args) => {
this.videoRef.current && this.videoRef.current.dispatch(...args);
}
renderVideo() {
@ -102,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}
@ -113,10 +85,19 @@ class Player extends Component {
);
}
renderBufferingLoader() {
return (
<BufferingLoader
className={styles['layer']}
buffering={this.state.buffering}
/>
);
}
renderControlBar() {
return (
<ControlBar
className={styles['layer']}
className={classnames(styles['layer'], styles['control-bar-layer'])}
paused={this.state.paused}
time={this.state.time}
duration={this.state.duration}
@ -124,14 +105,9 @@ class Player extends Component {
subtitleTracks={this.state.subtitleTracks}
selectedSubtitleTrackId={this.state.selectedSubtitleTrackId}
subtitleSize={this.state.subtitleSize}
play={this.play}
pause={this.pause}
setTime={this.setTime}
setVolume={this.setVolume}
setSelectedSubtitleTrackId={this.setSelectedSubtitleTrackId}
setSubtitleSize={this.setSubtitleSize}
mute={this.mute}
unmute={this.unmute}
subtitleDelay={this.state.subtitleDelay}
subtitleDarkBackground={this.state.subtitleDarkBackground}
dispatch={this.dispatch}
/>
);
}
@ -140,6 +116,7 @@ class Player extends Component {
return (
<div className={styles['player-container']}>
{this.renderVideo()}
{this.renderBufferingLoader()}
{this.renderControlBar()}
</div>
);
@ -150,10 +127,10 @@ Player.propTypes = {
stream: PropTypes.object.isRequired
};
Player.defaultProps = {
stream: {
stream: Object.freeze({
// ytId: 'E4A0bcCQke0',
url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'
}
})
};
export default Player;

View file

@ -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;

View file

@ -1,8 +1,8 @@
var EventEmitter = require('events');
var subtitleUtils = require('./utils/subtitles');
var HTMLVideo = function(container) {
if (!(container instanceof HTMLElement)) {
var HTMLVideo = function(containerElement) {
if (!(containerElement instanceof HTMLElement)) {
throw new Error('Instance of HTMLElement required as a first argument');
}
@ -14,42 +14,56 @@ var HTMLVideo = function(container) {
var subtitleCues = {};
var subtitleTracks = [];
var selectedSubtitleTrackId = null;
var styles = document.createElement('style');
var video = document.createElement('video');
var subtitles = document.createElement('div');
var subtitleDelay = 0;
var stylesElement = document.createElement('style');
var videoElement = document.createElement('video');
var subtitlesElement = document.createElement('div');
container.appendChild(styles);
styles.sheet.insertRule('#' + container.id + ' video { width: 100%; height: 100%; position: relative; z-index: 0; }', styles.sheet.cssRules.length);
var subtitleStylesIndex = styles.sheet.insertRule('#' + container.id + ' .subtitles { position: absolute; right: 0; bottom: 120px; left: 0; font-size: 16pt; color: white; text-align: center; }', styles.sheet.cssRules.length);
container.appendChild(video);
video.crossOrigin = 'anonymous';
video.controls = false;
container.appendChild(subtitles);
subtitles.classList.add('subtitles');
containerElement.appendChild(stylesElement);
stylesElement.sheet.insertRule('#' + containerElement.id + ' video { width: 100%; height: 100%; position: relative; z-index: 0; }', stylesElement.sheet.cssRules.length);
var subtitleStylesIndex = stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles { position: absolute; right: 0; bottom: 0; left: 0; font-size: 26pt; color: white; text-align: center; }', stylesElement.sheet.cssRules.length);
stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles .cue { display: inline-block; padding: 0.2em; text-shadow: #222222 0px 0px 1.8px, #222222 0px 0px 1.8px, #222222 0px 0px 1.8px, #222222 0px 0px 1.8px, #222222 0px 0px 1.8px; }', stylesElement.sheet.cssRules.length);
stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles.dark-background .cue { text-shadow: none; background-color: #222222; }', stylesElement.sheet.cssRules.length);
containerElement.appendChild(videoElement);
videoElement.crossOrigin = 'anonymous';
videoElement.controls = false;
containerElement.appendChild(subtitlesElement);
subtitlesElement.classList.add('subtitles');
function getPaused() {
if (!loaded) {
return null;
}
return !!video.paused;
return !!videoElement.paused;
}
function getTime() {
if (!loaded) {
return null;
}
return Math.floor(video.currentTime * 1000);
return Math.floor(videoElement.currentTime * 1000);
}
function getDuration() {
if (!loaded || isNaN(video.duration)) {
if (!loaded || isNaN(videoElement.duration)) {
return null;
}
return Math.floor(video.duration * 1000);
return Math.floor(videoElement.duration * 1000);
}
function getBuffering() {
if (!loaded) {
return null;
}
return videoElement.readyState < videoElement.HAVE_FUTURE_DATA;
}
function getVolume() {
return video.muted ? 0 : Math.floor(video.volume * 100);
if (destroyed) {
return null;
}
return videoElement.muted ? 0 : Math.floor(videoElement.volume * 100);
}
function getSubtitleTracks() {
if (!loaded) {
@ -65,8 +79,26 @@ var HTMLVideo = function(container) {
return selectedSubtitleTrackId;
}
function getSubtitleDelay() {
if (!loaded) {
return null;
}
return subtitleDelay;
}
function getSubtitleSize() {
return parseFloat(styles.sheet.cssRules[subtitleStylesIndex].style.fontSize);
if (destroyed) {
return null;
}
return parseFloat(stylesElement.sheet.cssRules[subtitleStylesIndex].style.fontSize);
}
function getSubtitleDarkBackground() {
if (destroyed) {
return null;
}
return subtitlesElement.classList.contains('dark-background');
}
function onEnded() {
events.emit('ended');
@ -74,7 +106,7 @@ var HTMLVideo = function(container) {
function onError() {
var message;
var critical;
switch (video.error.code) {
switch (videoElement.error.code) {
case 1:
message = 'Fetching process aborted';
critical = false;
@ -97,7 +129,7 @@ var HTMLVideo = function(container) {
}
events.emit('error', {
code: video.error.code,
code: videoElement.error.code,
message: message,
critical: critical
});
@ -115,6 +147,9 @@ var HTMLVideo = function(container) {
function onDurationChanged() {
events.emit('propChanged', 'duration', getDuration());
}
function onBufferingChanged() {
events.emit('propChanged', 'buffering', getBuffering());
}
function onVolumeChanged() {
events.emit('propChanged', 'volume', getVolume());
}
@ -124,23 +159,30 @@ var HTMLVideo = function(container) {
function onSelectedSubtitleTrackIdChanged() {
events.emit('propChanged', 'selectedSubtitleTrackId', getSelectedSubtitleTrackId());
}
function onSubtitleDelayChanged() {
events.emit('propChanged', 'subtitleDelay', getSubtitleDelay());
}
function onSubtitleSizeChanged() {
events.emit('propChanged', 'subtitleSize', getSubtitleSize());
}
function onSubtitleDarkBackgroundChanged() {
events.emit('propChanged', 'subtitleDarkBackground', getSubtitleDarkBackground());
}
function updateSubtitleText() {
while (subtitles.hasChildNodes()) {
subtitles.removeChild(subtitles.lastChild);
while (subtitlesElement.hasChildNodes()) {
subtitlesElement.removeChild(subtitlesElement.lastChild);
}
if (!loaded || !Array.isArray(subtitleCues.times)) {
return;
}
var time = getTime();
var time = getTime() + getSubtitleDelay();
var cuesForTime = subtitleUtils.cuesForTime(subtitleCues, time);
for (var i = 0; i < cuesForTime.length; i++) {
const cue = subtitleUtils.render(cuesForTime[i]);
subtitles.appendChild(cue);
var cueNode = subtitleUtils.render(cuesForTime[i]);
cueNode.classList.add('cue');
subtitlesElement.append(cueNode, document.createElement('br'));
}
}
function flushArgsQueue() {
@ -169,25 +211,34 @@ var HTMLVideo = function(container) {
switch (arguments[1]) {
case 'paused':
events.emit('propValue', 'paused', getPaused());
video.removeEventListener('pause', onPausedChanged);
video.removeEventListener('play', onPausedChanged);
video.addEventListener('pause', onPausedChanged);
video.addEventListener('play', onPausedChanged);
videoElement.removeEventListener('pause', onPausedChanged);
videoElement.removeEventListener('play', onPausedChanged);
videoElement.addEventListener('pause', onPausedChanged);
videoElement.addEventListener('play', onPausedChanged);
return;
case 'time':
events.emit('propValue', 'time', getTime());
video.removeEventListener('timeupdate', onTimeChanged);
video.addEventListener('timeupdate', onTimeChanged);
videoElement.removeEventListener('timeupdate', onTimeChanged);
videoElement.addEventListener('timeupdate', onTimeChanged);
return;
case 'duration':
events.emit('propValue', 'duration', getDuration());
video.removeEventListener('durationchange', onDurationChanged);
video.addEventListener('durationchange', onDurationChanged);
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());
video.removeEventListener('volumechange', onVolumeChanged);
video.addEventListener('volumechange', onVolumeChanged);
videoElement.removeEventListener('volumechange', onVolumeChanged);
videoElement.addEventListener('volumechange', onVolumeChanged);
return;
case 'subtitleTracks':
events.emit('propValue', 'subtitleTracks', getSubtitleTracks());
@ -198,6 +249,12 @@ var HTMLVideo = function(container) {
case 'subtitleSize':
events.emit('propValue', 'subtitleSize', getSubtitleSize());
return;
case 'subtitleDelay':
events.emit('propValue', 'subtitleDelay', getSubtitleDelay());
return;
case 'subtitleDarkBackground':
events.emit('propValue', 'subtitleDarkBackground', getSubtitleDarkBackground());
return;
default:
throw new Error('observeProp not supported: ' + arguments[1]);
}
@ -205,19 +262,20 @@ var HTMLVideo = function(container) {
switch (arguments[1]) {
case 'paused':
if (loaded) {
arguments[2] ? video.pause() : video.play();
arguments[2] ? videoElement.pause() : videoElement.play();
}
break;
case 'time':
if (loaded) {
if (!isNaN(arguments[2])) {
video.currentTime = arguments[2] / 1000;
videoElement.currentTime = arguments[2] / 1000;
}
}
break;
case 'selectedSubtitleTrackId':
if (loaded) {
selectedSubtitleTrackId = null;
subtitleDelay = 0;
subtitleCues = {};
for (var i = 0; i < subtitleTracks.length; i++) {
var subtitleTrack = subtitleTracks[i];
@ -227,15 +285,23 @@ var HTMLVideo = function(container) {
.then(function(resp) {
return resp.text();
})
.catch(function() {
events.emit('error', {
code: 70,
message: 'Failed to fetch subtitles from ' + subtitleTrack.origin,
critical: false
});
})
.then(function(text) {
if (selectedSubtitleTrackId === subtitleTrack.id) {
subtitleCues = subtitleUtils.parse(text);
updateSubtitleText();
}
})
.catch(function() {
events.emit('error', {
code: 68,
message: 'Failed to fetch subtitles from ' + subtitleTrack.origin,
code: 71,
message: 'Failed to parse subtitles from ' + subtitleTrack.origin,
critical: false
});
});
@ -243,20 +309,39 @@ var HTMLVideo = function(container) {
}
}
updateSubtitleText();
onSubtitleDelayChanged();
onSelectedSubtitleTrackIdChanged();
updateSubtitleText();
}
break;
case 'subtitleDelay':
if (loaded) {
if (!isNaN(arguments[2])) {
subtitleDelay = parseFloat(arguments[2]);
onSubtitleDelayChanged();
updateSubtitleText();
}
}
break;
case 'subtitleSize':
if (!isNaN(arguments[2])) {
styles.sheet.cssRules[subtitleStylesIndex].style.fontSize = parseFloat(arguments[2]) + 'pt';
stylesElement.sheet.cssRules[subtitleStylesIndex].style.fontSize = parseFloat(arguments[2]) + 'pt';
onSubtitleSizeChanged();
}
return;
case 'subtitleDarkBackground':
if (arguments[2]) {
subtitlesElement.classList.add('dark-background');
} else {
subtitlesElement.classList.remove('dark-background');
}
onSubtitleDarkBackgroundChanged();
return;
case 'volume':
if (!isNaN(arguments[2])) {
video.muted = false;
video.volume = arguments[2] / 100;
videoElement.muted = false;
videoElement.volume = arguments[2] / 100;
}
return;
default:
@ -295,62 +380,71 @@ var HTMLVideo = function(container) {
}
break;
case 'mute':
video.muted = true;
videoElement.muted = true;
return;
case 'unmute':
video.volume = video.volume !== 0 ? video.volume : 0.5;
video.muted = false;
videoElement.volume = videoElement.volume !== 0 ? videoElement.volume : 0.5;
videoElement.muted = false;
return;
case 'stop':
video.removeEventListener('ended', onEnded);
video.removeEventListener('error', onError);
video.removeEventListener('timeupdate', updateSubtitleText);
videoElement.removeEventListener('ended', onEnded);
videoElement.removeEventListener('error', onError);
videoElement.removeEventListener('timeupdate', updateSubtitleText);
loaded = false;
dispatchArgsQueue = [];
subtitleCues = {};
subtitleTracks = [];
selectedSubtitleTrackId = null;
video.removeAttribute('src');
video.load();
video.currentTime = 0;
subtitleDelay = 0;
videoElement.removeAttribute('src');
videoElement.load();
videoElement.currentTime = 0;
onPausedChanged();
onTimeChanged();
onDurationChanged();
onBufferingChanged();
onSubtitleTracksChanged();
onSelectedSubtitleTrackIdChanged();
onSubtitleDelayChanged();
updateSubtitleText();
return;
case 'load':
var dispatchArgsQueueCopy = dispatchArgsQueue.slice();
self.dispatch('command', 'stop');
dispatchArgsQueue = dispatchArgsQueueCopy;
video.addEventListener('ended', onEnded);
video.addEventListener('error', onError);
video.addEventListener('timeupdate', updateSubtitleText);
video.autoplay = typeof arguments[3].autoplay === 'boolean' ? arguments[3].autoplay : true;
video.currentTime = !isNaN(arguments[3].time) ? arguments[3].time / 1000 : 0;
video.src = arguments[2].url;
videoElement.addEventListener('ended', onEnded);
videoElement.addEventListener('error', onError);
videoElement.addEventListener('timeupdate', updateSubtitleText);
videoElement.autoplay = typeof arguments[3].autoplay === 'boolean' ? arguments[3].autoplay : true;
videoElement.currentTime = !isNaN(arguments[3].time) ? arguments[3].time / 1000 : 0;
videoElement.src = arguments[2].url;
loaded = true;
onPausedChanged();
onTimeChanged();
onDurationChanged();
onSubtitleTracksChanged();
onSelectedSubtitleTrackIdChanged();
onBufferingChanged();
onSubtitleDelayChanged();
updateSubtitleText();
flushArgsQueue();
return;
case 'destroy':
self.dispatch('command', 'stop');
events.removeAllListeners();
video.removeEventListener('pause', onPausedChanged);
video.removeEventListener('play', onPausedChanged);
video.removeEventListener('timeupdate', onTimeChanged);
video.removeEventListener('durationchange', onDurationChanged);
video.removeEventListener('volumechange', onVolumeChanged);
container.removeChild(video);
container.removeChild(styles);
container.removeChild(subtitles);
destroyed = true;
onVolumeChanged();
onSubtitleSizeChanged();
onSubtitleDarkBackgroundChanged();
events.removeAllListeners();
videoElement.removeEventListener('pause', onPausedChanged);
videoElement.removeEventListener('play', onPausedChanged);
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);
return;
default:
throw new Error('command not supported: ' + arguments[1]);
@ -369,7 +463,7 @@ var HTMLVideo = function(container) {
HTMLVideo.manifest = {
name: 'HTMLVideo',
embedded: true,
props: ['paused', 'time', 'duration', 'volume', 'subtitleTracks', 'selectedSubtitleTrackId', 'subtitleSize']
props: ['paused', 'time', 'duration', 'volume', 'buffering', 'subtitleTracks', 'selectedSubtitleTrackId', 'subtitleSize', 'subtitleDelay', 'subtitleDarkBackground']
};
module.exports = HTMLVideo;

View file

@ -31,8 +31,8 @@ function parse(text) {
var parser = new VTTJS.WebVTT.Parser(window, VTTJS.WebVTT.StringDecoder());
parser.oncue = function(c) {
var cue = {
startTime: c.startTime * 1000,
endTime: c.endTime * 1000,
startTime: (c.startTime * 1000) | 0,
endTime: (c.endTime * 1000) | 0,
text: c.text
};
cues.push(cue);

View file

@ -1,3 +1,7 @@
.player-container, :global(.player-popup-container) {
--control-bar-button-size: 60px;
}
.player-container {
position: relative;
z-index: 0;
@ -12,5 +16,13 @@
right: 0;
bottom: 0;
z-index: 0;
&.control-bar-layer {
top: initial;
}
}
:global(.subtitles) {
bottom: calc(var(--control-bar-button-size) * 3) !important;
}
}