mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-20 14:52:13 +00:00
Merge branch 'master' of github.com:Stremio/stremio-web into detail-page
This commit is contained in:
commit
71e1afc0b4
23 changed files with 651 additions and 389 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
25
src/routes/Player/BufferingLoader/BufferingLoader.js
Normal file
25
src/routes/Player/BufferingLoader/BufferingLoader.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import styles from './styles';
|
||||
|
||||
class BufferingLoader extends PureComponent {
|
||||
render() {
|
||||
if (!this.props.buffering) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classnames(this.props.className, styles['buffering-loader-container'])}>
|
||||
<div className={styles['bufferring-loader']} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BufferingLoader.propTypes = {
|
||||
className: PropTypes.string,
|
||||
buffering: PropTypes.bool
|
||||
};
|
||||
|
||||
export default BufferingLoader;
|
||||
3
src/routes/Player/BufferingLoader/index.js
Normal file
3
src/routes/Player/BufferingLoader/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import BufferingLoader from './BufferingLoader';
|
||||
|
||||
export default BufferingLoader;
|
||||
24
src/routes/Player/BufferingLoader/styles.less
Normal file
24
src/routes/Player/BufferingLoader/styles.less
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.buffering-loader-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.bufferring-loader {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: 50%;
|
||||
border: 10px solid #f3f3f3;
|
||||
border-top: 10px solid #3498db;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
3
src/routes/Player/ControlBar/PlayPauseButton/index.js
Normal file
3
src/routes/Player/ControlBar/PlayPauseButton/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import PlayPauseButton from './PlayPauseButton';
|
||||
|
||||
export default PlayPauseButton;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
align-items: center;
|
||||
|
||||
.label {
|
||||
font-size: var(--seek-bar-font-size);
|
||||
font-size: 1em;
|
||||
color: var(--color-surfacelight);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue