mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-21 03:22:11 +00:00
Merge branch 'master' of github.com:Stremio/stremio-web into addons-screen
This commit is contained in:
commit
cd44e3d71d
33 changed files with 2917 additions and 1224 deletions
36
package.json
36
package.json
|
|
@ -17,8 +17,8 @@
|
||||||
"hat": "0.0.3",
|
"hat": "0.0.3",
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"prop-types": "15.6.2",
|
"prop-types": "15.6.2",
|
||||||
"react": "16.6.0",
|
"react": "16.6.3",
|
||||||
"react-dom": "16.6.0",
|
"react-dom": "16.6.3",
|
||||||
"react-router": "4.3.1",
|
"react-router": "4.3.1",
|
||||||
"react-router-dom": "4.3.1",
|
"react-router-dom": "4.3.1",
|
||||||
"stremio-addon-client": "git+ssh://git@github.com/Stremio/stremio-addon-client.git#v1.5.1",
|
"stremio-addon-client": "git+ssh://git@github.com/Stremio/stremio-addon-client.git#v1.5.1",
|
||||||
|
|
@ -30,32 +30,32 @@
|
||||||
"stremio-json-data": "git+ssh://git@github.com/stremio/stremio-json-data.git#v1.2.3",
|
"stremio-json-data": "git+ssh://git@github.com/stremio/stremio-json-data.git#v1.2.3",
|
||||||
"stremio-models": "git+ssh://git@github.com/stremio/stremio-models.git#v1.51.5",
|
"stremio-models": "git+ssh://git@github.com/stremio/stremio-models.git#v1.51.5",
|
||||||
"stremio-official-addons": "git+ssh://git@github.com/Stremio/stremio-official-addons.git#v1.1.1",
|
"stremio-official-addons": "git+ssh://git@github.com/Stremio/stremio-official-addons.git#v1.1.1",
|
||||||
"stremio-translations": "git+ssh://git@github.com/Stremio/stremio-translations.git#v1.41.0"
|
"stremio-translations": "git+ssh://git@github.com/Stremio/stremio-translations.git#v1.41.0",
|
||||||
|
"vtt.js": "0.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.1.2",
|
"@babel/core": "7.2.2",
|
||||||
"@babel/plugin-proposal-class-properties": "7.1.0",
|
"@babel/plugin-proposal-class-properties": "7.2.1",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "7.0.0",
|
"@babel/plugin-proposal-object-rest-spread": "7.2.0",
|
||||||
"@babel/preset-env": "7.1.0",
|
"@babel/preset-env": "7.2.0",
|
||||||
"@babel/preset-react": "7.0.0",
|
"@babel/preset-react": "7.0.0",
|
||||||
"@babel/runtime": "7.1.2",
|
"@babel/runtime": "7.2.0",
|
||||||
"@storybook/addon-actions": "4.0.4",
|
"@storybook/addon-actions": "4.1.2",
|
||||||
"@storybook/addon-links": "4.0.4",
|
"@storybook/addon-links": "4.1.2",
|
||||||
"@storybook/addons": "4.0.4",
|
"@storybook/addons": "4.1.2",
|
||||||
"@storybook/react": "4.0.4",
|
"@storybook/react": "4.1.2",
|
||||||
"autoprefixer": "9.4.0",
|
"autoprefixer": "9.4.3",
|
||||||
"babel-loader": "8.0.4",
|
"babel-loader": "8.0.4",
|
||||||
"copy-webpack-plugin": "4.6.0",
|
"copy-webpack-plugin": "4.6.0",
|
||||||
"css-loader": "1.0.1",
|
"css-loader": "2.0.1",
|
||||||
"html-webpack-plugin": "3.2.0",
|
"html-webpack-plugin": "3.2.0",
|
||||||
"less": "3.8.1",
|
"less": "3.9.0",
|
||||||
"less-loader": "4.1.0",
|
"less-loader": "4.1.0",
|
||||||
"postcss-loader": "3.0.0",
|
"postcss-loader": "3.0.0",
|
||||||
"style-loader": "0.23.1",
|
"style-loader": "0.23.1",
|
||||||
"terser-webpack-plugin": "1.1.0",
|
"terser-webpack-plugin": "1.1.0",
|
||||||
"uglifyjs-webpack-plugin": "2.0.1",
|
"webpack": "4.27.1",
|
||||||
"webpack": "4.25.1",
|
|
||||||
"webpack-cli": "3.1.2",
|
"webpack-cli": "3.1.2",
|
||||||
"webpack-dev-server": "3.1.10"
|
"webpack-dev-server": "3.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent, StrictMode } from 'react';
|
||||||
import { Router } from 'stremio-common';
|
import { Router } from 'stremio-common';
|
||||||
import routerConfig from './routerConfig';
|
import routerConfig from './routerConfig';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
@ -6,10 +6,12 @@ import styles from './styles';
|
||||||
class App extends PureComponent {
|
class App extends PureComponent {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Router
|
<StrictMode>
|
||||||
routeContainerClassName={styles['route-container']}
|
<Router
|
||||||
config={routerConfig}
|
routeContainerClassName={styles['route-container']}
|
||||||
/>
|
config={routerConfig}
|
||||||
|
/>
|
||||||
|
</StrictMode>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--scroll-bar-width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
:global {
|
:global {
|
||||||
* {
|
* {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|
@ -61,6 +65,19 @@
|
||||||
min-width: 1000px;
|
min-width: 1000px;
|
||||||
min-height: 650px;
|
min-height: 650px;
|
||||||
font-family: 'Roboto', 'sans-serif';
|
font-family: 'Roboto', 'sans-serif';
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: var(--scroll-bar-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--color-secondarylighter80);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: var(--color-backgroundlight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
|
|
@ -84,5 +101,6 @@
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
}
|
}
|
||||||
|
|
@ -5,22 +5,33 @@ import Icon from 'stremio-icons/dom';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
class Checkbox extends Component {
|
class Checkbox extends Component {
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps) {
|
||||||
return nextProps.checked !== this.props.checked ||
|
return nextProps.checked !== this.props.checked ||
|
||||||
nextProps.enabled !== this.props.enabled;
|
nextProps.disabled !== this.props.disabled ||
|
||||||
|
nextProps.className !== this.props.className;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick = () => {
|
onClick = (event) => {
|
||||||
if (this.props.enabled && typeof this.props.onClick === 'function') {
|
event.preventDefault();
|
||||||
|
if (typeof this.props.onClick === 'function') {
|
||||||
this.props.onClick();
|
this.props.onClick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={classnames(styles['root'], this.props.className, { [styles['checkbox-checked']]: this.props.checked }, { [styles['checkbox-disabled']]: !this.props.enabled })}>
|
<div className={classnames(this.props.className, styles['checkbox-container'], { 'checked': this.props.checked }, { 'disabled': this.props.disabled })}>
|
||||||
<Icon className={classnames(styles['icon'])} icon={this.props.checked ? 'ic_check' : 'ic_box_empty'} />
|
<Icon
|
||||||
<input type={'checkbox'} className={styles['native-checkbox']} defaultChecked={this.props.checked} disabled={!this.props.enabled} onClick={this.onClick} />
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -28,13 +39,13 @@ class Checkbox extends Component {
|
||||||
|
|
||||||
Checkbox.propTypes = {
|
Checkbox.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
enabled: PropTypes.bool.isRequired,
|
disabled: PropTypes.bool.isRequired,
|
||||||
checked: PropTypes.bool.isRequired,
|
checked: PropTypes.bool.isRequired,
|
||||||
onClick: PropTypes.func
|
onClick: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
Checkbox.defaultProps = {
|
Checkbox.defaultProps = {
|
||||||
enabled: true,
|
disabled: false,
|
||||||
checked: false
|
checked: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,14 @@
|
||||||
.root {
|
.checkbox-container {
|
||||||
cursor: pointer;
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
|
||||||
&.checkbox-checked {
|
.icon {
|
||||||
background-color: var(--color-primarylight);
|
height: 100%;
|
||||||
|
fill: var(--icon-color);
|
||||||
.icon {
|
|
||||||
padding: 10%;
|
|
||||||
fill: var(--color-surfacelighter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.checkbox-disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.native-checkbox {
|
.native-checkbox {
|
||||||
|
|
@ -20,14 +16,13 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
top: -9999px;
|
top: -99999999px;
|
||||||
left: -9999px;
|
left: -99999999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
&:global(.checked) {
|
||||||
width: 100%;
|
.icon {
|
||||||
height: 100%;
|
height: 55%;
|
||||||
margin: auto;
|
}
|
||||||
fill: var(--color-surfacelighter60);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ class Popup extends Component {
|
||||||
this.menuBorderRightRef = React.createRef();
|
this.menuBorderRightRef = React.createRef();
|
||||||
this.menuBorderBottomRef = React.createRef();
|
this.menuBorderBottomRef = React.createRef();
|
||||||
this.menuBorderLeftRef = React.createRef();
|
this.menuBorderLeftRef = React.createRef();
|
||||||
|
this.hiddenBorderRef = React.createRef();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
open: false
|
open: false
|
||||||
|
|
@ -60,7 +61,7 @@ class Popup extends Component {
|
||||||
const bodyRect = document.body.getBoundingClientRect();
|
const bodyRect = document.body.getBoundingClientRect();
|
||||||
const menuRect = this.menuRef.current.getBoundingClientRect();
|
const menuRect = this.menuRef.current.getBoundingClientRect();
|
||||||
const labelRect = this.labelRef.current.getBoundingClientRect();
|
const labelRect = this.labelRef.current.getBoundingClientRect();
|
||||||
const borderWidth = 1 / window.devicePixelRatio;
|
const borderSize = parseFloat(window.getComputedStyle(this.hiddenBorderRef.current).getPropertyValue('border-top-width'));
|
||||||
const labelPosition = {
|
const labelPosition = {
|
||||||
left: labelRect.x - bodyRect.x,
|
left: labelRect.x - bodyRect.x,
|
||||||
top: labelRect.y - bodyRect.y,
|
top: labelRect.y - bodyRect.y,
|
||||||
|
|
@ -105,23 +106,23 @@ class Popup extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.border) {
|
if (this.props.border) {
|
||||||
this.menuBorderTopRef.current.style.height = `${borderWidth}px`;
|
this.menuBorderTopRef.current.style.height = `${borderSize}px`;
|
||||||
this.menuBorderRightRef.current.style.width = `${borderWidth}px`;
|
this.menuBorderRightRef.current.style.width = `${borderSize}px`;
|
||||||
this.menuBorderBottomRef.current.style.height = `${borderWidth}px`;
|
this.menuBorderBottomRef.current.style.height = `${borderSize}px`;
|
||||||
this.menuBorderLeftRef.current.style.width = `${borderWidth}px`;
|
this.menuBorderLeftRef.current.style.width = `${borderSize}px`;
|
||||||
this.labelBorderTopRef.current.style.height = `${borderWidth}px`;
|
this.labelBorderTopRef.current.style.height = `${borderSize}px`;
|
||||||
this.labelBorderTopRef.current.style.top = `${labelPosition.top}px`;
|
this.labelBorderTopRef.current.style.top = `${labelPosition.top}px`;
|
||||||
this.labelBorderTopRef.current.style.right = `${labelPosition.right}px`;
|
this.labelBorderTopRef.current.style.right = `${labelPosition.right}px`;
|
||||||
this.labelBorderTopRef.current.style.left = `${labelPosition.left}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.top = `${labelPosition.top}px`;
|
||||||
this.labelBorderRightRef.current.style.right = `${labelPosition.right}px`;
|
this.labelBorderRightRef.current.style.right = `${labelPosition.right}px`;
|
||||||
this.labelBorderRightRef.current.style.bottom = `${labelPosition.bottom}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.right = `${labelPosition.right}px`;
|
||||||
this.labelBorderBottomRef.current.style.bottom = `${labelPosition.bottom}px`;
|
this.labelBorderBottomRef.current.style.bottom = `${labelPosition.bottom}px`;
|
||||||
this.labelBorderBottomRef.current.style.left = `${labelPosition.left}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.top = `${labelPosition.top}px`;
|
||||||
this.labelBorderLeftRef.current.style.bottom = `${labelPosition.bottom}px`;
|
this.labelBorderLeftRef.current.style.bottom = `${labelPosition.bottom}px`;
|
||||||
this.labelBorderLeftRef.current.style.left = `${labelPosition.left}px`;
|
this.labelBorderLeftRef.current.style.left = `${labelPosition.left}px`;
|
||||||
|
|
@ -129,16 +130,16 @@ class Popup extends Component {
|
||||||
if (menuDirections.top) {
|
if (menuDirections.top) {
|
||||||
this.labelBorderTopRef.current.style.display = 'none';
|
this.labelBorderTopRef.current.style.display = 'none';
|
||||||
if (menuDirections.left) {
|
if (menuDirections.left) {
|
||||||
this.menuBorderBottomRef.current.style.right = `${labelRect.width - borderWidth}px`;
|
this.menuBorderBottomRef.current.style.right = `${labelRect.width - borderSize}px`;
|
||||||
} else {
|
} else {
|
||||||
this.menuBorderBottomRef.current.style.left = `${labelRect.width - borderWidth}px`;
|
this.menuBorderBottomRef.current.style.left = `${labelRect.width - borderSize}px`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.labelBorderBottomRef.current.style.display = 'none';
|
this.labelBorderBottomRef.current.style.display = 'none';
|
||||||
if (menuDirections.left) {
|
if (menuDirections.left) {
|
||||||
this.menuBorderTopRef.current.style.right = `${labelRect.width - borderWidth}px`;
|
this.menuBorderTopRef.current.style.right = `${labelRect.width - borderSize}px`;
|
||||||
} else {
|
} else {
|
||||||
this.menuBorderTopRef.current.style.left = `${labelRect.width - borderWidth}px`;
|
this.menuBorderTopRef.current.style.left = `${labelRect.width - borderSize}px`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -189,6 +190,7 @@ class Popup extends Component {
|
||||||
<div ref={this.labelBorderRightRef} className={classnames(styles['border'], styles['border-right'])} />
|
<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.labelBorderBottomRef} className={classnames(styles['border'], styles['border-bottom'])} />
|
||||||
<div ref={this.labelBorderLeftRef} className={classnames(styles['border'], styles['border-left'])} />
|
<div ref={this.labelBorderLeftRef} className={classnames(styles['border'], styles['border-left'])} />
|
||||||
|
<div ref={this.hiddenBorderRef} className={classnames(styles['border'], styles['border-hidden'])} />
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,4 +35,9 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-hidden {
|
||||||
|
display: none;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,38 +8,25 @@ class Slider extends Component {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.orientation = props.orientation;
|
this.orientation = props.orientation;
|
||||||
this.state = {
|
|
||||||
active: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
return nextState.active !== this.state.active ||
|
return nextProps.value !== this.props.value ||
|
||||||
nextProps.value !== this.props.value ||
|
|
||||||
nextProps.minimumValue !== this.props.minimumValue ||
|
nextProps.minimumValue !== this.props.minimumValue ||
|
||||||
nextProps.maximumValue !== this.props.maximumValue ||
|
nextProps.maximumValue !== this.props.maximumValue ||
|
||||||
nextProps.className !== this.props.className;
|
nextProps.className !== this.props.className;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSlide = (...args) => {
|
onSlide = (value) => {
|
||||||
this.setState({ active: true });
|
this.props.onSlide(value);
|
||||||
if (typeof this.props.onSlide === 'function') {
|
|
||||||
this.props.onSlide(...args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onComplete = (...args) => {
|
onComplete = (value) => {
|
||||||
this.setState({ active: false });
|
this.props.onComplete(value);
|
||||||
if (typeof this.props.onComplete === 'function') {
|
|
||||||
this.props.onComplete(...args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCancel = (...args) => {
|
onCancel = () => {
|
||||||
this.setState({ active: false });
|
this.props.onCancel();
|
||||||
if (typeof this.props.onCancel === 'function') {
|
|
||||||
this.props.onCancel(...args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateSlidingValue = ({ mouseX, mouseY, sliderElement }) => {
|
calculateSlidingValue = ({ mouseX, mouseY, sliderElement }) => {
|
||||||
|
|
@ -50,7 +37,7 @@ class Slider extends Component {
|
||||||
const thumbStart = Math.min(Math.max(mouseStart - sliderStart, 0), sliderLength);
|
const thumbStart = Math.min(Math.max(mouseStart - sliderStart, 0), sliderLength);
|
||||||
const slidingValueCoef = this.orientation === 'horizontal' ? thumbStart / sliderLength : (sliderLength - thumbStart) / sliderLength;
|
const slidingValueCoef = this.orientation === 'horizontal' ? thumbStart / sliderLength : (sliderLength - thumbStart) / sliderLength;
|
||||||
const slidingValue = slidingValueCoef * (this.props.maximumValue - this.props.minimumValue) + this.props.minimumValue;
|
const slidingValue = slidingValueCoef * (this.props.maximumValue - this.props.minimumValue) + this.props.minimumValue;
|
||||||
return Math.floor(slidingValue);
|
return slidingValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
onStartSliding = ({ currentTarget: sliderElement, clientX: mouseX, clientY: mouseY, button }) => {
|
onStartSliding = ({ currentTarget: sliderElement, clientX: mouseX, clientY: mouseY, button }) => {
|
||||||
|
|
@ -89,10 +76,12 @@ class Slider extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const thumbStartProp = this.orientation === 'horizontal' ? 'left' : 'bottom';
|
const thumbStartProp = this.orientation === 'horizontal' ? 'left' : 'bottom';
|
||||||
const thumbStart = (this.props.value - this.props.minimumValue) / (this.props.maximumValue - this.props.minimumValue);
|
const trackBeforeSizeProp = this.orientation === 'horizontal' ? 'width' : 'height';
|
||||||
|
const thumbStart = Math.min((this.props.value - this.props.minimumValue) / (this.props.maximumValue - this.props.minimumValue), 1);
|
||||||
return (
|
return (
|
||||||
<div className={classnames(styles['slider-container'], styles[this.orientation], { [styles['active']]: this.state.active }, this.props.className)} onMouseDown={this.onStartSliding}>
|
<div className={classnames(styles['slider-container'], styles[this.orientation], this.props.className)} onMouseDown={this.onStartSliding}>
|
||||||
<div className={styles['track']} />
|
<div className={styles['track']} />
|
||||||
|
<div className={styles['track-before']} style={{ [trackBeforeSizeProp]: `calc(100% * ${thumbStart})` }} />
|
||||||
<div className={styles['thumb']} style={{ [thumbStartProp]: `calc(100% * ${thumbStart})` }} />
|
<div className={styles['thumb']} style={{ [thumbStartProp]: `calc(100% * ${thumbStart})` }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -105,9 +94,9 @@ Slider.propTypes = {
|
||||||
minimumValue: PropTypes.number.isRequired,
|
minimumValue: PropTypes.number.isRequired,
|
||||||
maximumValue: PropTypes.number.isRequired,
|
maximumValue: PropTypes.number.isRequired,
|
||||||
orientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
|
orientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
|
||||||
onSlide: PropTypes.func,
|
onSlide: PropTypes.func.isRequired,
|
||||||
onComplete: PropTypes.func,
|
onComplete: PropTypes.func.isRequired,
|
||||||
onCancel: PropTypes.func
|
onCancel: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
Slider.defaultProps = {
|
Slider.defaultProps = {
|
||||||
value: 0,
|
value: 0,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
.slider-container {
|
.slider-container {
|
||||||
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
min-width: var(--thumb-size);
|
|
||||||
min-height: var(--thumb-size);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.track {
|
.track {
|
||||||
|
|
@ -11,55 +10,58 @@
|
||||||
background-color: var(--track-color);
|
background-color: var(--track-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb {
|
.track-before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
background-color: var(--track-before-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
width: var(--thumb-size);
|
width: var(--thumb-size);
|
||||||
height: var(--thumb-size);
|
height: var(--thumb-size);
|
||||||
|
border-radius: 50%;
|
||||||
&::after {
|
background-color: var(--thumb-color);
|
||||||
display: block;
|
|
||||||
width: var(--thumb-size);
|
|
||||||
height: var(--thumb-size);
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--thumb-color);
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.horizontal {
|
&.horizontal {
|
||||||
.track {
|
.track {
|
||||||
left: 0;
|
top: calc(50% - var(--track-size) * 0.5);
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 40%;
|
left: 0;
|
||||||
bottom: 40%;
|
height: var(--track-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb::after {
|
.track-before {
|
||||||
margin-left: calc(-0.5 * var(--thumb-size));
|
top: calc(50% - var(--track-size) * 0.5);
|
||||||
|
left: 0;
|
||||||
|
height: var(--track-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
top: calc(50% - var(--thumb-size) * 0.5);
|
||||||
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.vertical {
|
&.vertical {
|
||||||
.track {
|
.track {
|
||||||
left: 40%;
|
|
||||||
right: 40%;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
left: calc(50% - var(--track-size) * 0.1);
|
||||||
|
width: var(--track-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb::after {
|
.track-before {
|
||||||
margin-top: calc(0.5 * var(--thumb-size));
|
bottom: 0;
|
||||||
}
|
left: calc(50% - var(--track-size) * 0.1);
|
||||||
}
|
width: var(--track-size);
|
||||||
|
|
||||||
&:hover, &.active {
|
|
||||||
.track {
|
|
||||||
background-color: var(--track-active-color, var(--track-color));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb::after {
|
.thumb {
|
||||||
background-color: var(--thumb-active-color, var(--thumb-color));
|
left: calc(50% - var(--thumb-size) * 0.5);
|
||||||
|
transform: translateY(50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,16 +3,25 @@ import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import Icon from 'stremio-icons/dom';
|
import Icon from 'stremio-icons/dom';
|
||||||
import { Popup } from 'stremio-common';
|
import { Popup } from 'stremio-common';
|
||||||
import TimeSlider from './TimeSlider';
|
import SeekBar from './SeekBar';
|
||||||
import VolumeSlider from './VolumeSlider';
|
import PlayPauseButton from './PlayPauseButton';
|
||||||
|
import VolumeBar from './VolumeBar';
|
||||||
|
import SubtitlesPicker from './SubtitlesPicker';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
|
const ControlBarButton = React.forwardRef(({ icon, active, onClick }, ref) => (
|
||||||
|
<div ref={ref} className={classnames(styles['control-bar-button'], { 'active': active })} onClick={onClick}>
|
||||||
|
<Icon className={styles['icon']} icon={icon} />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
class ControlBar extends Component {
|
class ControlBar extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
sharePopupOpen: false
|
sharePopupOpen: false,
|
||||||
|
subtitlesPopupOpen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,23 +31,17 @@ class ControlBar extends Component {
|
||||||
nextProps.time !== this.props.time ||
|
nextProps.time !== this.props.time ||
|
||||||
nextProps.duration !== this.props.duration ||
|
nextProps.duration !== this.props.duration ||
|
||||||
nextProps.volume !== this.props.volume ||
|
nextProps.volume !== this.props.volume ||
|
||||||
nextState.sharePopupOpen !== this.state.sharePopupOpen;
|
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) => {
|
dispatch = (...args) => {
|
||||||
this.props.setTime(time);
|
this.props.dispatch(...args);
|
||||||
}
|
|
||||||
|
|
||||||
setVolume = (volume) => {
|
|
||||||
this.props.setVolume(volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
toogleVolumeMute = () => {
|
|
||||||
this.props.volume === 0 ? this.props.unmute() : this.props.mute();
|
|
||||||
}
|
|
||||||
|
|
||||||
onPlayPauseButtonClick = () => {
|
|
||||||
this.props.paused ? this.props.play() : this.props.pause();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSharePopupOpen = () => {
|
onSharePopupOpen = () => {
|
||||||
|
|
@ -49,13 +52,54 @@ class ControlBar extends Component {
|
||||||
this.setState({ sharePopupOpen: false });
|
this.setState({ sharePopupOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSubtitlesPopupOpen = () => {
|
||||||
|
this.setState({ subtitlesPopupOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubtitlesPopupClose = () => {
|
||||||
|
this.setState({ subtitlesPopupOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSeekBar() {
|
||||||
|
return (
|
||||||
|
<SeekBar
|
||||||
|
className={styles['seek-bar']}
|
||||||
|
time={this.props.time}
|
||||||
|
duration={this.props.duration}
|
||||||
|
dispatch={this.dispatch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPlayPauseButton() {
|
||||||
|
return (
|
||||||
|
<PlayPauseButton
|
||||||
|
toggleButtonComponent={ControlBarButton}
|
||||||
|
paused={this.props.paused}
|
||||||
|
dispatch={this.dispatch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderVolumeBar() {
|
||||||
|
return (
|
||||||
|
<VolumeBar
|
||||||
|
className={styles['volume-bar']}
|
||||||
|
toggleButtonComponent={ControlBarButton}
|
||||||
|
volume={this.props.volume}
|
||||||
|
dispatch={this.dispatch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderShareButton() {
|
renderShareButton() {
|
||||||
return (
|
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>
|
<Popup.Label>
|
||||||
<div className={classnames(styles['control-bar-button'], { [styles['active']]: this.state.sharePopupOpen })}>
|
<ControlBarButton
|
||||||
<Icon className={styles['icon']} icon={'ic_share'} />
|
icon={'ic_share'}
|
||||||
</div>
|
active={this.state.sharePopupOpen}
|
||||||
|
/>
|
||||||
</Popup.Label>
|
</Popup.Label>
|
||||||
<Popup.Menu>
|
<Popup.Menu>
|
||||||
<div className={classnames(styles['popup-content'], styles['share-popup-content'])} />
|
<div className={classnames(styles['popup-content'], styles['share-popup-content'])} />
|
||||||
|
|
@ -64,57 +108,43 @@ class ControlBar extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderVolumeButton() {
|
renderSubtitlesButton() {
|
||||||
if (this.props.volume === null) {
|
if (this.props.subtitleTracks.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon = this.props.volume === 0 ? 'ic_volume0' :
|
|
||||||
this.props.volume < 50 ? 'ic_volume1' :
|
|
||||||
this.props.volume < 100 ? 'ic_volume2' :
|
|
||||||
'ic_volume3';
|
|
||||||
return (
|
return (
|
||||||
<div className={styles['control-bar-button']} onClick={this.toogleVolumeMute}>
|
<Popup className={'player-popup-container'} border={true} onOpen={this.onSubtitlesPopupOpen} onClose={this.onSubtitlesPopupClose}>
|
||||||
<Icon className={styles['icon']} icon={icon} />
|
<Popup.Label>
|
||||||
</div>
|
<ControlBarButton
|
||||||
);
|
icon={'ic_sub'}
|
||||||
}
|
active={this.state.subtitlesPopupOpen}
|
||||||
|
/>
|
||||||
renderPlayPauseButton() {
|
</Popup.Label>
|
||||||
if (this.props.paused === null) {
|
<Popup.Menu>
|
||||||
return null;
|
<SubtitlesPicker
|
||||||
}
|
className={classnames(styles['popup-content'], styles['subtitles-popup-content'])}
|
||||||
|
subtitleTracks={this.props.subtitleTracks}
|
||||||
const icon = this.props.paused ? 'ic_play' : 'ic_pause';
|
selectedSubtitleTrackId={this.props.selectedSubtitleTrackId}
|
||||||
return (
|
subtitleSize={this.props.subtitleSize}
|
||||||
<div className={styles['control-bar-button']} onClick={this.onPlayPauseButtonClick}>
|
subtitleDelay={this.props.subtitleDelay}
|
||||||
<Icon className={styles['icon']} icon={icon} />
|
subtitleDarkBackground={this.props.subtitleDarkBackground}
|
||||||
</div>
|
dispatch={this.dispatch}
|
||||||
|
/>
|
||||||
|
</Popup.Menu>
|
||||||
|
</Popup >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (['paused', 'time', 'duration', 'volume', 'subtitles'].every(propName => this.props[propName] === null)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames(styles['control-bar-container'], this.props.className)}>
|
<div className={classnames(styles['control-bar-container'], this.props.className)}>
|
||||||
<TimeSlider
|
{this.renderSeekBar()}
|
||||||
className={styles['time-slider']}
|
|
||||||
time={this.props.time}
|
|
||||||
duration={this.props.duration}
|
|
||||||
setTime={this.setTime}
|
|
||||||
/>
|
|
||||||
<div className={styles['control-bar-buttons-container']}>
|
<div className={styles['control-bar-buttons-container']}>
|
||||||
{this.renderPlayPauseButton()}
|
{this.renderPlayPauseButton()}
|
||||||
{this.renderVolumeButton()}
|
{this.renderVolumeBar()}
|
||||||
<VolumeSlider
|
<div className={styles['spacing']} />
|
||||||
className={styles['volume-slider']}
|
{this.renderSubtitlesButton()}
|
||||||
volume={this.props.volume}
|
|
||||||
setVolume={this.setVolume}
|
|
||||||
/>
|
|
||||||
<div className={styles['flex-spacing']} />
|
|
||||||
{this.renderShareButton()}
|
{this.renderShareButton()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -128,17 +158,16 @@ ControlBar.propTypes = {
|
||||||
time: PropTypes.number,
|
time: PropTypes.number,
|
||||||
duration: PropTypes.number,
|
duration: PropTypes.number,
|
||||||
volume: PropTypes.number,
|
volume: PropTypes.number,
|
||||||
subtitles: PropTypes.arrayOf(PropTypes.shape({
|
subtitleTracks: PropTypes.arrayOf(PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
language: PropTypes.string.isRequired
|
origin: PropTypes.string.isRequired
|
||||||
})),
|
})).isRequired,
|
||||||
play: PropTypes.func.isRequired,
|
selectedSubtitleTrackId: PropTypes.string,
|
||||||
pause: PropTypes.func.isRequired,
|
subtitleSize: PropTypes.number,
|
||||||
setTime: PropTypes.func.isRequired,
|
subtitleDelay: PropTypes.number,
|
||||||
setVolume: PropTypes.func.isRequired,
|
subtitleDarkBackground: PropTypes.bool,
|
||||||
mute: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired
|
||||||
unmute: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ControlBar;
|
export default ControlBar;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
@ -5,7 +5,7 @@ import debounce from 'lodash.debounce';
|
||||||
import { Slider } from 'stremio-common';
|
import { Slider } from 'stremio-common';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
class TimeSlider extends Component {
|
class SeekBar extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ class TimeSlider extends Component {
|
||||||
onComplete = (time) => {
|
onComplete = (time) => {
|
||||||
this.resetTimeDebounced();
|
this.resetTimeDebounced();
|
||||||
this.setState({ time });
|
this.setState({ time });
|
||||||
this.props.setTime(time);
|
this.props.dispatch('setProp', 'time', time);
|
||||||
}
|
}
|
||||||
|
|
||||||
onCancel = () => {
|
onCancel = () => {
|
||||||
|
|
@ -99,7 +99,7 @@ class TimeSlider extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames(styles['time-slider-container'], { [styles['active']]: this.state.time !== null }, this.props.className)}>
|
<div className={classnames(styles['seek-bar-container'], { 'active': this.state.time !== null }, this.props.className)}>
|
||||||
{this.renderTimeLabel()}
|
{this.renderTimeLabel()}
|
||||||
{this.renderSlider()}
|
{this.renderSlider()}
|
||||||
{this.renderDurationLabel()}
|
{this.renderDurationLabel()}
|
||||||
|
|
@ -108,11 +108,11 @@ class TimeSlider extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeSlider.propTypes = {
|
SeekBar.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
time: PropTypes.number,
|
time: PropTypes.number,
|
||||||
duration: PropTypes.number,
|
duration: PropTypes.number,
|
||||||
setTime: PropTypes.func.isRequired
|
dispatch: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TimeSlider;
|
export default SeekBar;
|
||||||
3
src/routes/Player/ControlBar/SeekBar/index.js
Normal file
3
src/routes/Player/ControlBar/SeekBar/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import SeekBar from './SeekBar';
|
||||||
|
|
||||||
|
export default SeekBar;
|
||||||
32
src/routes/Player/ControlBar/SeekBar/styles.less
Normal file
32
src/routes/Player/ControlBar/SeekBar/styles.less
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
.seek-bar-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 1em;
|
||||||
|
color: var(--color-surfacelight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
--thumb-size: var(--seek-bar-thumb-size);
|
||||||
|
--track-size: var(--seek-bar-track-size);
|
||||||
|
--track-before-color: var(--color-primary);
|
||||||
|
--track-color: var(--color-backgroundlighter);
|
||||||
|
--thumb-color: transparent;
|
||||||
|
flex: 1;
|
||||||
|
align-self: stretch;
|
||||||
|
margin: 0 var(--seek-bar-thumb-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:global(.active) {
|
||||||
|
.label {
|
||||||
|
color: var(--color-surfacelighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
--track-before-color: var(--color-primarylight);
|
||||||
|
--thumb-color: var(--color-surfacelighter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
235
src/routes/Player/ControlBar/SubtitlesPicker/SubtitlesPicker.js
Normal file
235
src/routes/Player/ControlBar/SubtitlesPicker/SubtitlesPicker.js
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
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 = {
|
||||||
|
'LOCAL': 1,
|
||||||
|
'EMBEDDED': 2
|
||||||
|
};
|
||||||
|
|
||||||
|
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];
|
||||||
|
if (!isNaN(valueA) && !isNaN(valueB)) return valueA - valueB;
|
||||||
|
if (!isNaN(valueA)) return -1;
|
||||||
|
if (!isNaN(valueB)) return 1;
|
||||||
|
return a - b;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSubtitleEnabled = () => {
|
||||||
|
const selectedSubtitleTrackId = this.props.selectedSubtitleTrackId === null ? this.props.subtitleTracks[0].id : null
|
||||||
|
this.props.dispatch('setProp', 'selectedSubtitleTrackId', selectedSubtitleTrackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
labelOnClick = (event) => {
|
||||||
|
const subtitleTrack = this.props.subtitleTracks.find(({ label, origin }) => {
|
||||||
|
return label === event.currentTarget.dataset.label &&
|
||||||
|
origin === event.currentTarget.dataset.origin;
|
||||||
|
});
|
||||||
|
if (subtitleTrack) {
|
||||||
|
this.props.dispatch('setProp', 'selectedSubtitleTrackId', subtitleTrack.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variantOnClick = (event) => {
|
||||||
|
this.props.dispatch('setProp', 'selectedSubtitleTrackId', event.currentTarget.dataset.trackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubtitleSize = (event) => {
|
||||||
|
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.toggleSubtitleEnabled}>
|
||||||
|
<div className={styles['toggle-label']}>ON</div>
|
||||||
|
<div className={styles['toggle-label']}>OFF</div>
|
||||||
|
<div className={classnames(styles['toggle-thumb'], { [styles['on']]: selectedTrack })} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLabelsList({ groupedTracks, selectedTrack }) {
|
||||||
|
return (
|
||||||
|
<div className={styles['labels-list-container']}>
|
||||||
|
{
|
||||||
|
Object.keys(groupedTracks)
|
||||||
|
.sort(this.subtitlesComparator(ORIGIN_PRIORITIES))
|
||||||
|
.map((origin) => (
|
||||||
|
<Fragment key={origin}>
|
||||||
|
<div className={styles['track-origin']}>{origin}</div>
|
||||||
|
{
|
||||||
|
Object.keys(groupedTracks[origin])
|
||||||
|
.sort(this.subtitlesComparator(this.props.languagePriorities))
|
||||||
|
.map((label) => {
|
||||||
|
const selected = selectedTrack && selectedTrack.label === label && selectedTrack.origin === origin;
|
||||||
|
return (
|
||||||
|
<div key={label}
|
||||||
|
className={classnames(styles['language-label'], { [styles['selected']]: selected })}
|
||||||
|
onClick={this.labelOnClick}
|
||||||
|
data-label={label}
|
||||||
|
data-origin={origin}
|
||||||
|
children={label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderVariantsList({ groupedTracks, selectedTrack }) {
|
||||||
|
if (groupedTracks[selectedTrack.origin][selectedTrack.label].length <= 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles['variants-container']}>
|
||||||
|
{groupedTracks[selectedTrack.origin][selectedTrack.label].map((track, index) => {
|
||||||
|
return (
|
||||||
|
<div key={track.id}
|
||||||
|
className={classnames(styles['variant-button'], { [styles['selected']]: track.id === selectedTrack.id })}
|
||||||
|
title={track.id}
|
||||||
|
onClick={this.variantOnClick}
|
||||||
|
data-track-id={track.id}
|
||||||
|
children={index + 1}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDarkBackgroundToggle() {
|
||||||
|
if (this.props.subtitleDarkBackground === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPreferences({ groupedTracks, selectedTrack }) {
|
||||||
|
if (!selectedTrack) {
|
||||||
|
return (
|
||||||
|
<div className={styles['preferences-container']}>
|
||||||
|
<div className={styles['subtitles-disabled-label']}>Subtitles are disabled</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles['preferences-container']}>
|
||||||
|
<div className={styles['preferences-title']}>Preferences</div>
|
||||||
|
{this.renderVariantsList({ groupedTracks, selectedTrack })}
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const selectedTrack = this.props.subtitleTracks.find(({ id }) => id === this.props.selectedSubtitleTrackId);
|
||||||
|
const groupedTracks = this.props.subtitleTracks.reduce((result, track) => {
|
||||||
|
result[track.origin] = result[track.origin] || {};
|
||||||
|
result[track.origin][track.label] = result[track.origin][track.label] || [];
|
||||||
|
result[track.origin][track.label].push(track);
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classnames(this.props.className, styles['subtitles-picker-container'])}>
|
||||||
|
{this.renderToggleButton({ selectedTrack })}
|
||||||
|
{this.renderLabelsList({ groupedTracks, selectedTrack })}
|
||||||
|
{this.renderPreferences({ groupedTracks, selectedTrack })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SubtitlesPicker.propTypes = {
|
||||||
|
className: 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,
|
||||||
|
selectedSubtitleTrackId: PropTypes.string,
|
||||||
|
subtitleSize: PropTypes.number,
|
||||||
|
subtitleDelay: PropTypes.number,
|
||||||
|
subtitleDarkBackground: PropTypes.bool,
|
||||||
|
dispatch: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
SubtitlesPicker.defaultProps = {
|
||||||
|
languagePriorities: Object.freeze({
|
||||||
|
English: 1
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SubtitlesPicker;
|
||||||
3
src/routes/Player/ControlBar/SubtitlesPicker/index.js
Normal file
3
src/routes/Player/ControlBar/SubtitlesPicker/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import SubtitlesPicker from './SubtitlesPicker';
|
||||||
|
|
||||||
|
export default SubtitlesPicker;
|
||||||
235
src/routes/Player/ControlBar/SubtitlesPicker/styles.less
Normal file
235
src/routes/Player/ControlBar/SubtitlesPicker/styles.less
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
.subtitles-picker-container {
|
||||||
|
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) * 1.5);
|
||||||
|
grid-template-rows: var(--subtitles-picker-button-size) auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"toggle-button preferences"
|
||||||
|
"labels-list preferences";
|
||||||
|
|
||||||
|
.toggle-button-container {
|
||||||
|
grid-area: toggle-button;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
width: 40%;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-surfacelight);
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-right: 20%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-thumb {
|
||||||
|
width: 40%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: var(--color-primarydark);
|
||||||
|
transition: left 0.2s;
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
left: 60%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.toggle-label {
|
||||||
|
color: var(--color-surfacelighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-thumb {
|
||||||
|
background-color: var(--color-primarylight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-list-container {
|
||||||
|
grid-area: labels-list;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
padding-right: calc(var(--scroll-bar-width) * 0.5);
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
.track-origin {
|
||||||
|
padding: 0 0.2em;
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
font-size: 85%;
|
||||||
|
font-style: italic;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
border-bottom: 1px solid var(--color-surfacelighter80);
|
||||||
|
color: var(--color-surfacelighter80);
|
||||||
|
|
||||||
|
&:not(:first-of-type) {
|
||||||
|
margin-top: 0.7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-label {
|
||||||
|
padding: 0.5em 0.3em;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-surfacelighter);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--color-primarydark);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primarylight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences-container {
|
||||||
|
grid-area: preferences;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.subtitles-disabled-label {
|
||||||
|
font-size: 150%;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
word-spacing: calc(var(--subtitles-picker-button-size) * 9);
|
||||||
|
color: var(--color-surfacelighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences-title {
|
||||||
|
height: var(--subtitles-picker-button-size);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: calc(var(--subtitles-picker-button-size) * 0.3);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 90%;
|
||||||
|
color: var(--color-surfacelight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.variants-container {
|
||||||
|
align-self: stretch;
|
||||||
|
height: calc(var(--subtitles-picker-button-size) * 2.4);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.variant-button {
|
||||||
|
flex: none;
|
||||||
|
min-width: var(--subtitles-picker-button-size);
|
||||||
|
height: var(--subtitles-picker-button-size);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--color-surfacelighter);
|
||||||
|
background-color: var(--color-backgroundlighter);
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--color-primarydark);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primarylight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-input-container {
|
||||||
|
flex: 1;
|
||||||
|
width: 60%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.number-input-button {
|
||||||
|
width: var(--subtitles-picker-button-size);
|
||||||
|
height: var(--subtitles-picker-button-size);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--color-primarydark);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.number-input-icon {
|
||||||
|
height: 50%;
|
||||||
|
fill: var(--color-surfacelighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primarylight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-input-value {
|
||||||
|
flex: 1;
|
||||||
|
height: var(--subtitles-picker-button-size);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--color-surfacelighter);
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
border-width: 1px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import TimeSlider from './TimeSlider';
|
|
||||||
|
|
||||||
export default TimeSlider;
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
.time-slider-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: calc(var(--time-slider-thumb-size) * 0.7);
|
|
||||||
line-height: 1;
|
|
||||||
color: var(--color-surfacelight);
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider {
|
|
||||||
--thumb-size: var(--time-slider-thumb-size);
|
|
||||||
--track-color: var(--color-primarydark);
|
|
||||||
--thumb-color: var(--color-primary);
|
|
||||||
--thumb-active-color: var(--color-primarylight);
|
|
||||||
flex: 1;
|
|
||||||
margin: 0 var(--time-slider-thumb-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover, &.active {
|
|
||||||
.label {
|
|
||||||
color: var(--color-surfacelighter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
88
src/routes/Player/ControlBar/VolumeBar/VolumeBar.js
Normal file
88
src/routes/Player/ControlBar/VolumeBar/VolumeBar.js
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
import { Slider } from 'stremio-common';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
|
class VolumeBar extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
volume: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
return nextState.volume !== this.state.volume ||
|
||||||
|
nextProps.className !== this.props.className ||
|
||||||
|
nextProps.volume !== this.props.volume ||
|
||||||
|
nextProps.toggleButtonComponent !== this.props.toggleButtonComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.resetVolumeDebounced.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
toogleVolumeMute = () => {
|
||||||
|
const command = this.props.volume > 0 ? 'mute' : 'unmute';
|
||||||
|
this.props.dispatch('command', command);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetVolumeDebounced = debounce(() => {
|
||||||
|
this.setState({ volume: null });
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
onSlide = (volume) => {
|
||||||
|
this.resetVolumeDebounced.cancel();
|
||||||
|
this.setState({ volume });
|
||||||
|
}
|
||||||
|
|
||||||
|
onComplete = (volume) => {
|
||||||
|
this.resetVolumeDebounced();
|
||||||
|
this.setState({ volume });
|
||||||
|
this.props.dispatch('setProp', 'volume', volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel = () => {
|
||||||
|
this.resetVolumeDebounced.cancel();
|
||||||
|
this.setState({ volume: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.props.volume === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const volume = this.state.volume !== null ? this.state.volume : this.props.volume;
|
||||||
|
const icon = volume === 0 ? 'ic_volume0' :
|
||||||
|
volume < 30 ? 'ic_volume1' :
|
||||||
|
volume < 70 ? 'ic_volume2' :
|
||||||
|
'ic_volume3';
|
||||||
|
return (
|
||||||
|
<div className={classnames(styles['volume-bar-container'], { 'active': this.state.volume !== null }, this.props.className)}>
|
||||||
|
{React.createElement(this.props.toggleButtonComponent, { icon, onClick: this.toogleVolumeMute }, null)}
|
||||||
|
<Slider
|
||||||
|
className={styles['slider']}
|
||||||
|
value={volume}
|
||||||
|
minimumValue={0}
|
||||||
|
maximumValue={100}
|
||||||
|
orientation={'horizontal'}
|
||||||
|
onSlide={this.onSlide}
|
||||||
|
onComplete={this.onComplete}
|
||||||
|
onCancel={this.onCancel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VolumeBar.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
volume: PropTypes.number,
|
||||||
|
toggleButtonComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VolumeBar;
|
||||||
3
src/routes/Player/ControlBar/VolumeBar/index.js
Normal file
3
src/routes/Player/ControlBar/VolumeBar/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import VolumeBar from './VolumeBar';
|
||||||
|
|
||||||
|
export default VolumeBar;
|
||||||
23
src/routes/Player/ControlBar/VolumeBar/styles.less
Normal file
23
src/routes/Player/ControlBar/VolumeBar/styles.less
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
.volume-bar-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
--thumb-size: var(--volume-bar-thumb-size);
|
||||||
|
--track-size: var(--volume-bar-track-size);
|
||||||
|
--track-before-color: var(--color-primary);
|
||||||
|
--track-color: var(--color-backgroundlighter);
|
||||||
|
--thumb-color: transparent;
|
||||||
|
flex: 1;
|
||||||
|
align-self: stretch;
|
||||||
|
margin: 0 calc(var(--volume-bar-thumb-size) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:global(.active) {
|
||||||
|
.slider {
|
||||||
|
--track-before-color: var(--color-primarylight);
|
||||||
|
--thumb-color: var(--color-surfacelighter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import debounce from 'lodash.debounce';
|
|
||||||
import { Slider } from 'stremio-common';
|
|
||||||
import styles from './styles';
|
|
||||||
|
|
||||||
class VolumeSlider extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
volume: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
return nextState.volume !== this.state.volume ||
|
|
||||||
nextProps.volume !== this.props.volume ||
|
|
||||||
nextProps.className !== this.props.className;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.resetVolumeDebounced.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
resetVolumeDebounced = debounce(() => {
|
|
||||||
this.setState({ volume: null });
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
onSlide = (volume) => {
|
|
||||||
this.resetVolumeDebounced.cancel();
|
|
||||||
this.setState({ volume });
|
|
||||||
}
|
|
||||||
|
|
||||||
onComplete = (volume) => {
|
|
||||||
this.resetVolumeDebounced();
|
|
||||||
this.setState({ volume });
|
|
||||||
this.props.setVolume(volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
onCancel = () => {
|
|
||||||
this.resetVolumeDebounced.cancel();
|
|
||||||
this.setState({ volume: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.props.volume === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const volume = this.state.volume !== null ? this.state.volume : this.props.volume;
|
|
||||||
return (
|
|
||||||
<Slider
|
|
||||||
className={classnames(styles['slider'], this.props.className)}
|
|
||||||
value={volume}
|
|
||||||
minimumValue={0}
|
|
||||||
maximumValue={100}
|
|
||||||
orientation={'horizontal'}
|
|
||||||
onSlide={this.onSlide}
|
|
||||||
onComplete={this.onComplete}
|
|
||||||
onCancel={this.onCancel}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VolumeSlider.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
volume: PropTypes.number,
|
|
||||||
setVolume: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VolumeSlider;
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import VolumeSlider from './VolumeSlider';
|
|
||||||
|
|
||||||
export default VolumeSlider;
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
.slider {
|
|
||||||
--thumb-size: var(--volume-slider-thumb-size);
|
|
||||||
--track-color: var(--color-surfacelight);
|
|
||||||
--thumb-color: var(--color-surfacelight);
|
|
||||||
--thumb-active-color: var(--color-surfacelighter);
|
|
||||||
--track-active-color: var(--color-surfacelighter);
|
|
||||||
margin: 0 calc(var(--volume-slider-thumb-size) * 0.5);
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +1,26 @@
|
||||||
.control-bar-container, .popup-container {
|
|
||||||
--control-bar-button-height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-bar-container {
|
.control-bar-container {
|
||||||
top: initial !important;
|
padding: 0 calc(var(--control-bar-button-size) * 0.4);
|
||||||
padding: 0 calc(var(--control-bar-button-height) * 0.4);
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
.time-slider {
|
.seek-bar {
|
||||||
--time-slider-thumb-size: calc(var(--control-bar-button-height) * 0.4);
|
--seek-bar-thumb-size: calc(var(--control-bar-button-size) * 0.40);
|
||||||
height: var(--time-slider-thumb-size);
|
--seek-bar-track-size: calc(var(--control-bar-button-size) * 0.12);
|
||||||
width: 100%;
|
height: calc(var(--control-bar-button-size) * 0.6);
|
||||||
|
font-size: calc(var(--control-bar-button-size) * 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-bar-buttons-container {
|
.control-bar-buttons-container {
|
||||||
height: var(--control-bar-button-height);
|
height: var(--control-bar-button-size);
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.control-bar-button {
|
.control-bar-button {
|
||||||
width: var(--control-bar-button-height);
|
width: var(--control-bar-button-size);
|
||||||
height: var(--control-bar-button-height);
|
height: var(--control-bar-button-size);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -34,7 +33,7 @@
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&:global(.active) {
|
||||||
background-color: var(--color-backgrounddarker);
|
background-color: var(--color-backgrounddarker);
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
|
@ -49,27 +48,50 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume-slider {
|
.volume-bar {
|
||||||
--volume-slider-thumb-size: calc(var(--control-bar-button-height) * 0.3);
|
--volume-bar-thumb-size: calc(var(--control-bar-button-size) * 0.36);
|
||||||
height: var(--volume-slider-thumb-size);
|
--volume-bar-track-size: calc(var(--control-bar-button-size) * 0.10);
|
||||||
width: calc(var(--control-bar-button-height) * 3);
|
height: var(--control-bar-button-size);
|
||||||
|
width: calc(var(--control-bar-button-size) * 5);
|
||||||
|
|
||||||
|
&:hover, &:global(.active) {
|
||||||
|
.control-bar-button {
|
||||||
|
.icon {
|
||||||
|
fill: var(--color-surfacelighter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-spacing {
|
.spacing {
|
||||||
flex: 1
|
flex: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: -1;
|
||||||
|
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 {
|
:global(.player-popup-container) {
|
||||||
--border-color: var(--color-primarylight);
|
--border-color: var(--color-primarylight);
|
||||||
|
|
||||||
.popup-content {
|
.popup-content {
|
||||||
background-color: var(--color-backgrounddark);
|
background-color: var(--color-backgrounddark);
|
||||||
|
|
||||||
&.share-popup-content {
|
&.share-popup-content {
|
||||||
width: calc(var(--control-bar-button-height) * 5);
|
width: calc(var(--control-bar-button-size) * 5);
|
||||||
height: calc(var(--control-bar-button-height) * 3);
|
height: calc(var(--control-bar-button-size) * 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.subtitles-popup-content {
|
||||||
|
--subtitles-picker-button-size: calc(var(--control-bar-button-size) * 0.6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
import Video from './Video';
|
import Video from './Video';
|
||||||
import ControlBar from './ControlBar';
|
import ControlBar from './ControlBar';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
@ -15,7 +16,11 @@ class Player extends Component {
|
||||||
time: null,
|
time: null,
|
||||||
duration: null,
|
duration: null,
|
||||||
volume: null,
|
volume: null,
|
||||||
subtitles: null
|
subtitleTracks: [],
|
||||||
|
selectedSubtitleTrackId: null,
|
||||||
|
subtitleSize: null,
|
||||||
|
subtitleDelay: null,
|
||||||
|
subtitleDarkBackground: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,18 +29,20 @@ class Player extends Component {
|
||||||
nextState.time !== this.state.time ||
|
nextState.time !== this.state.time ||
|
||||||
nextState.duration !== this.state.duration ||
|
nextState.duration !== this.state.duration ||
|
||||||
nextState.volume !== this.state.volume ||
|
nextState.volume !== this.state.volume ||
|
||||||
nextState.subtitles !== this.state.subtitles;
|
nextState.subtitleTracks !== this.state.subtitleTracks ||
|
||||||
|
nextState.selectedSubtitleTrackId !== this.state.selectedSubtitleTrackId ||
|
||||||
|
nextState.subtitleSize !== this.state.subtitleSize ||
|
||||||
|
nextState.subtitleDelay !== this.state.subtitleDelay ||
|
||||||
|
nextState.subtitleDarkBackground !== this.state.subtitleDarkBackground;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.addExtraSubtitles([
|
this.dispatch('command', 'addSubtitleTracks', [{
|
||||||
{
|
url: 'https://raw.githubusercontent.com/caitp/ng-media/master/example/assets/captions/bunny-en.vtt',
|
||||||
id: 'id1',
|
origin: 'Github',
|
||||||
url: 'https://raw.githubusercontent.com/amzn/web-app-starter-kit-for-fire-tv/master/out/mrss/assets/sample_video-en.vtt',
|
label: 'English'
|
||||||
label: 'English (Github)',
|
}]);
|
||||||
language: 'en'
|
this.dispatch('setProp', 'selectedSubtitleTrackId', 'https://raw.githubusercontent.com/caitp/ng-media/master/example/assets/captions/bunny-en.vtt');
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnded = () => {
|
onEnded = () => {
|
||||||
|
|
@ -43,16 +50,6 @@ class Player extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onError = (error) => {
|
onError = (error) => {
|
||||||
if (error.critical) {
|
|
||||||
this.stop();
|
|
||||||
this.setState({
|
|
||||||
paused: null,
|
|
||||||
time: null,
|
|
||||||
duration: null,
|
|
||||||
volume: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
alert(error.message);
|
alert(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,36 +61,8 @@ class Player extends Component {
|
||||||
this.setState({ [propName]: propValue });
|
this.setState({ [propName]: propValue });
|
||||||
}
|
}
|
||||||
|
|
||||||
play = () => {
|
dispatch = (...args) => {
|
||||||
this.videoRef.current && this.videoRef.current.dispatch('setProp', 'paused', false);
|
this.videoRef.current && this.videoRef.current.dispatch(...args);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
mute = () => {
|
|
||||||
this.videoRef.current && this.videoRef.current.dispatch('command', 'mute');
|
|
||||||
}
|
|
||||||
|
|
||||||
unmute = () => {
|
|
||||||
this.videoRef.current && this.videoRef.current.dispatch('command', 'unmute');
|
|
||||||
}
|
|
||||||
|
|
||||||
addExtraSubtitles = (subtitles) => {
|
|
||||||
this.videoRef.current && this.videoRef.current.dispatch('command', 'addExtraSubtitles', subtitles);
|
|
||||||
}
|
|
||||||
|
|
||||||
stop = () => {
|
|
||||||
this.videoRef.current && this.videoRef.current.dispatch('command', 'stop');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderVideo() {
|
renderVideo() {
|
||||||
|
|
@ -116,18 +85,17 @@ class Player extends Component {
|
||||||
renderControlBar() {
|
renderControlBar() {
|
||||||
return (
|
return (
|
||||||
<ControlBar
|
<ControlBar
|
||||||
className={styles['layer']}
|
className={classnames(styles['layer'], styles['control-bar-layer'])}
|
||||||
paused={this.state.paused}
|
paused={this.state.paused}
|
||||||
time={this.state.time}
|
time={this.state.time}
|
||||||
duration={this.state.duration}
|
duration={this.state.duration}
|
||||||
volume={this.state.volume}
|
volume={this.state.volume}
|
||||||
subtitles={this.state.subtitles}
|
subtitleTracks={this.state.subtitleTracks}
|
||||||
play={this.play}
|
selectedSubtitleTrackId={this.state.selectedSubtitleTrackId}
|
||||||
pause={this.pause}
|
subtitleSize={this.state.subtitleSize}
|
||||||
setTime={this.setTime}
|
subtitleDelay={this.state.subtitleDelay}
|
||||||
setVolume={this.setVolume}
|
subtitleDarkBackground={this.state.subtitleDarkBackground}
|
||||||
mute={this.mute}
|
dispatch={this.dispatch}
|
||||||
unmute={this.unmute}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -146,10 +114,10 @@ Player.propTypes = {
|
||||||
stream: PropTypes.object.isRequired
|
stream: PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
Player.defaultProps = {
|
Player.defaultProps = {
|
||||||
stream: {
|
stream: Object.freeze({
|
||||||
// ytId: 'E4A0bcCQke0',
|
// ytId: 'E4A0bcCQke0',
|
||||||
url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'
|
url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'
|
||||||
}
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Player;
|
export default Player;
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class Video extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.dispatch('stop');
|
this.dispatch('command', 'destroy');
|
||||||
}
|
}
|
||||||
|
|
||||||
selectVideoImplementation = () => {
|
selectVideoImplementation = () => {
|
||||||
|
|
@ -67,7 +67,7 @@ Video.propTypes = {
|
||||||
onPropChanged: PropTypes.func.isRequired
|
onPropChanged: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
Video.defaultProps = {
|
Video.defaultProps = {
|
||||||
extra: {}
|
extra: Object.freeze({})
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Video;
|
export default Video;
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,102 @@
|
||||||
var EventEmitter = require('events');
|
var EventEmitter = require('events');
|
||||||
|
var subtitleUtils = require('./utils/subtitles');
|
||||||
|
|
||||||
var HTMLVideo = function(containerElement) {
|
var HTMLVideo = function(containerElement) {
|
||||||
var style = document.createElement('style');
|
if (!(containerElement instanceof HTMLElement)) {
|
||||||
containerElement.appendChild(style);
|
throw new Error('Instance of HTMLElement required as a first argument');
|
||||||
style.sheet.insertRule('#' + containerElement.id + ' video { width: 100%; height: 100%; }', style.sheet.cssRules.length);
|
}
|
||||||
style.sheet.insertRule('#' + containerElement.id + ' video::cue { font-size: 22px; }', style.sheet.cssRules.length);
|
|
||||||
|
var self = this;
|
||||||
|
var events = new EventEmitter();
|
||||||
|
var loaded = false;
|
||||||
|
var destroyed = false;
|
||||||
|
var dispatchArgsQueue = [];
|
||||||
|
var subtitleCues = {};
|
||||||
|
var subtitleTracks = [];
|
||||||
|
var selectedSubtitleTrackId = null;
|
||||||
|
var subtitleDelay = 0;
|
||||||
|
var stylesElement = document.createElement('style');
|
||||||
var videoElement = document.createElement('video');
|
var videoElement = document.createElement('video');
|
||||||
|
var subtitlesElement = document.createElement('div');
|
||||||
|
|
||||||
|
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);
|
containerElement.appendChild(videoElement);
|
||||||
videoElement.crossOrigin = 'anonymous';
|
videoElement.crossOrigin = 'anonymous';
|
||||||
videoElement.autoplay = true;
|
videoElement.controls = false;
|
||||||
var events = new EventEmitter();
|
containerElement.appendChild(subtitlesElement);
|
||||||
var ready = false;
|
subtitlesElement.classList.add('subtitles');
|
||||||
var dispatchArgsQueue = [];
|
|
||||||
var onEnded = function() {
|
function getPaused() {
|
||||||
|
if (!loaded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!videoElement.paused;
|
||||||
|
}
|
||||||
|
function getTime() {
|
||||||
|
if (!loaded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(videoElement.currentTime * 1000);
|
||||||
|
}
|
||||||
|
function getDuration() {
|
||||||
|
if (!loaded || isNaN(videoElement.duration)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(videoElement.duration * 1000);
|
||||||
|
}
|
||||||
|
function getVolume() {
|
||||||
|
if (destroyed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoElement.muted ? 0 : Math.floor(videoElement.volume * 100);
|
||||||
|
}
|
||||||
|
function getSubtitleTracks() {
|
||||||
|
if (!loaded) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return subtitleTracks.slice();
|
||||||
|
}
|
||||||
|
function getSelectedSubtitleTrackId() {
|
||||||
|
if (!loaded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedSubtitleTrackId;
|
||||||
|
}
|
||||||
|
function getSubtitleDelay() {
|
||||||
|
if (!loaded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return subtitleDelay;
|
||||||
|
}
|
||||||
|
function getSubtitleSize() {
|
||||||
|
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');
|
events.emit('ended');
|
||||||
};
|
}
|
||||||
var onError = function() {
|
function onError() {
|
||||||
var message;
|
var message;
|
||||||
var critical;
|
var critical;
|
||||||
switch (videoElement.error.code) {
|
switch (videoElement.error.code) {
|
||||||
|
|
@ -45,164 +126,310 @@ var HTMLVideo = function(containerElement) {
|
||||||
message: message,
|
message: message,
|
||||||
critical: critical
|
critical: critical
|
||||||
});
|
});
|
||||||
};
|
|
||||||
var onPausedChanged = function() {
|
if (critical) {
|
||||||
events.emit('propChanged', 'paused', videoElement.paused);
|
self.dispatch('command', 'stop');
|
||||||
};
|
}
|
||||||
var onTimeChanged = function() {
|
}
|
||||||
events.emit('propChanged', 'time', videoElement.currentTime * 1000);
|
function onPausedChanged() {
|
||||||
};
|
events.emit('propChanged', 'paused', getPaused());
|
||||||
var onDurationChanged = function() {
|
}
|
||||||
events.emit('propChanged', 'duration', isNaN(videoElement.duration) ? null : videoElement.duration * 1000);
|
function onTimeChanged() {
|
||||||
};
|
events.emit('propChanged', 'time', getTime());
|
||||||
var onVolumeChanged = function() {
|
}
|
||||||
events.emit('propChanged', 'volume', !videoElement.muted ? videoElement.volume * 100 : 0);
|
function onDurationChanged() {
|
||||||
};
|
events.emit('propChanged', 'duration', getDuration());
|
||||||
var onSubtitlesChanged = function() {
|
}
|
||||||
var subtitles = [];
|
function onVolumeChanged() {
|
||||||
for (var i = 0; i < videoElement.textTracks.length; i++) {
|
events.emit('propChanged', 'volume', getVolume());
|
||||||
if (videoElement.textTracks[i].kind === 'subtitles') {
|
}
|
||||||
subtitles.push({
|
function onSubtitleTracksChanged() {
|
||||||
id: videoElement.textTracks[i].id,
|
events.emit('propChanged', 'subtitleTracks', getSubtitleTracks());
|
||||||
language: videoElement.textTracks[i].language,
|
}
|
||||||
label: videoElement.textTracks[i].label
|
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 (subtitlesElement.hasChildNodes()) {
|
||||||
|
subtitlesElement.removeChild(subtitlesElement.lastChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
events.emit('propChanged', 'subtitles', subtitles);
|
if (!loaded || !Array.isArray(subtitleCues.times)) {
|
||||||
};
|
return;
|
||||||
var onReady = function() {
|
}
|
||||||
ready = true;
|
|
||||||
|
var time = getTime() + getSubtitleDelay();
|
||||||
|
var cuesForTime = subtitleUtils.cuesForTime(subtitleCues, time);
|
||||||
|
for (var i = 0; i < cuesForTime.length; i++) {
|
||||||
|
var cueNode = subtitleUtils.render(cuesForTime[i]);
|
||||||
|
cueNode.classList.add('cue');
|
||||||
|
subtitlesElement.append(cueNode, document.createElement('br'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function flushArgsQueue() {
|
||||||
for (var i = 0; i < dispatchArgsQueue.length; i++) {
|
for (var i = 0; i < dispatchArgsQueue.length; i++) {
|
||||||
this.dispatch.apply(this, dispatchArgsQueue[i]);
|
self.dispatch.apply(self, dispatchArgsQueue[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchArgsQueue = [];
|
dispatchArgsQueue = [];
|
||||||
}.bind(this);
|
}
|
||||||
videoElement.addEventListener('ended', onEnded);
|
|
||||||
videoElement.addEventListener('error', onError);
|
|
||||||
|
|
||||||
this.on = function(eventName, listener) {
|
this.on = function(eventName, listener) {
|
||||||
|
if (destroyed) {
|
||||||
|
throw new Error('Unable to add ' + eventName + ' listener to destroyed video');
|
||||||
|
}
|
||||||
|
|
||||||
events.on(eventName, listener);
|
events.on(eventName, listener);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dispatch = function() {
|
this.dispatch = function() {
|
||||||
if (arguments[0] === 'observeProp') {
|
if (destroyed) {
|
||||||
switch (arguments[1]) {
|
throw new Error('Unable to dispatch ' + arguments[0] + ' to destroyed video');
|
||||||
case 'paused':
|
|
||||||
events.emit('propValue', 'paused', videoElement.paused);
|
|
||||||
videoElement.removeEventListener('pause', onPausedChanged);
|
|
||||||
videoElement.removeEventListener('play', onPausedChanged);
|
|
||||||
videoElement.addEventListener('pause', onPausedChanged);
|
|
||||||
videoElement.addEventListener('play', onPausedChanged);
|
|
||||||
return;
|
|
||||||
case 'time':
|
|
||||||
events.emit('propValue', 'time', videoElement.currentTime * 1000);
|
|
||||||
videoElement.removeEventListener('timeupdate', onTimeChanged);
|
|
||||||
videoElement.addEventListener('timeupdate', onTimeChanged);
|
|
||||||
return;
|
|
||||||
case 'duration':
|
|
||||||
events.emit('propValue', 'duration', isNaN(videoElement.duration) ? null : videoElement.duration * 1000);
|
|
||||||
videoElement.removeEventListener('durationchange', onDurationChanged);
|
|
||||||
videoElement.addEventListener('durationchange', onDurationChanged);
|
|
||||||
return;
|
|
||||||
case 'volume':
|
|
||||||
events.emit('propValue', 'volume', !videoElement.muted ? videoElement.volume * 100 : 0);
|
|
||||||
videoElement.removeEventListener('volumechange', onVolumeChanged);
|
|
||||||
videoElement.addEventListener('volumechange', onVolumeChanged);
|
|
||||||
return;
|
|
||||||
case 'subtitles':
|
|
||||||
var subtitles = [];
|
|
||||||
for (var i = 0; i < videoElement.textTracks.length; i++) {
|
|
||||||
if (videoElement.textTracks[i].kind === 'subtitles') {
|
|
||||||
subtitles.push({
|
|
||||||
id: videoElement.textTracks[i].id,
|
|
||||||
language: videoElement.textTracks[i].language,
|
|
||||||
label: videoElement.textTracks[i].label
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
events.emit('propValue', 'subtitles', subtitles);
|
|
||||||
videoElement.textTracks.removeEventListener('addtrack', onSubtitlesChanged);
|
|
||||||
videoElement.textTracks.removeEventListener('removetrack', onSubtitlesChanged);
|
|
||||||
videoElement.textTracks.addEventListener('addtrack', onSubtitlesChanged);
|
|
||||||
videoElement.textTracks.addEventListener('removetrack', onSubtitlesChanged);
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
throw new Error('observeProp not supported: ' + arguments[1]);
|
|
||||||
}
|
|
||||||
} else if (arguments[0] === 'setProp') {
|
|
||||||
switch (arguments[1]) {
|
|
||||||
case 'paused':
|
|
||||||
arguments[2] ? videoElement.pause() : videoElement.play();
|
|
||||||
return;
|
|
||||||
case 'time':
|
|
||||||
videoElement.currentTime = arguments[2] / 1000;
|
|
||||||
return;
|
|
||||||
case 'volume':
|
|
||||||
videoElement.muted = false;
|
|
||||||
videoElement.volume = arguments[2] / 100;
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
throw new Error('setProp not supported: ' + arguments[1]);
|
|
||||||
}
|
|
||||||
} else if (arguments[0] === 'command') {
|
|
||||||
switch (arguments[1]) {
|
|
||||||
case 'load':
|
|
||||||
if (!isNaN(arguments[3].time)) {
|
|
||||||
videoElement.currentTime = arguments[3].time / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
videoElement.src = arguments[2].url;
|
|
||||||
videoElement.load();
|
|
||||||
onReady();
|
|
||||||
return;
|
|
||||||
case 'mute':
|
|
||||||
videoElement.muted = true;
|
|
||||||
break;
|
|
||||||
case 'unmute':
|
|
||||||
if (videoElement.volume === 0) {
|
|
||||||
videoElement.volume = 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
videoElement.muted = false;
|
|
||||||
break;
|
|
||||||
case 'addExtraSubtitles':
|
|
||||||
if (ready) {
|
|
||||||
for (var i = 0; i < arguments[2].length; i++) {
|
|
||||||
var track = document.createElement('track');
|
|
||||||
track.kind = 'subtitles';
|
|
||||||
track.id = arguments[2][i].id;
|
|
||||||
track.src = arguments[2][i].url;
|
|
||||||
track.label = arguments[2][i].label;
|
|
||||||
track.srclang = arguments[2][i].language;
|
|
||||||
videoElement.appendChild(track);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'stop':
|
|
||||||
events.removeAllListeners();
|
|
||||||
videoElement.removeEventListener('ended', onEnded);
|
|
||||||
videoElement.removeEventListener('error', onError);
|
|
||||||
videoElement.removeEventListener('pause', onPausedChanged);
|
|
||||||
videoElement.removeEventListener('play', onPausedChanged);
|
|
||||||
videoElement.removeEventListener('timeupdate', onTimeChanged);
|
|
||||||
videoElement.removeEventListener('durationchange', onDurationChanged);
|
|
||||||
videoElement.removeEventListener('volumechange', onVolumeChanged);
|
|
||||||
videoElement.textTracks.removeEventListener('addtrack', onSubtitlesChanged);
|
|
||||||
videoElement.textTracks.removeEventListener('removetrack', onSubtitlesChanged);
|
|
||||||
videoElement.removeAttribute('src');
|
|
||||||
videoElement.load();
|
|
||||||
ready = false;
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
throw new Error('command not supported: ' + arguments[1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ready) {
|
switch (arguments[0]) {
|
||||||
|
case 'observeProp':
|
||||||
|
switch (arguments[1]) {
|
||||||
|
case 'paused':
|
||||||
|
events.emit('propValue', 'paused', getPaused());
|
||||||
|
videoElement.removeEventListener('pause', onPausedChanged);
|
||||||
|
videoElement.removeEventListener('play', onPausedChanged);
|
||||||
|
videoElement.addEventListener('pause', onPausedChanged);
|
||||||
|
videoElement.addEventListener('play', onPausedChanged);
|
||||||
|
return;
|
||||||
|
case 'time':
|
||||||
|
events.emit('propValue', 'time', getTime());
|
||||||
|
videoElement.removeEventListener('timeupdate', onTimeChanged);
|
||||||
|
videoElement.addEventListener('timeupdate', onTimeChanged);
|
||||||
|
return;
|
||||||
|
case 'duration':
|
||||||
|
events.emit('propValue', 'duration', getDuration());
|
||||||
|
videoElement.removeEventListener('durationchange', onDurationChanged);
|
||||||
|
videoElement.addEventListener('durationchange', onDurationChanged);
|
||||||
|
return;
|
||||||
|
case 'volume':
|
||||||
|
events.emit('propValue', 'volume', getVolume());
|
||||||
|
videoElement.removeEventListener('volumechange', onVolumeChanged);
|
||||||
|
videoElement.addEventListener('volumechange', onVolumeChanged);
|
||||||
|
return;
|
||||||
|
case 'subtitleTracks':
|
||||||
|
events.emit('propValue', 'subtitleTracks', getSubtitleTracks());
|
||||||
|
return;
|
||||||
|
case 'selectedSubtitleTrackId':
|
||||||
|
events.emit('propValue', 'selectedSubtitleTrackId', getSelectedSubtitleTrackId());
|
||||||
|
return;
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
case 'setProp':
|
||||||
|
switch (arguments[1]) {
|
||||||
|
case 'paused':
|
||||||
|
if (loaded) {
|
||||||
|
arguments[2] ? videoElement.pause() : videoElement.play();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'time':
|
||||||
|
if (loaded) {
|
||||||
|
if (!isNaN(arguments[2])) {
|
||||||
|
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];
|
||||||
|
if (subtitleTrack.id === arguments[2]) {
|
||||||
|
selectedSubtitleTrackId = subtitleTrack.id;
|
||||||
|
fetch(subtitleTrack.url)
|
||||||
|
.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: 71,
|
||||||
|
message: 'Failed to parse subtitles from ' + subtitleTrack.origin,
|
||||||
|
critical: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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])) {
|
||||||
|
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])) {
|
||||||
|
videoElement.muted = false;
|
||||||
|
videoElement.volume = arguments[2] / 100;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
throw new Error('setProp not supported: ' + arguments[1]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'command':
|
||||||
|
switch (arguments[1]) {
|
||||||
|
case 'addSubtitleTracks':
|
||||||
|
if (loaded) {
|
||||||
|
var extraSubtitleTracks = (Array.isArray(arguments[2]) ? arguments[2] : [])
|
||||||
|
.filter(function(track) {
|
||||||
|
return track &&
|
||||||
|
typeof track.url === 'string' &&
|
||||||
|
track.url.length > 0 &&
|
||||||
|
typeof track.origin === 'string' &&
|
||||||
|
track.origin.length > 0 &&
|
||||||
|
track.origin !== 'EMBEDDED';
|
||||||
|
})
|
||||||
|
.map(function(track) {
|
||||||
|
return Object.freeze(Object.assign({}, track, {
|
||||||
|
id: track.url
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
subtitleTracks = subtitleTracks.concat(extraSubtitleTracks)
|
||||||
|
.filter(function(track, index, tracks) {
|
||||||
|
for (var i = 0; i < tracks.length; i++) {
|
||||||
|
if (tracks[i].id === track.id) {
|
||||||
|
return i === index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
onSubtitleTracksChanged();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'mute':
|
||||||
|
videoElement.muted = true;
|
||||||
|
return;
|
||||||
|
case 'unmute':
|
||||||
|
videoElement.volume = videoElement.volume !== 0 ? videoElement.volume : 0.5;
|
||||||
|
videoElement.muted = false;
|
||||||
|
return;
|
||||||
|
case 'stop':
|
||||||
|
videoElement.removeEventListener('ended', onEnded);
|
||||||
|
videoElement.removeEventListener('error', onError);
|
||||||
|
videoElement.removeEventListener('timeupdate', updateSubtitleText);
|
||||||
|
loaded = false;
|
||||||
|
dispatchArgsQueue = [];
|
||||||
|
subtitleCues = {};
|
||||||
|
subtitleTracks = [];
|
||||||
|
selectedSubtitleTrackId = null;
|
||||||
|
subtitleDelay = 0;
|
||||||
|
videoElement.removeAttribute('src');
|
||||||
|
videoElement.load();
|
||||||
|
videoElement.currentTime = 0;
|
||||||
|
onPausedChanged();
|
||||||
|
onTimeChanged();
|
||||||
|
onDurationChanged();
|
||||||
|
onSubtitleTracksChanged();
|
||||||
|
onSelectedSubtitleTrackIdChanged();
|
||||||
|
onSubtitleDelayChanged();
|
||||||
|
updateSubtitleText();
|
||||||
|
return;
|
||||||
|
case 'load':
|
||||||
|
var dispatchArgsQueueCopy = dispatchArgsQueue.slice();
|
||||||
|
self.dispatch('command', 'stop');
|
||||||
|
dispatchArgsQueue = dispatchArgsQueueCopy;
|
||||||
|
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();
|
||||||
|
updateSubtitleText();
|
||||||
|
flushArgsQueue();
|
||||||
|
return;
|
||||||
|
case 'destroy':
|
||||||
|
self.dispatch('command', 'stop');
|
||||||
|
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);
|
||||||
|
containerElement.removeChild(videoElement);
|
||||||
|
containerElement.removeChild(stylesElement);
|
||||||
|
containerElement.removeChild(subtitlesElement);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
throw new Error('command not supported: ' + arguments[1]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid dispatch call: ' + Array.from(arguments).map(String));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
dispatchArgsQueue.push(Array.from(arguments));
|
dispatchArgsQueue.push(Array.from(arguments));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -211,7 +438,7 @@ var HTMLVideo = function(containerElement) {
|
||||||
HTMLVideo.manifest = {
|
HTMLVideo.manifest = {
|
||||||
name: 'HTMLVideo',
|
name: 'HTMLVideo',
|
||||||
embedded: true,
|
embedded: true,
|
||||||
props: ['paused', 'time', 'duration', 'volume', 'subtitles']
|
props: ['paused', 'time', 'duration', 'volume', 'subtitleTracks', 'selectedSubtitleTrackId', 'subtitleSize', 'subtitleDelay', 'subtitleDarkBackground']
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = HTMLVideo;
|
module.exports = HTMLVideo;
|
||||||
|
|
|
||||||
85
src/routes/Player/Video/stremio-video/utils/subtitles.js
Normal file
85
src/routes/Player/Video/stremio-video/utils/subtitles.js
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
var VTTJS = require('vtt.js');
|
||||||
|
|
||||||
|
function binarySearchUpperBound(array, value) {
|
||||||
|
if (value < array[0] || array[array.length - 1] < value) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var left = 0;
|
||||||
|
var right = array.length - 1;
|
||||||
|
var index = -1;
|
||||||
|
while (left <= right) {
|
||||||
|
var middle = Math.floor((left + right) / 2);
|
||||||
|
if (array[middle] > value) {
|
||||||
|
right = middle - 1;
|
||||||
|
} else if (array[middle] < value) {
|
||||||
|
left = middle + 1;
|
||||||
|
} else {
|
||||||
|
index = middle;
|
||||||
|
left = middle + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return index !== -1 ? index : right;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse(text) {
|
||||||
|
var nativeVTTCue = VTTCue;
|
||||||
|
global.VTTCue = VTTJS.VTTCue;
|
||||||
|
var cues = [];
|
||||||
|
var cuesForTime = {};
|
||||||
|
var parser = new VTTJS.WebVTT.Parser(window, VTTJS.WebVTT.StringDecoder());
|
||||||
|
parser.oncue = function(c) {
|
||||||
|
var cue = {
|
||||||
|
startTime: (c.startTime * 1000) | 0,
|
||||||
|
endTime: (c.endTime * 1000) | 0,
|
||||||
|
text: c.text
|
||||||
|
};
|
||||||
|
cues.push(cue);
|
||||||
|
cuesForTime[cue.startTime] = cuesForTime[cue.startTime] || [];
|
||||||
|
cuesForTime[cue.endTime] = cuesForTime[cue.endTime] || [];
|
||||||
|
};
|
||||||
|
parser.parse(text);
|
||||||
|
parser.flush();
|
||||||
|
cuesForTime.times = Object.keys(cuesForTime)
|
||||||
|
.map(function(time) {
|
||||||
|
return parseInt(time);
|
||||||
|
})
|
||||||
|
.sort(function(t1, t2) {
|
||||||
|
return t1 - t2;
|
||||||
|
});
|
||||||
|
for (var i = 0; i < cues.length; i++) {
|
||||||
|
cuesForTime[cues[i].startTime].push(cues[i]);
|
||||||
|
var startTimeIndex = binarySearchUpperBound(cuesForTime.times, cues[i].startTime);
|
||||||
|
for (var j = startTimeIndex + 1; j < cuesForTime.times.length; j++) {
|
||||||
|
if (cues[i].endTime <= cuesForTime.times[j]) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cuesForTime[cuesForTime.times[j]].push(cues[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var i = 0; i < cuesForTime.times.length; i++) {
|
||||||
|
cuesForTime[cuesForTime.times[i]].sort(function(c1, c2) {
|
||||||
|
return c1.startTime - c2.startTime ||
|
||||||
|
c1.endTime - c2.endTime;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
global.VTTCue = nativeVTTCue;
|
||||||
|
return cuesForTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cuesForTime(cues, time) {
|
||||||
|
var index = binarySearchUpperBound(cues.times, time);
|
||||||
|
return index !== -1 ? cues[cues.times[index]] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(cue) {
|
||||||
|
return VTTJS.WebVTT.convertCueToDOMTree(window, cue.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
parse,
|
||||||
|
cuesForTime,
|
||||||
|
render
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
|
.player-container, :global(.player-popup-container) {
|
||||||
|
--control-bar-button-size: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
.player-container {
|
.player-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -11,5 +16,13 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 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