├── .gitignore
├── .vscode
├── extensions.json
└── settings.json
├── README.md
├── images.d.ts
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── screenshots
├── edit.png
├── fullplayer1.gif
├── fullplayer2.gif
├── home.png
├── mobile.gif
├── reactube.svg
└── video-preview.png
├── src
├── App.test.tsx
├── App.tsx
├── AppProvider.tsx
├── components
│ ├── Form
│ │ ├── InputLabel.tsx
│ │ ├── InputTime.tsx
│ │ └── styles.tsx
│ ├── FullVideo
│ │ ├── BigPlayButton.tsx
│ │ ├── ComingNext.tsx
│ │ ├── ControlBar.tsx
│ │ ├── EditButton.tsx
│ │ ├── FragmentControl.tsx
│ │ ├── FullVideo.tsx
│ │ ├── FullscreenToggle.tsx
│ │ ├── LoadingSpinner.tsx
│ │ ├── PlayToggle.tsx
│ │ ├── PlaylistButtons.tsx
│ │ ├── ProgressControl.tsx
│ │ ├── Provider.tsx
│ │ ├── Shortcut.tsx
│ │ ├── Video.tsx
│ │ ├── VolumeMenuButton.tsx
│ │ ├── consts.ts
│ │ └── styles.tsx
│ ├── Playlist
│ │ ├── Item.tsx
│ │ ├── Playlist.tsx
│ │ └── styles.tsx
│ ├── VideoItem
│ │ ├── VideoItem.tsx
│ │ └── styles.tsx
│ ├── VideoPlayer
│ │ ├── VideoPlayer.tsx
│ │ └── styles.tsx
│ ├── index.ts
│ ├── styles.tsx
│ └── types.ts
├── container
│ ├── MobileVideo
│ │ ├── MobileVideo.tsx
│ │ └── styles.tsx
│ └── Navbar
│ │ ├── Navbar.tsx
│ │ ├── SearchBox.tsx
│ │ └── styles.tsx
├── db.ts
├── index.tsx
├── registerServiceWorker.ts
├── screens
│ ├── Edit
│ │ ├── Edit.tsx
│ │ ├── EditForm.tsx
│ │ ├── type.ts
│ │ └── utils.ts
│ ├── FullPlayer
│ │ └── FullPlayer.tsx
│ ├── Home
│ │ └── Home.tsx
│ └── Search
│ │ └── Search.tsx
├── styles.css
├── theme
│ └── index.ts
└── utils
│ └── index.ts
├── tsconfig.json
├── tsconfig.prod.json
├── tsconfig.test.json
├── tslint.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 | .yarn.lock
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "eg2.tslint",
4 | "dbaeumer.vscode-eslint",
5 | "msjsdiag.debugger-for-chrome"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "prettier.eslintIntegration": true,
3 | "files.exclude": {
4 | ".git": true,
5 | ".build": true,
6 | "**/.DS_Store": true,
7 | "build/**/*.js": {
8 | "when": "$(basename).ts"
9 | }
10 | },
11 | "tslint.enable": true,
12 | "editor.rulers": [80],
13 | "html.format.endWithNewline": true,
14 | "files.trimTrailingWhitespace": true,
15 | "editor.trimAutoWhitespace": true,
16 | "typescript.tsdk": ".\\node_modules\\typescript\\lib",
17 | "tslint.autoFixOnSave": true
18 | }
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | with :rocket: Typescript :rocket:
9 |
10 |
11 | Reactube-client is an open source project relying on [React context](https://reactjs.org/docs/context.html) an useful feature of React that it is great for passing down data to deeply nested components. In this project, I tried to show some features of react/react components, react context with Typescript.
12 |
13 | # [LIVE DEMO (WIP)](http://rafaelescala.com/reactube/)
14 |
15 | 
16 |
17 | ## Main Features:
18 |
19 | * Video player customized
20 | * Playlist
21 | * Preview videos
22 | * Responsive
23 | * It's possible crop videos
24 | * Support with localstorage
25 |
26 | ## Contain:
27 |
28 | - [x] React
29 | - [x] Typescript
30 | - [x] React Context (not Redux)
31 | - [x] Styled components
32 | - [x] React Router
33 |
34 | ## Build Setup
35 |
36 | ```` bash
37 | # install dependencies
38 | npm install
39 |
40 | # serve with hot reload at localhost:3000
41 | npm run start
42 | ````
43 |
44 | ## Screencast:
45 |
46 | :tv: Responsive
47 |
48 | 
49 | ___
50 |
51 | :scissors: Crop videos
52 |
53 | 
54 | ___
55 |
56 | :house: Homepage
57 |
58 | 
59 | ___
60 |
61 | :tv: Video preview
62 |
63 | 
64 | ___
65 |
66 | :pencil2: Edit video
67 |
68 | 
69 |
70 | ## Contributing :heart:
71 |
72 | [Reactube-client](http://rafaelescala.com/reactube/) has been made by love:heart:.
73 | I'd greatly appreciate any contribution to improve this project. Feel free to sent a PR.
74 |
75 | ## Acknowledgments
76 |
77 | * React
78 | * JavaScript
79 | * TypeScript
80 |
81 | ## Author and license
82 |
83 | MIT License
84 |
85 | Copyright (c) 2018-present, [Rafael Escala](https://github.com/rafaesc)
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/images.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg'
2 | declare module '*.png'
3 | declare module '*.jpg'
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactube",
3 | "version": "0.1.0",
4 | "homepage": ".",
5 | "private": true,
6 | "dependencies": {
7 | "rc-slider": "^8.6.1",
8 | "rc-switch": "^1.6.0",
9 | "react": "^16.4.1",
10 | "react-dom": "^16.4.1",
11 | "react-router": "^4.3.1",
12 | "react-router-dom": "^4.3.1",
13 | "react-scripts-ts": "2.16.0",
14 | "react-tagsinput": "^3.19.0",
15 | "react-text-mask": "^5.4.2",
16 | "styled-components": "^3.3.2"
17 | },
18 | "scripts": {
19 | "start": "react-scripts-ts start",
20 | "build": "react-scripts-ts build",
21 | "test": "react-scripts-ts test --env=jsdom",
22 | "eject": "react-scripts-ts eject"
23 | },
24 | "devDependencies": {
25 | "@types/jest": "^23.1.1",
26 | "@types/node": "^10.3.4",
27 | "@types/react": "^16.4.0",
28 | "@types/react-dom": "^16.0.6",
29 | "@types/react-router-dom": "^4.2.7",
30 | "typescript": "^2.9.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafaesc/reactube-client/cbf3d9d95a3964564e961ae1ff48661db22c7481/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
14 |
15 |
16 |
17 |
26 | React App
27 |
28 |
29 |
30 |
33 |
34 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/screenshots/edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafaesc/reactube-client/cbf3d9d95a3964564e961ae1ff48661db22c7481/screenshots/edit.png
--------------------------------------------------------------------------------
/screenshots/fullplayer1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafaesc/reactube-client/cbf3d9d95a3964564e961ae1ff48661db22c7481/screenshots/fullplayer1.gif
--------------------------------------------------------------------------------
/screenshots/fullplayer2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafaesc/reactube-client/cbf3d9d95a3964564e961ae1ff48661db22c7481/screenshots/fullplayer2.gif
--------------------------------------------------------------------------------
/screenshots/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafaesc/reactube-client/cbf3d9d95a3964564e961ae1ff48661db22c7481/screenshots/home.png
--------------------------------------------------------------------------------
/screenshots/mobile.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafaesc/reactube-client/cbf3d9d95a3964564e961ae1ff48661db22c7481/screenshots/mobile.gif
--------------------------------------------------------------------------------
/screenshots/reactube.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
55 |
--------------------------------------------------------------------------------
/screenshots/video-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafaesc/reactube-client/cbf3d9d95a3964564e961ae1ff48661db22c7481/screenshots/video-preview.png
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { HashRouter as Router, Route } from "react-router-dom";
3 | import AppProvider from "./AppProvider";
4 | import Navbar from "./container/Navbar/Navbar";
5 | import MobileVideo from "./container/MobileVideo/MobileVideo";
6 | import Home from "./screens/Home/Home";
7 | import FullPlayer from "./screens/FullPlayer/FullPlayer";
8 | import Search from "./screens/Search/Search";
9 | import Edit from "./screens/Edit/Edit";
10 | import { isMobile } from "./utils";
11 |
12 | export default () => {
13 | return (
14 |
15 |
16 |
17 | {isMobile && }
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/AppProvider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { IVideoClip, IVideoClipOptional } from "./components";
3 | import {
4 | findVideoClipIndexForId,
5 | copy,
6 | getStorage,
7 | setStorage,
8 | cleanDeprecatedStorage,
9 | arrayShuffle
10 | } from "./utils";
11 | import db from "./db";
12 |
13 | const NAME_LOCALSTORAGE = "dbYoutubeReact";
14 | const VERSION_LOCALSTORAGE = "3";
15 |
16 | const KEY_LOCALSTORAGE = NAME_LOCALSTORAGE + VERSION_LOCALSTORAGE;
17 |
18 | const Context = React.createContext({});
19 |
20 | export interface IState {
21 | idVideo: string;
22 | inFullPlayer: boolean;
23 | playlist: IVideoClip[];
24 | autoPlaylist: boolean;
25 | random: boolean;
26 | repeat: boolean;
27 | theaterMode: boolean; // TODO: theater Mode
28 | }
29 |
30 | export interface IAppProvider extends IState {
31 | resetDatabase: () => void;
32 | removePlaylistItem: (id: string) => void;
33 | setIdVideo: (idVideo: string) => void;
34 | setInFullPlayer: (inFullPlayer: boolean) => void;
35 | setVideoClip: (id: string, videoClip: IVideoClipOptional) => void;
36 | addVideoClip: (videoClip: IVideoClip) => void;
37 | setAutoPlaylist: (autoPlay: boolean) => void;
38 | setRandom: (random: boolean) => void;
39 | setRepeat: (repeat: boolean) => void;
40 | setTheaterMode: (theaterMode: boolean) => void;
41 | }
42 |
43 | export default class AppProvider extends React.Component {
44 | public static Consumer = Context.Consumer;
45 |
46 | constructor(props) {
47 | super(props);
48 |
49 | let state: IState;
50 |
51 | const cacheState = getStorage(KEY_LOCALSTORAGE);
52 | cleanDeprecatedStorage(NAME_LOCALSTORAGE, VERSION_LOCALSTORAGE);
53 | if (cacheState) {
54 | state = {
55 | ...cacheState,
56 | idVideo: ""
57 | };
58 | } else {
59 | state = {
60 | autoPlaylist: true,
61 | idVideo: "",
62 | inFullPlayer: false,
63 | playlist: copy(arrayShuffle(db)),
64 | random: false,
65 | repeat: false,
66 | theaterMode: false
67 | };
68 | setStorage(KEY_LOCALSTORAGE, state);
69 | }
70 | this.state = state;
71 | }
72 |
73 | public resetDatabase = () => {
74 | this.setState(
75 | {
76 | playlist: copy(arrayShuffle(db))
77 | },
78 | this.updateStorage
79 | );
80 | };
81 |
82 | public setIdVideo = (idVideo: string) => {
83 | this.setState({
84 | idVideo
85 | });
86 | };
87 |
88 | public setInFullPlayer = (inFullPlayer: boolean) => {
89 | this.setState({
90 | inFullPlayer
91 | });
92 | };
93 |
94 | public removePlaylistItem = (id: string) => {
95 | const playlist = this.state.playlist.filter(item => item.id !== id);
96 | this.setState({ playlist }, this.updateStorage);
97 | };
98 |
99 | public addPlaylistItem = (videoClip: IVideoClip) => {
100 | const list = this.state.playlist.slice();
101 | list.push(videoClip);
102 |
103 | this.setState(
104 | {
105 | playlist: list
106 | },
107 | this.updateStorage
108 | );
109 | };
110 |
111 | public updatePlaylistItem = (id: string, videoClip: IVideoClipOptional) => {
112 | const playlist = this.state.playlist.slice();
113 | const index = findVideoClipIndexForId(playlist, id);
114 | playlist[index] = {
115 | ...playlist[index],
116 | ...videoClip
117 | };
118 |
119 | this.setState(
120 | {
121 | playlist
122 | },
123 | this.updateStorage
124 | );
125 | };
126 |
127 | public setRepeat = (repeat: boolean) => {
128 | this.setState({ repeat }, this.updateStorage);
129 | };
130 |
131 | public setRandom = (random: boolean) => {
132 | this.setState({ random }, this.updateStorage);
133 | };
134 |
135 | public setAutoPlaylist = (autoPlaylist: boolean) => {
136 | this.setState({ autoPlaylist }, this.updateStorage);
137 | };
138 |
139 | public setTheaterMode = (theaterMode: boolean) => {
140 | this.setState({ theaterMode }, this.updateStorage);
141 | };
142 |
143 | public render() {
144 | const value: IAppProvider = {
145 | ...this.state,
146 | addVideoClip: this.addPlaylistItem,
147 | removePlaylistItem: this.removePlaylistItem,
148 | resetDatabase: this.resetDatabase,
149 | setAutoPlaylist: this.setAutoPlaylist,
150 | setIdVideo: this.setIdVideo,
151 | setInFullPlayer: this.setInFullPlayer,
152 | setRandom: this.setRandom,
153 | setRepeat: this.setRepeat,
154 | setTheaterMode: this.setTheaterMode,
155 | setVideoClip: this.updatePlaylistItem
156 | };
157 |
158 | return ;
159 | }
160 |
161 | private updateStorage = () => {
162 | setStorage(KEY_LOCALSTORAGE, this.state);
163 | };
164 | }
165 |
--------------------------------------------------------------------------------
/src/components/Form/InputLabel.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import TagsInput from "react-tagsinput";
3 | import InputTime from "./InputTime";
4 | import {
5 | InputStyled,
6 | LabelStyled,
7 | InputLabelGeneralStyled,
8 | InputLabelTagStyled,
9 | ErrorStyled,
10 | VerifyStyled,
11 | LoadingStyled,
12 | LabelTagsStyled
13 | } from "./styles";
14 |
15 | interface IProps {
16 | value: any;
17 | error?: string;
18 | loading?: boolean;
19 | complete?: boolean;
20 | type?: string;
21 | label?: string;
22 | placeholder?: string;
23 | onChange: (value: string) => void;
24 | }
25 |
26 | const InputLabel: React.SFC = props => {
27 | const type = props.type || "text";
28 |
29 | const handleChange = event => {
30 | if (type === "tags") {
31 | const tags = event;
32 | let formatTags: any = [];
33 | tags.forEach((tag: string) => {
34 | if (tag.indexOf(",") > -1) {
35 | formatTags = formatTags.concat(
36 | tag.split(",").map(item => item.trim())
37 | );
38 | } else {
39 | formatTags.push(tag);
40 | }
41 | });
42 | props.onChange(formatTags);
43 | } else {
44 | props.onChange(event.target.value);
45 | }
46 | };
47 |
48 | return type === "tags" ? (
49 |
50 | {props.label}
51 |
52 |
53 | ) : (
54 |
55 | {props.label && {props.label}}
56 | {type === "time" ? (
57 |
62 | ) : (
63 |
68 | )}
69 | {props.error && {props.error}}
70 | {props.loading && (
71 |
72 |
73 |
74 | )}
75 | {props.complete && (
76 |
77 |
78 |
79 | )}
80 |
81 | );
82 | };
83 |
84 | export default InputLabel;
85 |
--------------------------------------------------------------------------------
/src/components/Form/InputTime.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import MaskedInput from "react-text-mask";
3 |
4 | export default props => (
5 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/components/Form/styles.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { StyledFunction } from "styled-components";
3 | import styled, { theme, device } from "../../theme";
4 | // tslint:disable:no-shadowed-variable
5 |
6 | interface IDivStyled {
7 | [x: string]: any;
8 | }
9 | const div: StyledFunction> =
10 | styled.div;
11 | const input: StyledFunction> =
12 | styled.input;
13 | const form: StyledFunction> =
14 | styled.form;
15 | const label: StyledFunction> =
16 | styled.label;
17 |
18 | export const InputLabelGeneralStyled = div`
19 | margin-bottom: 15px;
20 | input {
21 | width: 250px;
22 | padding: 0px 10px;
23 | border-color: #b9b9b9;
24 | border: 1px solid #d3d3d3;
25 | font-size: 13px;
26 | box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.05);
27 | line-height: 24px;
28 | outline: none;
29 | :hover,
30 | :focus {
31 | border-color: #b9b9b9;
32 | }
33 | }
34 | `;
35 |
36 | export const FormStyled = form`
37 | ${InputLabelGeneralStyled} {
38 | input {
39 | margin-left: 200px;
40 | @media ${device.laptop} {
41 | margin-left: 45%;
42 | width: 50%;
43 | }
44 | }
45 | }
46 | `;
47 |
48 | export const ErrorStyled = div`
49 | color: red;
50 | font-size: 11px;
51 | margin-left: 200px;
52 | margin-top: 5px;
53 | `;
54 |
55 | const BubbleStyled = div`
56 | vertical-align: middle;
57 | margin-left: 5px;
58 | display: inline-block;
59 | `;
60 |
61 | export const VerifyStyled = styled(BubbleStyled)`
62 | color: #49d649;
63 | `;
64 |
65 | export const LoadingStyled = styled(BubbleStyled)`
66 | color: #49d649;
67 | `;
68 |
69 | export const InputLabelTagStyled = div`
70 | margin-bottom: 15px;
71 | .react-tagsinput {
72 | width: 450px;
73 | margin-left: 200px;
74 | @media ${device.laptop} {
75 | margin-left: 0;
76 | width: 100%;
77 | }
78 | :hover {
79 | border-color: #b9b9b9;
80 | }
81 | }
82 | .react-tagsinput-tag {
83 | border: 1px solid ${() => theme.primaryColor};
84 | color: #ffffff;
85 | background-color: ${() => theme.primaryColor};
86 | }
87 | .react-tagsinput--focused {
88 | border-color: #b9b9b9;
89 | }
90 | .react-tagsinput-remove {
91 | color: #0d5082;
92 | }
93 | `;
94 |
95 | export const InputStyled = input`
96 | `;
97 |
98 | export const LabelStyled = label`
99 | float: left;
100 | margin-right: 10px;
101 | padding-top: 6px;
102 | font-size: 13px;
103 | line-height: 1.2;
104 | color: #555;
105 | position: absolute;
106 | @media ${device.laptop} {
107 | max-width: 37%;
108 | }
109 | `;
110 |
111 | export const LabelTagsStyled = styled(LabelStyled)`
112 | @media ${device.laptop} {
113 | display: block;
114 | position: relative;
115 | margin-bottom: 10px;
116 | }
117 | `
118 |
--------------------------------------------------------------------------------
/src/components/FullVideo/BigPlayButton.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Video from "./Video";
3 |
4 | import { IPropsChildrens as IPropsFromFullVideo } from "./FullVideo";
5 | import { BigPlayButtonStyled } from "./styles";
6 |
7 | interface IProps extends IPropsFromFullVideo {
8 | video: Video;
9 | }
10 |
11 | const BigPlayButton: React.SFC = props => {
12 | const handleClick = () => {
13 | const { video } = props;
14 | video.togglePlay();
15 | };
16 |
17 | return ;
18 | };
19 |
20 | export default BigPlayButton;
21 |
--------------------------------------------------------------------------------
/src/components/FullVideo/ComingNext.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Video from "./Video";
3 |
4 | import { IPropsChildrens as IPropsFromFullVideo } from "./FullVideo";
5 | import {
6 | ComingNextStyled,
7 | ComingNextImageStyled,
8 | ComingNextTopStyled,
9 | ComingNextHeaderStyled,
10 | ComingNextAutoplayStyled,
11 | ComingNextTitleStyled,
12 | ComingNextIconStyled,
13 | ComingNextBottomStyled,
14 | ComingNextButtonStyled
15 | } from "./styles";
16 | import { IVideoClip } from "../types";
17 | import { showSpinnerNextVideo } from "../../utils";
18 |
19 | const NEXT_DURATION = 3;
20 |
21 | class CommingNext extends React.Component<
22 | {
23 | nextVideoClip: IVideoClip;
24 | onChangeAutoPlay?: (autoPlay: boolean) => void;
25 | onClickPlaylistAction: (videoClip: IVideoClip) => void;
26 | },
27 | { count: number }
28 | > {
29 | public time;
30 | public state = {
31 | count: NEXT_DURATION
32 | };
33 |
34 | public componentDidMount() {
35 | this.time = setInterval(() => {
36 | const newCount = this.state.count - 1;
37 | this.setState({ count: newCount });
38 | if (newCount === 0) {
39 | this.launchNextVideoClip();
40 | }
41 | }, 1000);
42 | }
43 |
44 | public componentWillUnmount() {
45 | clearInterval(this.time);
46 | this.setState({ count: NEXT_DURATION });
47 | }
48 |
49 | public launchNextVideoClip = () => {
50 | if (this.props.onClickPlaylistAction) {
51 | this.props.onClickPlaylistAction(this.props.nextVideoClip);
52 | }
53 | clearInterval(this.time);
54 | };
55 |
56 | public handleStopTime = () => {
57 | const { onChangeAutoPlay } = this.props;
58 | if (onChangeAutoPlay) {
59 | onChangeAutoPlay(false);
60 | }
61 | };
62 |
63 | public render() {
64 | const { nextVideoClip } = this.props;
65 |
66 | return (
67 |
68 |
69 |
70 | Up Next
71 | {nextVideoClip.title}
72 |
73 |
74 |
75 |
76 |
77 |
92 |
93 |
94 |
95 | CANCEL
96 |
97 |
98 |
99 | );
100 | }
101 | }
102 |
103 | interface IProps extends IPropsFromFullVideo {
104 | video: Video;
105 | }
106 | const WrapperComingNext: React.SFC = props => {
107 | const {
108 | autoPlaylist,
109 | currentVideoClip: { endTime },
110 | nextVideoClip,
111 |
112 | onChangeAutoPlaylist,
113 | onClickPlaylistAction,
114 | provider: { currentTime, duration }
115 | } = props;
116 |
117 | if (
118 | showSpinnerNextVideo(currentTime, endTime, duration, autoPlaylist) &&
119 | onClickPlaylistAction &&
120 | nextVideoClip
121 | ) {
122 | return (
123 |
128 | );
129 | }
130 | return null;
131 | };
132 |
133 | export default WrapperComingNext;
134 |
--------------------------------------------------------------------------------
/src/components/FullVideo/ControlBar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import FragmentControl from "./FragmentControl";
3 | import EditButton from "./EditButton";
4 | import VolumeMenuButton from "./VolumeMenuButton";
5 | import { BackButton, NextButton } from "./PlaylistButtons";
6 | import FullscreenToggle from "./FullscreenToggle";
7 | import PlayToggle from "./PlayToggle";
8 | import ProgressControl from "./ProgressControl";
9 | import Video from "./Video";
10 |
11 | import { IPropsChildrens as IPropsFromFullVideo } from "./FullVideo";
12 | import {
13 | ControlBarStyled,
14 | ControlBarGradientStyled,
15 | ControlBarBottomStyled,
16 | ControlBarBottomSideStyled,
17 | ControlBarTopStyled,
18 | TextPlayerStyled
19 | } from "./styles";
20 | import { formatTime } from "../../utils";
21 |
22 | interface IProps extends IPropsFromFullVideo {
23 | video: Video;
24 | }
25 |
26 | const ControlBar: React.SFC = props => {
27 | const { currentTime, duration } = props.provider;
28 | const totalTimeFormat = formatTime(duration);
29 | const currentTimeFormat = formatTime(currentTime, duration);
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {currentTimeFormat} / {totalTimeFormat}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | export default ControlBar;
58 |
--------------------------------------------------------------------------------
/src/components/FullVideo/EditButton.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Video from "./Video";
3 |
4 | import { IPropsChildrens as IPropsFromFullVideo } from "./FullVideo";
5 | import { EditButtonStyled } from "./styles";
6 |
7 | interface IProps extends IPropsFromFullVideo {
8 | video: Video;
9 | }
10 |
11 | const EditButton: React.SFC = props => {
12 | const handleClick = () => {
13 | props.editing(true);
14 | };
15 | const handleCancel = () => {
16 | props.editing(false);
17 | };
18 |
19 | const handleSave = () => {
20 | const {
21 | editing,
22 | onChangeTimeFragment,
23 | provider: { editMin, editMax }
24 | } = props;
25 |
26 | if (onChangeTimeFragment) {
27 | onChangeTimeFragment(editMin, editMax);
28 | }
29 | editing(false);
30 | };
31 |
32 | const { editActive } = props.provider;
33 | return editActive ? (
34 |
35 |
40 | Cancel
41 |
42 |
47 | Save
48 |
49 |
50 | ) : (
51 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default EditButton;
62 |
--------------------------------------------------------------------------------
/src/components/FullVideo/FragmentControl.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Video from "./Video";
3 |
4 | import { IPropsChildrens as IPropsFromFullVideo } from "./FullVideo";
5 | import Slider, { createSliderWithTooltip } from "rc-slider";
6 | import { FragmentControlStyled } from "./styles";
7 | import {
8 | convertToTimeRange,
9 | convertToTime,
10 | formatTooltipRange
11 | } from "../../utils";
12 |
13 | interface IState {
14 | changed: boolean;
15 | value: { min: number; max: number };
16 | }
17 |
18 | interface IProps extends IPropsFromFullVideo {
19 | video: Video;
20 | }
21 |
22 | const SliderWithTooltip = createSliderWithTooltip(Slider.Range);
23 |
24 | export default class FragmentControl extends React.Component {
25 | public state = {
26 | changed: false,
27 | value: {
28 | max: 1,
29 | min: 0
30 | }
31 | };
32 |
33 | public componentWillMount() {
34 | const {
35 | provider: { duration },
36 | currentVideoClip: { startTime, endTime }
37 | } = this.props;
38 |
39 | this.updateValue(startTime, endTime, duration);
40 | }
41 |
42 | public componentWillReceiveProps(nextProps: IProps) {
43 | const {
44 | provider: { duration },
45 | currentVideoClip: { startTime, endTime }
46 | } = this.props;
47 |
48 | // Changed video
49 | if (
50 | nextProps.provider.duration &&
51 | (nextProps.currentVideoClip.startTime !== startTime ||
52 | nextProps.currentVideoClip.endTime !== endTime ||
53 | nextProps.provider.duration !== duration)
54 | ) {
55 | const provider = nextProps.provider;
56 | this.updateValue(
57 | nextProps.currentVideoClip.startTime,
58 | nextProps.currentVideoClip.endTime,
59 | provider.duration
60 | );
61 | }
62 | }
63 |
64 | public updateValue(startTime, endTime, duration) {
65 | const { editingValues } = this.props;
66 | const max = convertToTimeRange(endTime, duration);
67 | const min = convertToTimeRange(startTime, duration);
68 |
69 | this.setState({
70 | value: {
71 | max,
72 | min
73 | }
74 | });
75 |
76 | editingValues({
77 | editMax: endTime,
78 | editMin: startTime
79 | });
80 | }
81 |
82 | get range() {
83 | const {
84 | provider: { duration, editActive },
85 | currentVideoClip: { startTime, endTime }
86 | } = this.props;
87 |
88 | const { changed, value } = this.state;
89 |
90 | if (changed || editActive) {
91 | return value;
92 | }
93 |
94 | return {
95 | max: convertToTimeRange(endTime, duration),
96 | min: convertToTimeRange(startTime, duration)
97 | };
98 | }
99 |
100 | get value() {
101 | const { min, max } = this.range;
102 | return [min, max];
103 | }
104 |
105 | public handleChange = ([min, max]: [number, number]) => {
106 | this.setState({ changed: true, value: { min, max } });
107 | };
108 |
109 | public handleChangeComplete = ([min, max]: [number, number]) => {
110 | const {
111 | provider: { duration, editActive },
112 | currentVideoClip: { startTime, endTime },
113 | editingValues
114 | } = this.props;
115 |
116 | if (editActive) {
117 | editingValues({
118 | editMax: convertToTime(max, duration),
119 | editMin: convertToTime(min, duration)
120 | });
121 | } else {
122 | min = convertToTimeRange(startTime, duration);
123 | max = convertToTimeRange(endTime, duration);
124 | }
125 | this.setState({ changed: false, value: { min, max } });
126 | };
127 |
128 | public render() {
129 | const {
130 | provider: { duration, editActive }
131 | } = this.props;
132 | return (
133 |
134 |
142 |
143 | );
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/components/FullVideo/FullVideo.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import ControlBar from "./ControlBar";
3 | import LoadingSpinner from "./LoadingSpinner";
4 | import ComingNext from "./ComingNext";
5 | import Shortcut from "./Shortcut";
6 | import Video from "./Video";
7 | import { IVideoClip, IVideoClipOptional } from "../types";
8 | import FullVideoProvider, { IFullVideoProvider } from "./Provider";
9 |
10 | import { FullVideoStyled, TitleStyled } from "./styles";
11 | import BigPlayButton from "./BigPlayButton";
12 | import { isiOS } from "../../utils";
13 |
14 | export interface IExternalProps {
15 | autoPlay?: boolean;
16 | showControls?: boolean;
17 | autoPlaylist?: boolean;
18 | currentVideoClip?: IVideoClip;
19 | backVideoClip?: IVideoClip;
20 | nextVideoClip?: IVideoClip;
21 | onChangeTimeFragment?: (startTime: number, endTime: number) => void;
22 | onChangeAutoPlaylist?: (autoPlaylist: boolean) => void;
23 | onClickPlaylistAction?: (videoClip: IVideoClip) => void;
24 | children?: any;
25 | }
26 |
27 | export interface IProps extends IExternalProps, IFullVideoProvider {
28 | currentVideoClip: IVideoClip;
29 | }
30 |
31 | interface IDefaultProps {
32 | currentVideoClip: IVideoClipOptional;
33 | showControls: boolean;
34 | }
35 |
36 | export interface IPropsChildrens extends IProps {
37 | video: any;
38 | }
39 |
40 | class FullVideo extends React.Component {
41 | public static defaultProps: IDefaultProps = {
42 | currentVideoClip: {
43 | id: "",
44 | src: "",
45 | startTime: 0
46 | },
47 | showControls: true
48 | };
49 |
50 | public video: Video;
51 | public controlsHideTimer: any;
52 |
53 | public startControlsTimer = () => {
54 | const { userActivate } = this.props;
55 | userActivate(true);
56 | clearTimeout(this.controlsHideTimer);
57 | this.controlsHideTimer = setTimeout(() => {
58 | userActivate(false);
59 | }, 1500);
60 | };
61 |
62 | public handleFocus = () => {
63 | this.props.playerActivate(true);
64 | };
65 |
66 | public handleBlur = () => {
67 | this.props.playerActivate(false);
68 | };
69 |
70 | public render() {
71 | const provider = this.props.provider;
72 | const { fullscreen, userActivity, paused } = provider;
73 | const { src, id, title } = this.props.currentVideoClip;
74 |
75 | const existsID = id !== "";
76 |
77 | const showControls = this.props.showControls && existsID;
78 |
79 | const propsWithoutChildren: IProps = {
80 | ...this.props,
81 | children: null
82 | };
83 |
84 | const propsActionChildren: IPropsChildrens = {
85 | ...propsWithoutChildren,
86 | video: this.video ? this.video : null
87 | };
88 |
89 | return (
90 |
100 | {showControls && {title}}
101 |
109 | {showControls && }
110 |
111 | {showControls && }
112 | {showControls && }
113 |
114 |
115 | );
116 | }
117 | }
118 |
119 | export default (props: IExternalProps | any) => (
120 |
121 |
122 | {(value: any) => }
123 |
124 |
125 | );
126 |
--------------------------------------------------------------------------------
/src/components/FullVideo/FullscreenToggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Video from "./Video";
3 |
4 | import { IPropsChildrens as IPropsFromFullVideo } from "./FullVideo";
5 | import { PlayerButtonStyled } from "./styles";
6 |
7 | interface IProps extends IPropsFromFullVideo {
8 | video: Video;
9 | }
10 |
11 | const PlayToggle: React.SFC = props => {
12 | const handleClick = () => {
13 | const { toggleFullscreen, video } = props;
14 | toggleFullscreen(video);
15 | };
16 |
17 | const { fullscreen } = props.provider;
18 |
19 | return (
20 |
21 | {fullscreen ? (
22 |
23 | ) : (
24 |
25 | )}
26 |
27 | );
28 | };
29 |
30 | export default PlayToggle;
31 |
--------------------------------------------------------------------------------
/src/components/FullVideo/LoadingSpinner.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Video from "./Video";
3 | import {
4 | LoadingSpinnerStyled,
5 | LoadingSpinnerContainer,
6 | LoadingSpinnerRotator,
7 | LoadingSpinnerLeft,
8 | LoadingSpinnerCircle,
9 | LoadingSpinnerRight
10 | } from "./styles";
11 | import { IPropsChildrens as IPropsFromFullVideo } from "./FullVideo";
12 |
13 | interface IProps extends IPropsFromFullVideo {
14 | video: Video;
15 | }
16 |
17 | const LoadingSpinner: React.SFC = ({ provider: { waiting } }) => (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 |
32 | export default LoadingSpinner;
33 |
--------------------------------------------------------------------------------
/src/components/FullVideo/PlayToggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Video from "./Video";
3 |
4 | import { IPropsChildrens as IPropsFromFullVideo } from "./FullVideo";
5 | import { PlayerButtonStyled } from "./styles";
6 |
7 | interface IProps extends IPropsFromFullVideo {
8 | video: Video;
9 | }
10 |
11 | const PlayToggle: React.SFC = props => {
12 | const handleClick = () => {
13 | const { video } = props;
14 | video.togglePlay();
15 | };
16 |
17 | const { paused } = props.provider;
18 |
19 | return (
20 |
21 | {paused ? : }
22 |
23 | );
24 | };
25 |
26 | export default PlayToggle;
27 |
--------------------------------------------------------------------------------
/src/components/FullVideo/PlaylistButtons.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Video from "./Video";
3 |
4 | import { IPropsChildrens as IPropsFromFullVideo } from "./FullVideo";
5 | import { PlayerButtonStyled } from "./styles";
6 |
7 | interface IPropsCompound extends IProps {
8 | value: "next" | "back";
9 | children: any;
10 | }
11 |
12 | const CompoundPlaylistButton: React.SFC = props => {
13 | const { value, onClickPlaylistAction, nextVideoClip, backVideoClip } = props;
14 | const sendVideoClip = value === "next" ? nextVideoClip : backVideoClip;
15 | const visible = !!sendVideoClip;
16 |
17 | if (visible && onClickPlaylistAction) {
18 | return (
19 | visible && (
20 |
23 | {props.children}
24 |
25 | )
26 | );
27 | }
28 | return null;
29 | };
30 |
31 | interface IProps extends IPropsFromFullVideo {
32 | video: Video;
33 | }
34 |
35 | export const NextButton: React.SFC = props => (
36 |
37 |
38 |
39 | );
40 | export const BackButton: React.SFC = props => (
41 |
42 |
43 |
44 | );
45 |
--------------------------------------------------------------------------------
/src/components/FullVideo/ProgressControl.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Video from "./Video";
3 |
4 | import { IPropsChildrens as IPropsFromFullVideo } from "./FullVideo";
5 | import Slider, { createSliderWithTooltip } from "rc-slider";
6 | import { ProgressControlStyled } from "./styles";
7 | import {
8 | convertToTimeRange,
9 | convertToTime,
10 | formatTooltipRange
11 | } from "../../utils";
12 |
13 | const SliderWithTooltip = createSliderWithTooltip(Slider);
14 |
15 | interface IState {
16 | changed: boolean;
17 | value: number;
18 | }
19 |
20 | interface IProps extends IPropsFromFullVideo {
21 | video: Video;
22 | }
23 |
24 | export default class PlayToggle extends React.Component {
25 | public state = {
26 | changed: false,
27 | value: 1000
28 | };
29 |
30 | public slideTime() {
31 | const { currentTime, duration } = this.props.provider;
32 | if (this.state.changed) {
33 | return this.state.value;
34 | }
35 | if (isNaN(duration) || duration === 0) {
36 | return 0;
37 | }
38 | return convertToTimeRange(currentTime, duration);
39 | }
40 |
41 | public handleChange = (value: number) => {
42 | this.setState({ changed: true, value });
43 | };
44 |
45 | public handleChangeComplete = (value: number) => {
46 | const {
47 | provider: { duration },
48 | currentVideoClip: { startTime, endTime }
49 | } = this.props;
50 | const selectedTime = convertToTime(value, duration);
51 | let seekTime;
52 | if (selectedTime >= startTime && selectedTime <= endTime) {
53 | seekTime = selectedTime;
54 | } else if (selectedTime < startTime) {
55 | seekTime = startTime;
56 | } else if (selectedTime > endTime) {
57 | seekTime = endTime;
58 | }
59 | this.props.video.seek(seekTime);
60 | this.setState({ changed: false, value });
61 | };
62 |
63 | public render() {
64 | const {
65 | provider: { duration, editActive },
66 | currentVideoClip: { startTime, endTime }
67 | } = this.props;
68 |
69 | return (
70 |
71 |
76 |
81 |
89 |
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/FullVideo/Provider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom";
3 | import Video from "./Video";
4 | import { setStorage, getStorage } from "../../utils";
5 |
6 | const Context = React.createContext({});
7 |
8 | const KEY_LOCALSTORAGE = "dbVolumeReact";
9 |
10 | interface IState {
11 | autoPaused: boolean;
12 | buffered: any;
13 | currentSrc: any;
14 | currentTime: number;
15 | duration: number;
16 | editActive: boolean;
17 | editMax: number;
18 | editMin: number;
19 | ended: boolean;
20 | fullscreen: boolean;
21 | hasStarted: boolean;
22 | isActive: boolean;
23 | muted: boolean;
24 | paused: boolean;
25 | readyState: number;
26 | seeking: boolean;
27 | seekingTime: number;
28 | userActivity: boolean;
29 | video: any;
30 | videoHeight: number;
31 | videoWidth: number;
32 | volume: number;
33 | waiting: boolean;
34 | }
35 |
36 | export interface IFullVideoProvider {
37 | provider: IState;
38 | editing: (ob: boolean) => any;
39 | changeVolume: (ob: any) => any;
40 | editingValues: (ob: any) => any;
41 | userActivate: (ob: boolean) => any;
42 | playerActivate: (ob: boolean) => any;
43 | seekingTime: (ob: number) => any;
44 | endSeeking: () => any;
45 | toggleFullscreen: (video: Video) => any;
46 | onLoadStart: (ob: any) => any;
47 | canPlay: (ob: any) => any;
48 | waiting: (ob: any) => any;
49 | playing: (ob: any) => any;
50 | play: (ob: any, autoPlay?: boolean) => any;
51 | pause: (ob: any) => any;
52 | end: (ob: any) => any;
53 | seeked: (ob: any) => any;
54 | seeking: (ob: any) => any;
55 | reload: (ob: any) => any;
56 | }
57 |
58 | export default class FullVideoProvider extends React.Component {
59 | public static Consumer = Context.Consumer;
60 |
61 | constructor(props: any) {
62 | super(props);
63 |
64 | let volume: number;
65 | const volumeCache = getStorage(KEY_LOCALSTORAGE);
66 | if (volumeCache !== undefined) {
67 | volume = volumeCache;
68 | } else {
69 | volume = 1;
70 | setStorage(KEY_LOCALSTORAGE, volume);
71 | }
72 | this.state = {
73 | autoPaused: false,
74 | buffered: null,
75 | currentSrc: null,
76 | currentTime: 0,
77 | duration: 0,
78 | editActive: false,
79 | editMax: 0,
80 | editMin: 0,
81 | ended: false,
82 | fullscreen: false,
83 | hasStarted: false,
84 | isActive: false,
85 | muted: false,
86 | paused: true,
87 | readyState: 0,
88 | seeking: false,
89 | seekingTime: 0,
90 | userActivity: false,
91 | video: null,
92 | videoHeight: 0,
93 | videoWidth: 0,
94 | volume,
95 | waiting: false
96 | };
97 | }
98 |
99 | public toggleFullscreen = (video: Video) => {
100 | const videoElement = ReactDOM.findDOMNode(video);
101 | const fullscreen = this.state.fullscreen;
102 | if (fullscreen) {
103 | if (document.exitFullscreen) {
104 | document.exitFullscreen();
105 | } else if (document.webkitExitFullscreen) {
106 | document.webkitExitFullscreen();
107 | }
108 | this.setState({ fullscreen: false });
109 | } else {
110 | if (videoElement && videoElement.parentElement) {
111 | const fullvideoElement = videoElement.parentElement;
112 | if (fullvideoElement.requestFullscreen) {
113 | fullvideoElement.requestFullscreen();
114 | } else if (fullvideoElement.webkitRequestFullscreen) {
115 | fullvideoElement.webkitRequestFullscreen();
116 | }
117 | this.setState({ fullscreen: true });
118 | }
119 | }
120 | };
121 |
122 | public editing = (editActive: boolean) => {
123 | this.setState({ editActive });
124 | };
125 |
126 | public editingValues = (editActive: { editMin: number; editMax: number }) => {
127 | this.setState(editActive);
128 | };
129 |
130 | public userActivate = (userActivity: boolean) => {
131 | this.setState({ userActivity });
132 | };
133 |
134 | public playerActivate = (isActive: boolean) => {
135 | this.setState({ isActive });
136 | };
137 |
138 | public seekingTime = (time: number) => {
139 | this.setState({ seekingTime: time });
140 | };
141 |
142 | public endSeeking = () => {
143 | this.setState({ seekingTime: 0 });
144 | };
145 |
146 | public onLoadStart = (videoProps: any) => {
147 | this.setState({
148 | ...videoProps,
149 | editActive: false,
150 | ended: false,
151 | hasStarted: false,
152 | waiting: true
153 | });
154 | };
155 |
156 | public canPlay = (videoProps: any) => {
157 | this.setState({ ...videoProps, waiting: false });
158 | };
159 |
160 | public waiting = (videoProps: any) => {
161 | this.setState({ ...videoProps, waiting: true });
162 | };
163 |
164 | public playing = (videoProps: any) => {
165 | this.setState({ ...videoProps, waiting: false });
166 | };
167 |
168 | public play = (videoProps: any) => {
169 | this.setState({
170 | ...videoProps,
171 | autoPaused: false,
172 | ended: false,
173 | hasStarted: true,
174 | paused: false,
175 | waiting: false
176 | });
177 | };
178 |
179 | public pause = (videoProps: any) => {
180 | this.setState({ ...videoProps, paused: true });
181 | };
182 |
183 | public end = (videoProps: any) => {
184 | this.setState({ ...videoProps, ended: true });
185 | };
186 |
187 | public seeking = (videoProps: any) => {
188 | this.setState({ ...videoProps, seeking: true });
189 | };
190 |
191 | public seeked = (videoProps: any) => {
192 | this.setState({ ...videoProps, seeking: false });
193 | };
194 |
195 | public changeVolume = (videoProps: any) => {
196 | this.setState(
197 | {
198 | ...this.state,
199 | ...videoProps
200 | },
201 | this.updateStorage
202 | );
203 | };
204 |
205 | public reload = (videoProps: any) => {
206 | const newState = {
207 | ...this.state,
208 | ...videoProps
209 | };
210 | if (videoProps.paused === false) {
211 | newState.hasStarted = true;
212 | newState.waiting = false;
213 | }
214 | this.setState(newState);
215 | };
216 |
217 | public render() {
218 | const value: IFullVideoProvider = {
219 | canPlay: this.canPlay,
220 | changeVolume: this.changeVolume,
221 | editing: this.editing,
222 | editingValues: this.editingValues,
223 | end: this.end,
224 | endSeeking: this.endSeeking,
225 | onLoadStart: this.onLoadStart,
226 | pause: this.pause,
227 | play: this.play,
228 | playerActivate: this.playerActivate,
229 | playing: this.playing,
230 | provider: this.state,
231 | reload: this.reload,
232 | seeked: this.seeked,
233 | seeking: this.seeking,
234 | seekingTime: this.seekingTime,
235 | toggleFullscreen: this.toggleFullscreen,
236 | userActivate: this.userActivate,
237 | waiting: this.waiting
238 | };
239 | return ;
240 | }
241 |
242 | private updateStorage = () => {
243 | setStorage(KEY_LOCALSTORAGE, this.state.volume);
244 | };
245 | }
246 |
--------------------------------------------------------------------------------
/src/components/FullVideo/Shortcut.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Video from "./Video";
3 |
4 | import { IPropsChildrens as IPropsFromPlayer } from "./FullVideo";
5 |
6 | interface IProps extends IPropsFromPlayer {
7 | video: Video;
8 | }
9 |
10 | interface IHandleKeyCode {
11 | handle: any;
12 | keyCode: number;
13 | }
14 |
15 | export default class Shortcut extends React.Component {
16 | public handleKeycodes: IHandleKeyCode[];
17 |
18 | constructor(props) {
19 | super(props);
20 | this.handleKeycodes = [
21 | {
22 | handle: this.togglePlay,
23 | keyCode: 32 // spacebar
24 | },
25 | {
26 | handle: this.leftArrow,
27 | keyCode: 37 // Left arrow
28 | },
29 | {
30 | handle: this.rightArrow,
31 | keyCode: 39 // Right arrow
32 | },
33 | {
34 | handle: this.upArrow,
35 | keyCode: 38 // Up arrow
36 | },
37 | {
38 | handle: this.downArrow,
39 | keyCode: 40 // Down arrow
40 | }
41 | ];
42 | }
43 |
44 | public componentDidMount() {
45 | document.addEventListener("keydown", this.handleKeyPress);
46 | }
47 |
48 | public componentWillUnmount() {
49 | document.removeEventListener("keydown", this.handleKeyPress);
50 | }
51 |
52 | public handleKeyPress = e => {
53 | const { isActive } = this.props.provider
54 |
55 | if (!isActive) {
56 | return
57 | }
58 | const keyCode = e.keyCode || e.which;
59 |
60 | const shortcut = this.handleKeycodes.filter(handle => {
61 | if (!handle.keyCode || handle.keyCode - keyCode !== 0) {
62 | return false;
63 | }
64 | return true;
65 | })[0];
66 |
67 | if (shortcut) {
68 | shortcut.handle();
69 | e.preventDefault();
70 | }
71 | };
72 |
73 | public togglePlay = () => {
74 | this.props.video.togglePlay();
75 | };
76 |
77 | public leftArrow = () => {
78 | this.props.video.replay(5);
79 | };
80 |
81 | public rightArrow = () => {
82 | this.props.video.forward(5);
83 | };
84 |
85 | public upArrow = () => {
86 | const { video } = this.props;
87 | let volume = video.volume + 0.05;
88 | if (volume > 1) {
89 | volume = 1;
90 | }
91 | video.volume = volume;
92 | this.checkMuted();
93 | };
94 |
95 | public downArrow = () => {
96 | const { video } = this.props;
97 | let volume = video.volume - 0.05;
98 | if (volume < 0) {
99 | volume = 0;
100 | }
101 | video.volume = volume;
102 | this.checkMuted();
103 | };
104 |
105 | public checkMuted() {
106 | const { video } = this.props;
107 | if (video.volume === 0) {
108 | video.muted = true;
109 | } else {
110 | video.muted = false;
111 | }
112 | }
113 |
114 | public render() {
115 | return null;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/FullVideo/Video.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { mediaProperties } from "./consts";
3 | import { IProps as IPropsFromPlayer } from "./FullVideo";
4 | import { showSpinnerNextVideo } from "../../utils";
5 | import { VideoStyled, VideoWrapperStyled } from "./styles";
6 |
7 | export interface IProps extends IPropsFromPlayer {
8 | children?: any;
9 | }
10 |
11 | export default class Video extends React.Component {
12 | public video: HTMLVideoElement;
13 |
14 | public componentWillReceiveProps(nextProps: IProps) {
15 | // Bug when try to repeat the same video with different id
16 | const {
17 | currentVideoClip: { id, src }
18 | } = this.props;
19 | if (
20 | nextProps.currentVideoClip.id !== id &&
21 | nextProps.currentVideoClip.src === src
22 | ) {
23 | this.video.currentTime = nextProps.currentVideoClip.startTime;
24 | this.play();
25 | }
26 | }
27 |
28 | public componentDidMount() {
29 | this.volume = this.props.provider.volume;
30 | }
31 |
32 | public getVideo() {
33 | if (!this.video) {
34 | return null;
35 | }
36 |
37 | return mediaProperties.reduce((properties, key) => {
38 | properties[key] = this.video[key];
39 | return properties;
40 | }, {});
41 | }
42 |
43 | get muted() {
44 | return this.video.muted;
45 | }
46 |
47 | set muted(val) {
48 | this.video.muted = val;
49 | }
50 |
51 | get volume() {
52 | return this.video.volume;
53 | }
54 |
55 | set volume(val) {
56 | if (val > 1) {
57 | val = 1;
58 | }
59 | if (val < 0) {
60 | val = 0;
61 | }
62 | this.video.volume = val;
63 | }
64 |
65 | public play = () => {
66 | const {
67 | currentVideoClip: { endTime, startTime },
68 | provider: { duration }
69 | } = this.props;
70 | const fixEndTime = endTime > duration ? duration : endTime;
71 | if (
72 | this.video.currentTime >= fixEndTime ||
73 | this.video.currentTime <= startTime
74 | ) {
75 | this.video.currentTime = startTime;
76 | }
77 | this.video.play();
78 | };
79 |
80 | public pause = () => {
81 | this.video.pause();
82 | };
83 |
84 | public togglePlay = () => {
85 | if (this.video.paused) {
86 | this.play();
87 | } else {
88 | this.pause();
89 | }
90 | };
91 |
92 | public seek = (time: number) => {
93 | try {
94 | this.video.currentTime = time;
95 | } catch (e) {
96 | //
97 | }
98 | };
99 |
100 | public forward = (seconds: number) => {
101 | const {
102 | currentVideoClip: { endTime, startTime }
103 | } = this.props;
104 | let finalCurrentTime = this.video.currentTime + seconds;
105 | if (finalCurrentTime >= endTime) {
106 | finalCurrentTime = startTime;
107 | } else if (finalCurrentTime <= startTime) {
108 | finalCurrentTime = endTime;
109 | }
110 | this.seek(finalCurrentTime);
111 | };
112 |
113 | public replay = (seconds: number) => {
114 | this.forward(-seconds);
115 | };
116 |
117 | public handleLoadStart = () => {
118 | const {
119 | onLoadStart,
120 | currentVideoClip: { startTime }
121 | } = this.props;
122 | onLoadStart(this.getVideo());
123 |
124 | this.video.currentTime = startTime || 0;
125 | };
126 |
127 | public handleCanPlay = () => {
128 | this.props.canPlay(this.getVideo());
129 | };
130 |
131 | public handlePlaying = () => {
132 | this.props.playing(this.getVideo());
133 | };
134 |
135 | public handlePlay = () => {
136 | this.props.play(this.getVideo());
137 | };
138 |
139 | public handlePause = () => {
140 | this.props.pause(this.getVideo());
141 | };
142 |
143 | public handleDurationChange = () => {
144 | this.props.reload(this.getVideo());
145 | };
146 |
147 | public handleProgress = () => {
148 | if (this.video) {
149 | this.props.reload(this.getVideo());
150 | }
151 | };
152 |
153 | public handleEnded = () => {
154 | const { end } = this.props;
155 | end(this.getVideo());
156 | };
157 |
158 | public handleWaiting = () => {
159 | this.props.waiting(this.getVideo());
160 | };
161 |
162 | public handleSeeking = () => {
163 | this.props.seeking(this.getVideo());
164 | };
165 |
166 | public handleSeeked = () => {
167 | this.props.seeked(this.getVideo());
168 | };
169 |
170 | public handleEmptied = () => {
171 | this.props.reload(this.getVideo());
172 | };
173 |
174 | public handleLoadedMetaData = () => {
175 | const {
176 | currentVideoClip: { startTime },
177 | reload
178 | } = this.props;
179 | if (startTime && startTime > 0) {
180 | this.video.currentTime = startTime;
181 | }
182 | reload(this.getVideo());
183 | };
184 |
185 | public handleLoadedData = () => {
186 | this.props.reload(this.getVideo());
187 | };
188 |
189 | public handleTimeUpdate = () => {
190 | const {
191 | reload,
192 | currentVideoClip: { endTime }
193 | } = this.props;
194 | if (this.video.currentTime >= endTime) {
195 | this.pause();
196 | }
197 | reload(this.getVideo());
198 | };
199 |
200 | public handleVolumeChange = () => {
201 | this.props.changeVolume(this.getVideo());
202 | };
203 |
204 | public render() {
205 | const {
206 | autoPlay = false,
207 | currentVideoClip: { src, endTime },
208 | autoPlaylist,
209 | provider: { currentTime, duration, fullscreen }
210 | } = this.props;
211 |
212 | return (
213 |
219 | {
222 | this.video = c;
223 | }}
224 | autoPlay={autoPlay}
225 | src={src}
226 | playsinline={true}
227 | preload="auto"
228 | onLoadStart={this.handleLoadStart}
229 | onWaiting={this.handleWaiting}
230 | onCanPlay={this.handleCanPlay}
231 | onPlaying={this.handlePlaying}
232 | onEnded={this.handleEnded}
233 | onSeeking={this.handleSeeking}
234 | onSeeked={this.handleSeeked}
235 | onPlay={this.handlePlay}
236 | onPause={this.handlePause}
237 | onProgress={this.handleProgress}
238 | onDurationChange={this.handleDurationChange}
239 | onEmptied={this.handleEmptied}
240 | onLoadedMetadata={this.handleLoadedMetaData}
241 | onLoadedData={this.handleLoadedData}
242 | onTimeUpdate={this.handleTimeUpdate}
243 | onVolumeChange={this.handleVolumeChange}
244 | >
245 | {this.props.children}
246 |
247 |
248 | );
249 | }
250 | }
251 |
--------------------------------------------------------------------------------
/src/components/FullVideo/VolumeMenuButton.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Slider, { createSliderWithTooltip } from "rc-slider";
3 | import Video from "./Video";
4 |
5 | import { IPropsChildrens as IPropsFromPlayer } from "./FullVideo";
6 |
7 | const SliderWithTooltip = createSliderWithTooltip(Slider);
8 | import {
9 | VolumeIcon,
10 | VolumenMenuButtonStyled,
11 | VolumenMenuWrapperStyled
12 | } from "./styles";
13 |
14 | interface IState {
15 | value: number;
16 | }
17 |
18 | interface IProps extends IPropsFromPlayer {
19 | video: Video;
20 | }
21 |
22 | function tipFormatter(value) {
23 | return value + "%";
24 | }
25 |
26 | export default class VolumenMenuButton extends React.Component {
27 | public state = {
28 | value: 100
29 | };
30 |
31 | get value() {
32 | const { value } = this.state;
33 | if (!this.props.video) {
34 | return value;
35 | }
36 | return this.props.video.volume * 100;
37 | }
38 |
39 | public handleChange = (value: any) => {
40 | this.setState({ value });
41 | this.props.video.volume = value ? value / 100 : value;
42 | this.checkMuted();
43 | };
44 |
45 | public checkMuted() {
46 | const { video } = this.props;
47 | if (video.volume === 0) {
48 | video.muted = true;
49 | } else {
50 | video.muted = false;
51 | }
52 | }
53 |
54 | public classVolumeIcon() {
55 | const volume = this.value;
56 | let variant;
57 | if (volume > 70) {
58 | variant = "volume-up";
59 | } else if (volume > 20) {
60 | variant = "volume-down";
61 | } else {
62 | variant = "volume-off";
63 | }
64 | return "fas fa-" + variant;
65 | }
66 |
67 | public handleClickVolume = () => {
68 | if (this.state.value > 1) {
69 | this.handleChange(0);
70 | } else {
71 | this.handleChange(100);
72 | }
73 | };
74 |
75 | public render() {
76 | return (
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
91 |
92 |
93 | );
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/FullVideo/consts.ts:
--------------------------------------------------------------------------------
1 | export const mediaProperties = [
2 | "error",
3 | "src",
4 | "srcObject",
5 | "currentSrc",
6 | "crossOrigin",
7 | "networkState",
8 | "preload",
9 | "buffered",
10 | "readyState",
11 | "seeking",
12 | "currentTime",
13 | "duration",
14 | "paused",
15 | "defaultPlaybackRate",
16 | "playbackRate",
17 | "played",
18 | "seekable",
19 | "ended",
20 | "autoplay",
21 | "loop",
22 | "mediaGroup",
23 | "controller",
24 | "controls",
25 | "volume",
26 | "muted",
27 | "defaultMuted",
28 | "audioTracks",
29 | "videoTracks",
30 | "textTracks",
31 | "width",
32 | "height",
33 | "videoWidth",
34 | "videoHeight",
35 | "poster"
36 | ];
37 |
--------------------------------------------------------------------------------
/src/components/FullVideo/styles.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { StyledFunction } from "styled-components";
3 | import styled, { theme, device } from "../../theme";
4 | import { LayoutFullVideo } from "../styles";
5 |
6 | interface IDivStyled {
7 | [x: string]: any;
8 | }
9 | const div: StyledFunction> =
10 | styled.div;
11 | const button: StyledFunction> =
12 | styled.button;
13 |
14 | const layoutFullVideo: StyledFunction<
15 | IDivStyled & React.HTMLProps
16 | > = styled(LayoutFullVideo);
17 |
18 | export const VideoWrapperStyled = layoutFullVideo`
19 | padding-top: ${({ fullscreen }) => (fullscreen ? "0" : "56.25%")};
20 | height: ${({ fullscreen }) => (fullscreen ? "100%" : "auto")};
21 | opacity: ${({ show }) => (show ? "1" : "0")};
22 | `;
23 |
24 | export const VideoStyled = styled.video`
25 | position: absolute;
26 | top: 0;
27 | left: 0;
28 | width: 100%;
29 | height: 100%;
30 | `;
31 |
32 | export const BigPlayButtonStyled = button`
33 | position: absolute;
34 | top: 0;
35 | left: 0;
36 | right: 0;
37 | bottom: 0;
38 | width: 100%;
39 | cursor: auto;
40 | `;
41 |
42 | export const ControlBarStyled = div`
43 | position: absolute;
44 | width: 100%;
45 | bottom: 0;
46 | color: white;
47 | opacity: 1;
48 | transition-property: opacity;
49 | transition-duration: ${() => theme.velocityTransition};
50 | `;
51 |
52 | export const ControlBarGradientStyled = div`
53 | width: 100%;
54 | position: absolute;
55 | background-repeat: repeat-x;
56 | background: -moz-linear-gradient(to bottom, rgba(0,0,0,0) 23%,rgba(0, 0, 0, 0.9) 96%);
57 | background: -webkit-linear-gradient(to bottom, rgba(0,0,0,0) 23%,rgba(0, 0, 0, 0.9) 96%);
58 | background: linear-gradient(to bottom, rgba(0,0,0,0) 23%,rgba(0, 0, 0, 0.9) 96%);
59 | height: 49px;
60 | padding-top: 49px;
61 | bottom: 0;
62 | background-position: bottom;
63 | `;
64 |
65 | export const ControlBarTopStyled: React.SFC = props => (
66 |
67 |
68 |
69 | );
70 |
71 | const ControlBarTopContainerStyled = div`
72 | position: relative;
73 | `;
74 |
75 | const ControlBarTopWrapperStyled = div`
76 | position: relative;
77 | padding: 8px;
78 | margin-bottom: 3px;
79 | text-shadow: 0 0 2px rgba(0,0,0,.5);
80 | z-index: 2;
81 | `;
82 |
83 | export const ControlBarBottomStyled = div`
84 | display: flex;
85 | padding: 8px;
86 | justify-content: space-between;
87 | text-shadow: 0 0 2px rgba(0,0,0,.5);
88 | z-index: 2;
89 | `;
90 |
91 | export const ControlBarBottomSideStyled = div`
92 | display: flex;
93 | z-index: 1;
94 | `;
95 |
96 | export const TitleStyled = div`
97 | position: absolute;
98 | text-shadow: 0 0 2px rgba(0,0,0,.5);
99 | color: white;
100 | z-index: 1;
101 | padding-top: 10px;
102 | padding-left: 20px;
103 | padding-right: 20px;
104 | font-size: 164%;
105 | opacity: 1;
106 | transition-property: opacity;
107 | transition-duration: ${() => theme.velocityTransition};
108 | `;
109 |
110 | export const LoadingSpinnerStyled = div`
111 | display: ${props => (props.waiting ? "block" : "none")};
112 | position: absolute;
113 | left: 50%;
114 | top: 50%;
115 | width: 64px;
116 | margin-left: -32px;
117 | z-index: 18;
118 | `;
119 |
120 | export const LoadingSpinnerContainer = div`
121 | position: absolute;
122 | width: 100%;
123 | padding-bottom: 100%;
124 | top: 50%;
125 | left: 50%;
126 | margin-top: -50%;
127 | margin-left: -50%;
128 | animation: spinner-linspin 1568.23529647ms linear infinite;
129 | -webkit-animation: spinner-linspin 1568.23529647ms linear infinite;
130 | `;
131 |
132 | export const LoadingSpinnerRotator = div`
133 | position: absolute;
134 | width: 100%;
135 | height: 100%;
136 | -webkit-animation: spinner-easespin 5332ms cubic-bezier(0.4,0.0,0.2,1) infinite both;
137 | animation: spinner-easespin 5332ms cubic-bezier(0.4,0.0,0.2,1) infinite both;
138 | `;
139 |
140 | const LoadingSpinnerSide = div`
141 | position: absolute;
142 | top: 0;
143 | left: 0;
144 | bottom: 0;
145 | overflow: hidden;
146 | `;
147 |
148 | export const LoadingSpinnerCircle = div`
149 | box-sizing: border-box;
150 | position: absolute;
151 | width: 200%;
152 | height: 100%;
153 | border-style: solid;
154 | border-color: #ddd #ddd transparent;
155 | border-radius: 50%;
156 | border-width: 6px;
157 | `;
158 |
159 | export const LoadingSpinnerLeft = styled(LoadingSpinnerSide)`
160 | right: 49%;
161 |
162 | ${LoadingSpinnerCircle} {
163 | left: 0;
164 | right: -100%;
165 | border-right-color: transparent;
166 | -webkit-animation: spinner-left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1)
167 | infinite both;
168 | animation: spinner-left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite
169 | both;
170 | }
171 | `;
172 |
173 | export const LoadingSpinnerRight = styled(LoadingSpinnerSide)`
174 | left: 49%;
175 |
176 | ${LoadingSpinnerCircle} {
177 | left: -100%;
178 | right: 0;
179 | border-left-color: transparent;
180 | -webkit-animation: right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite
181 | both;
182 | animation: right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;
183 | }
184 | `;
185 |
186 | export const ComingNextStyled = div`
187 | position: absolute;
188 | opacity: 1;
189 | overflow: hidden;
190 | width: 100%;
191 | height: 100%;
192 | top: 0;
193 | color: white;
194 | @media ${device.tablet} {
195 | z-index: 5;
196 | background: black;
197 | }
198 | `;
199 |
200 | export const ComingNextImageStyled = div`
201 | background-repeat: no-repeat;
202 | width: 100%;
203 | height: 100%;
204 | position: absolute;
205 | background-size: cover;
206 | background-image: url(${({ image }) => image});
207 | background-position: center;
208 | opacity: .4;
209 | `;
210 |
211 | export const ComingNextTopStyled = div`
212 | width: 100%;
213 | position: absolute;
214 | margin-left: auto;
215 | margin-right: auto;
216 | bottom: 50%;
217 | margin-bottom: 48px;
218 |
219 | @media ${device.tablet} {
220 | font-size: 8px;
221 | margin-bottom: 29px;
222 | }
223 | `;
224 |
225 | export const ComingNextHeaderStyled = div`
226 | display: block;
227 | font-size: 140%;
228 | text-align: center;
229 | padding-bottom: 8px;
230 | color: rgba(255,255,255,0.7);
231 | `;
232 |
233 | export const ComingNextTitleStyled = div`
234 | display: block;
235 | padding: 0 10px 2px;
236 | text-align: center;
237 | font-size: 200%;
238 | font-weight: 500;
239 | overflow: hidden;
240 | white-space: nowrap;
241 | word-wrap: normal;
242 | text-overflow: ellipsis;
243 | `;
244 |
245 | export const ComingNextAutoplayStyled = div`
246 | position: absolute;
247 | top: 50%;
248 | left: 50%;
249 | width: 64px;
250 | height: 64px;
251 | margin: -32px 0 0 -32px;
252 | cursor: pointer;
253 |
254 | @media ${device.tablet} {
255 | margin: -23px 0 0 -24px;
256 | width: 45px;
257 | height: 45px;
258 | }
259 | `;
260 |
261 | export const ComingNextIconStyled = div`
262 | position: absolute;
263 | font-size: 33px;
264 | left: 18px;
265 | top: 14px;
266 |
267 | @media ${device.tablet} {
268 | left: 14px;
269 | top: 12px;
270 | font-size: 19px;
271 | }
272 | `;
273 |
274 | export const ComingNextBottomStyled = div`
275 | width: 100%;
276 | position: absolute;
277 | margin-left: auto;
278 | margin-right: auto;
279 | top: 50%;
280 | margin-top: 48px;
281 | text-align: center;
282 |
283 | @media ${device.tablet} {
284 | margin-top: 27px;
285 | }
286 | `;
287 |
288 | export const ComingNextButtonStyled = div`
289 | display: inline-block;
290 | padding: 10px 20px;
291 | font-size: 140%;
292 | font-weight: 500;
293 | cursor: pointer;
294 | :hover {
295 | background-color: rgba(255,255,255,0.15);
296 | border-radius: 2px;
297 | }
298 | `;
299 |
300 | export const PlayerButtonStyled = button`
301 | color: ${() => theme.grayLightColor};
302 | `;
303 |
304 | export const VolumenMenuWrapperStyled = div`
305 | position: relative;
306 | display: flex;
307 | i {
308 | font-size: 16px;
309 | }
310 | `;
311 |
312 | export const VolumenMenuButtonStyled = div`
313 | width: 100px;
314 | margin-right: 15px;
315 | padding-top: 2px;
316 | @media ${device.laptop} {
317 | display:none;
318 | }
319 | `;
320 |
321 | export const VolumeIcon = styled(PlayerButtonStyled)`
322 | width: 30px;
323 | text-align: left;
324 | padding-left: 7px;
325 | padding-right: 0px;
326 | `;
327 |
328 | export const TextPlayerStyled = div`
329 | font-size: 13px;
330 | display: flex;
331 | align-self: center;
332 | `;
333 |
334 | export const EditButtonStyled = button`
335 | color: ${() => theme.grayLightColor};
336 | font-size: 130%;
337 | `;
338 |
339 | export const FragmentControlStyled = div`
340 | position: absolute;
341 | width: 100%;
342 | display: ${props => (props.editActive ? "block" : "none")}
343 | `;
344 |
345 | export const ProgressControlStyled = div`
346 | position: absolute;
347 | width: 100%;
348 | display: ${props => (props.show ? "none" : "block")}
349 | `;
350 | export const FullVideoStyled = div`
351 | position: relative;
352 | display: inline-block;
353 | width: 100%;
354 | max-width: ${({ fullscreen }) => (fullscreen ? "100%" : "900px")};
355 | height: ${({ fullscreen }) => (fullscreen ? "100%" : "auto")};
356 | background: black;
357 | font-size: 11px;
358 | margin: 0;
359 |
360 | @media ${device.laptopS} {
361 | margin: 0 auto;
362 | }
363 |
364 | ${TitleStyled}, ${ControlBarStyled} {
365 | opacity: ${({ showControls }) => (showControls ? "1" : "0")}
366 | }
367 | `;
368 |
--------------------------------------------------------------------------------
/src/components/Playlist/Item.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { IVideoClip } from "../types";
3 | import {
4 | ItemStyled,
5 | ItemContainStyled,
6 | ItemButtonsContainStyled,
7 | ItemLeftIndexStyled,
8 | ItemTitleStyled,
9 | ItemTagsStyled,
10 | ItemImageStyled
11 | } from "./styles";
12 |
13 | interface IProps extends IVideoClip {
14 | index: number;
15 | selected?: string;
16 | expand?: boolean;
17 | onClick?: (id: string) => void;
18 | }
19 |
20 | const Item: React.SFC = ({
21 | id,
22 | title,
23 | src,
24 | tags,
25 | children,
26 | expand,
27 | image,
28 | index,
29 | selected,
30 | onClick
31 | }) => (
32 |
33 |
34 | {!expand && (
35 |
36 | {selected && selected === id ? (
37 |
38 | ) : (
39 | index + 1
40 | )}
41 |
42 | )}
43 |
44 |
45 | {title}
46 |
47 | {tags.length ? : null}
48 | {tags.map((tag, tagIndex) => {tag})}
49 |
50 |
51 |
52 | {children}
53 |
54 | );
55 |
56 | export default Item;
57 |
--------------------------------------------------------------------------------
/src/components/Playlist/Playlist.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { IVideoClip } from "../types";
3 | import Item from "./Item";
4 | import {
5 | PlaylistHeadStyled,
6 | PlaylistBodyStyled,
7 | PlaylistHeadItemStyled,
8 | PlaylistWrapper,
9 | PlaylistContainer
10 | } from "./styles";
11 | import { isTypeEqual } from "../../utils";
12 |
13 | export interface IPropsExternal {
14 | onClick?: (id: string) => void;
15 | playlist?: IVideoClip[];
16 | idSelected?: string;
17 | expand?: boolean;
18 | children?: any;
19 | }
20 |
21 | export interface IProps extends IPropsExternal {}
22 |
23 | export const VideoClipContainer = props => props.children(props.id);
24 |
25 | export const PlaylistHeader: React.SFC = props => (
26 | {props.children}
27 | );
28 |
29 | const Playlist: React.SFC = props => {
30 | const renderHeader = (): JSX.Element | null => {
31 | const listPlaylistHeader = React.Children.toArray(props.children).filter(
32 | (child: any) => {
33 | return isTypeEqual(child, PlaylistHeader);
34 | }
35 | );
36 | if (listPlaylistHeader.length) {
37 | return {listPlaylistHeader};
38 | }
39 | return null;
40 | };
41 |
42 | const renderItemChildren = (id: string) => {
43 | return React.Children.toArray(props.children)
44 | .filter((child: any) => {
45 | return isTypeEqual(child, VideoClipContainer);
46 | })
47 | .map((child: any) => React.cloneElement(child, { id }));
48 | };
49 |
50 | const { playlist = [], onClick, idSelected, expand } = props;
51 | return (
52 |
53 |
54 | {renderHeader()}
55 |
56 | {playlist.map((item, index) => (
57 | -
65 | {renderItemChildren(item.id)}
66 |
67 | ))}
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | export default Playlist;
75 |
--------------------------------------------------------------------------------
/src/components/Playlist/styles.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { StyledFunction } from "styled-components";
3 | import styled, { theme, device } from "../../theme";
4 | // tslint:disable:no-shadowed-variable
5 |
6 | interface IDivStyled {
7 | [x: string]: any;
8 | }
9 | const div: StyledFunction> =
10 | styled.div;
11 | const button: StyledFunction> =
12 | styled.button;
13 |
14 | export const PlaylistWrapper = div`
15 | display: inline-block;
16 | background-color: ${({ expand }) =>
17 | expand ? theme.backgroundColor : theme.grayLightColor};
18 | width: ${({ expand }) => (expand ? "700px" : "500px")};
19 |
20 | @media ${device.laptopS} {
21 | width: 100%;
22 | }
23 | `;
24 |
25 | export const PlaylistContainer = div`
26 | display:block;
27 | color: ${() => theme.defaultColor};
28 | `;
29 |
30 | export const PlaylistHeadStyled = div`
31 | background: ${() => theme.grayColor};
32 | color: ${() => theme.darkColor};
33 | padding: 10px 15px;
34 | display: flex;
35 |
36 | button, a {
37 | color: ${() => theme.darkColor};
38 | font-size: 13px;
39 | display: inline-block;
40 | text-decoration: none;
41 | transition: all 0.2s;
42 | padding: 9px;
43 | transition: all 0.2s;
44 | border-radius: 5px;
45 | margin-right: 5px;
46 | background: ${() => theme.grayDarkColor};
47 | :hover {
48 | background: ${() => theme.grayDarkFocusColor};
49 | }
50 | }
51 | `;
52 |
53 | export const PlaylistHeadItemStyled = div`
54 | display: flex
55 | `;
56 |
57 | export const PlaylistSwitchStyled = div`
58 | cursor:pointer;
59 | color: ${() => theme.darkColor};
60 | font-size: 13px;
61 | margin-right: 5px;
62 | padding: 9px;
63 | display: flex;
64 | align-items: center;
65 | span {
66 | margin-left: 7px;
67 | }
68 | @media ${device.laptopS} {
69 | padding: 0;
70 | padding-right: 10px;
71 | }
72 | `;
73 |
74 | export const PlaylistHeadButtonStyled = button`
75 | color: ${({ active }) =>
76 | active ? theme.activeColor : theme.darkColor} !important;
77 | `;
78 |
79 | export const PlaylistBodyStyled = div`
80 | padding: 10px;
81 | overflow-y: auto;
82 | height: ${({ expand }) => (expand ? "auto" : "427px")};
83 | @media ${device.laptopS} {
84 | height: auto;
85 | }
86 | @media ${device.laptop} {
87 | text-align: left;
88 | height: auto;
89 | }
90 | `;
91 |
92 | export const ItemStyled = div`
93 | display: flex;
94 | margin-bottom: 5px;
95 | `;
96 |
97 | export const ItemImageStyled = div`
98 | width: 120px;
99 | height: 77px;
100 | background-color: black;
101 | background-image: url(${({ image }) => image});
102 | background-size: cover;
103 | background-position: center;
104 | `;
105 |
106 | export const ItemContainStyled = div`
107 | display: flex;
108 | flex: 1;
109 | align-items: center;
110 | cursor: ${props => (props.onClick ? "pointer" : "auto")};
111 | `;
112 |
113 | export const ItemButtonsContainStyled = div`
114 | align-items: center;
115 | display: flex;
116 | button {
117 | padding: 9px;
118 | transition: all 0.2s;
119 | border-radius: 50%;
120 | margin-left: 5px;
121 | width: 34px;
122 | i {
123 | font-size: 15px;
124 | color: ${() => theme.darkColor};
125 | }
126 | :hover {
127 | background: ${() => theme.grayColor};
128 | }
129 | }
130 | `;
131 |
132 | export const ItemTitleStyled = div`
133 | padding-left: 10px;
134 | font-weight: bold;
135 | font-size: 14px;
136 | white-space: normal;
137 | height: 38px;
138 | line-height: 1rem;
139 | width: 211px;
140 | overflow: hidden;
141 | text-overflow: ellipsis;
142 | display: -webkit-box;
143 | -webkit-line-clamp: 2;
144 | -webkit-box-orient: vertical;
145 | @media ${device.laptopS} {
146 | font-size: 12px;
147 | width: 50%;
148 | }
149 | h3 {
150 | margin-top: 5px;
151 | margin-bottom: 10px;
152 | }
153 | `;
154 |
155 | export const ItemTagsStyled = div`
156 | i {
157 | font-size: 12px;
158 | padding-right: 10px;
159 | color: ${() => theme.darkColor};
160 | }
161 | span {
162 | margin-right: 5px;
163 | padding: 5px;
164 | background: ${() => theme.grayColor};
165 | border-radius: 7px;
166 | font-size: 12px;
167 | color: ${() => theme.darkColor};
168 | }
169 | `;
170 |
171 | export const ItemLeftIndexStyled = div`
172 | font-size: 12px;
173 | width: 18px;
174 | min-width: 18px;
175 | color: ${() => theme.darkColor};
176 | i {
177 | font-size: 10px;
178 | }
179 | @media ${device.laptopS} {
180 | display:none
181 | }
182 | `;
183 |
--------------------------------------------------------------------------------
/src/components/VideoItem/VideoItem.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Link } from "react-router-dom";
3 | import { IVideoClip } from "../types";
4 | import {
5 | VideoItemStyled,
6 | VideoItemImage,
7 | VideoItemBottom,
8 | VideoItemTitle,
9 | VideoItemSubTitle
10 | } from "./styles";
11 |
12 | interface IProps extends IVideoClip { }
13 |
14 | const VideoItem: React.SFC = props => {
15 | return (
16 |
17 |
18 |
19 |
20 | {props.title}
21 | 2 weeks ago
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default VideoItem;
29 |
--------------------------------------------------------------------------------
/src/components/VideoItem/styles.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { StyledFunction } from "styled-components";
3 | import styled, { theme, device } from "../../theme";
4 | // tslint:disable:no-shadowed-variable
5 |
6 | interface IDivStyled {
7 | [x: string]: any;
8 | }
9 |
10 | const div: StyledFunction> =
11 | styled.div;
12 |
13 | export const VideoItemListStyled = div`
14 | width: 100%;
15 | display: flex;
16 | flex-wrap: wrap;
17 | @media ${device.laptopS} {
18 | justify-content: center;
19 | }
20 | `;
21 |
22 | export const VideoItemStyled = div`
23 | width: 210px;
24 | display: inline-block;
25 | position: relative;
26 | vertical-align: top;
27 | padding-right: 4px;
28 | margin-bottom: 20px;
29 | @media ${device.mobileL} {
30 | width: 100%;
31 | padding: 12px;
32 | }
33 | `;
34 |
35 | export const VideoItemImage = div`
36 | height: 118px;
37 | background-color: black;
38 | background-image: url(${({ image }) => image});
39 | background-size: cover;
40 | background-position: center;
41 | @media ${device.mobileL} {
42 | padding-bottom: 56.25%;
43 | position: relative;
44 | width: 100%;
45 | height: 0px;
46 | }
47 | `;
48 |
49 | export const VideoItemBottom = div`
50 | padding-right: 24px;
51 | padding-top: 5px;
52 | text-align: left;
53 | `;
54 |
55 | export const VideoItemTitle = div`
56 | font-weight: bold;
57 | font-size: 14px;
58 | overflow: hidden;
59 | text-overflow: ellipsis;
60 | white-space: normal;
61 | -webkit-line-clamp: 2;
62 | max-height: 32px;
63 | line-height: 1rem;
64 | margin: 8px 0 8px;
65 | display: -webkit-box;
66 | color: ${() => theme.defaultColor};
67 | `;
68 |
69 | export const VideoItemSubTitle = div`
70 | max-height: 3.6rem;
71 | overflow: hidden;
72 | font-size: 13px;
73 | font-weight: 400;
74 | color: ${() => theme.darkColor};
75 | `;
76 |
--------------------------------------------------------------------------------
/src/components/VideoPlayer/VideoPlayer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Switch from "rc-switch";
3 | import FullVideo, {
4 | IExternalProps as IFullVideoProps
5 | } from "../FullVideo/FullVideo";
6 | import Playlist, {
7 | IPropsExternal as IPlaylistProps,
8 | PlaylistHeader
9 | } from "../Playlist/Playlist";
10 | import {
11 | PlaylistHeadButtonStyled,
12 | PlaylistSwitchStyled
13 | } from "../Playlist/styles";
14 | import { VideoPlayStyled } from "./styles";
15 | import {
16 | isTypeEqual,
17 | findVideoClipIndexForId,
18 | getPlaylistActions
19 | } from "../../utils";
20 | import { IVideoClip, IVideoClipOptional } from "../types";
21 | import { LayoutFullVideo } from "../styles";
22 |
23 | export interface IPropsExternal {
24 | idSelected: string;
25 | playlist: IVideoClip[];
26 | repeat: boolean;
27 | random: boolean;
28 | autoPlaylist: boolean;
29 | onChangeRandom: (random: boolean) => void;
30 | onRemoveVideoClip: (id: string) => void;
31 | onChangeRepeat: (repeat: boolean) => void;
32 | onChangeAutoPlaylist: (autoPlay: boolean) => void;
33 | onChangeSelected: (id: string) => void;
34 | onChangeVideoClip: (id: string, playlist: IVideoClipOptional) => void;
35 | children?: any;
36 | }
37 |
38 | interface IProps extends IPropsExternal {
39 | children?: React.ReactNode;
40 | }
41 |
42 | const VideoPlayer: React.SFC = props => {
43 | const handleChangeTimeFragment = (startTime, endTime) => {
44 | const { onChangeVideoClip, idSelected } = props;
45 | onChangeVideoClip(idSelected, { startTime, endTime });
46 | };
47 |
48 | const handleClickPlaylistItem = (id: string) => {
49 | const { onChangeSelected } = props;
50 | document.body.scrollTop = 0;
51 | document.documentElement.scrollTop = 0;
52 | onChangeSelected(id);
53 | };
54 |
55 | const handlePlaylistAction = (videoClip: IVideoClip) => {
56 | const { onChangeSelected } = props;
57 | onChangeSelected(videoClip.id);
58 | };
59 |
60 | const getPlaylistChildren = child => {
61 | const {
62 | onChangeAutoPlaylist,
63 | repeat,
64 | onChangeRepeat,
65 | autoPlaylist,
66 | random,
67 | onChangeRandom
68 | } = props;
69 | const children = React.Children.toArray(child.props.children);
70 |
71 | children.unshift(
72 |
73 |
74 |
75 | Autoplay
76 |
77 |
81 |
82 | Loop
83 |
84 |
88 |
89 | Random
90 |
91 |
92 | );
93 |
94 | return children;
95 | };
96 |
97 | const renderChildren = () => {
98 | const {
99 | playlist,
100 | idSelected,
101 | autoPlaylist,
102 | repeat,
103 | random,
104 | onChangeAutoPlaylist
105 | } = props;
106 |
107 | return React.Children.toArray(props.children).map((child: any) => {
108 | // Take all Playlist components
109 | if (isTypeEqual(child, Playlist)) {
110 | const newProps: IPlaylistProps = {
111 | children: getPlaylistChildren(child),
112 | idSelected,
113 | onClick: handleClickPlaylistItem,
114 | playlist
115 | };
116 | return React.cloneElement(child, newProps);
117 | }
118 |
119 | // Take all FullVideo components
120 | if (
121 | isTypeEqual(child, FullVideo) ||
122 | isTypeEqual(child, LayoutFullVideo)
123 | ) {
124 | const index = findVideoClipIndexForId(playlist, idSelected);
125 |
126 | const {
127 | nextVideoClip,
128 | backVideoClip,
129 | currentVideoClip
130 | } = getPlaylistActions(index, playlist, repeat, random);
131 |
132 | const newProps: IFullVideoProps = {
133 | autoPlay: true,
134 | autoPlaylist: nextVideoClip ? autoPlaylist : false,
135 | backVideoClip,
136 | currentVideoClip,
137 | nextVideoClip,
138 | onChangeAutoPlaylist,
139 | onChangeTimeFragment: handleChangeTimeFragment,
140 | onClickPlaylistAction: handlePlaylistAction
141 | };
142 |
143 | return React.cloneElement(child, newProps);
144 | }
145 | return null;
146 | });
147 | };
148 |
149 | return {renderChildren()};
150 | };
151 |
152 | export default VideoPlayer;
153 |
--------------------------------------------------------------------------------
/src/components/VideoPlayer/styles.tsx:
--------------------------------------------------------------------------------
1 | import { StyledFunction } from "styled-components";
2 | import styled, { device } from "../../theme";
3 |
4 | interface IDivStyled {
5 | [x: string]: any;
6 | }
7 | const div: StyledFunction> =
8 | styled.div;
9 |
10 | export const VideoPlayStyled = div`
11 | width: 100%;
12 | padding: 0px 20px;
13 | display: flex;
14 | justify-content: center;
15 | align-items: flex-start;
16 | flex-direction: row;
17 |
18 | @media ${device.laptopS} {
19 | flex-direction: column;
20 | padding-left: 0;
21 | padding-right: 0;
22 | }
23 | `;
24 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as InputLabel } from "./Form/InputLabel";
2 | export {
3 | default as FullVideo,
4 | IExternalProps as IFullVideoProps
5 | } from "./FullVideo/FullVideo";
6 | export { default as VideoPlayer } from "./VideoPlayer/VideoPlayer";
7 | export { default as VideoItem } from "./VideoItem/VideoItem";
8 | export { VideoItemListStyled } from "./VideoItem/styles";
9 | export { default as Playlist } from "./Playlist/Playlist";
10 | export { PlaylistHeader, VideoClipContainer } from "./Playlist/Playlist";
11 | export { FormStyled } from "./Form/styles";
12 | export { IVideoClip, IVideoClipOptional } from "./types";
13 |
14 | export {
15 | PageStyled,
16 | ButtonStyled,
17 | TitlePageStyled,
18 | CardStyled,
19 | ContentStyled,
20 | ButtonPrimaryStyled,
21 | SidebarStyled,
22 | ContentDinamicStyled,
23 | PageWithSidebarStyled,
24 | ContentFullStyled,
25 | LayoutFullVideo,
26 | } from "./styles";
27 |
--------------------------------------------------------------------------------
/src/components/styles.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { StyledFunction } from "styled-components";
3 | import styled, { theme, device } from "../theme";
4 | // tslint:disable:no-shadowed-variable
5 |
6 | interface IDivStyled {
7 | [x: string]: any;
8 | }
9 | const div: StyledFunction> =
10 | styled.div;
11 | const button: StyledFunction> =
12 | styled.button;
13 |
14 | export const PageStyled = div`
15 | margin-top: 56px;
16 | `;
17 |
18 | export const SidebarStyled = div`
19 | position: fixed;
20 | width: 170px;
21 | background: whitesmoke;
22 | height: 100%;
23 | top: 0;
24 | display:block;
25 | @media ${device.laptopL} {
26 | display:none;
27 | }
28 | `;
29 |
30 | export const LayoutFullVideo = div`
31 | display: block;
32 | box-sizing: border-box;
33 | color: #fff;
34 | background-color: #000;
35 | position: relative;
36 | font-size: 10px;
37 | line-height: 1;
38 | font-family: serif;
39 | width: 100%;
40 | max-width: 100%;
41 | height: 0;
42 | padding-top: 56.25%;
43 | `
44 |
45 | export const PageWithSidebarStyled = styled(PageStyled)`
46 | background: #fafafa;
47 | margin-left: 170px;
48 | @media ${device.laptopL} {
49 | margin-left: 0px;
50 | }
51 | `;
52 |
53 | export const ButtonStyled = button`
54 | display: inline-block;
55 | height: 28px;
56 | border: solid 1px transparent;
57 | padding: 0 10px;
58 | outline: 0;
59 | font-weight: 500;
60 | font-size: 11px;
61 | text-decoration: none;
62 | white-space: nowrap;
63 | word-wrap: normal;
64 | line-height: normal;
65 | vertical-align: middle;
66 | cursor: pointer;
67 | border-radius: 2px;
68 | box-shadow: 0 1px 0 rgba(0,0,0,0.05);
69 | border-color: #d3d3d3;
70 | background: #f8f8f8;
71 | color: #333;
72 | :hover {
73 | border-color: #c6c6c6;
74 | background: #f0f0f0;
75 | box-shadow: 0 1px 0 rgba(0,0,0,0.10);
76 | }
77 | :disabled {
78 | opacity: 0.7;
79 | cursor: no-drop;
80 | :hover {
81 | background: #f8f8f8;
82 | border-color: #d3d3d3;
83 | }
84 | }
85 | `;
86 |
87 | const buttonDefault: StyledFunction<
88 | IDivStyled & React.HTMLProps
89 | > = styled(ButtonStyled);
90 |
91 | export const ButtonPrimaryStyled = buttonDefault`
92 | border-color: #167ac6;
93 | background: #167ac6;
94 | color: #fff;
95 | padding: 0 20px;
96 | margin-bottom: 10px;
97 | :hover {
98 | background: #126db3;
99 | color: #fff;
100 | border-color: #167ac6;
101 | }
102 | `;
103 |
104 | const TitleStyled = div`
105 | font-size: 12px;
106 | display: inline-block;
107 | border-bottom: 4px solid ${() => theme.primaryColor};
108 | padding-bottom: 12px;
109 | padding-left: 15px;
110 | padding-right: 15px;
111 | `;
112 |
113 | const TitleWrapperStyled = div`
114 | border-bottom: 1px solid #ccc;
115 | margin-bottom: 20px;
116 | `;
117 |
118 | export const TitlePageStyled: React.SFC = props => (
119 |
120 |
121 |
122 | );
123 |
124 | export const ContentStyled = div`
125 | position: relative;
126 | padding-top: 20px;
127 | margin-left: auto;
128 | margin-right: auto;
129 | max-width: 1003px;
130 | width: 100%;
131 | text-align: left;
132 | display: flex;
133 | `;
134 |
135 | export const ContentDinamicStyled = styled(ContentStyled)`
136 | max-width: initial;
137 | padding-top: 40px;
138 | width: 1284px;
139 | text-align: left;
140 | @media ${device.laptopM} {
141 | width: 1070px;
142 | }
143 | @media ${device.laptopL} {
144 | width: 856px;
145 | }
146 | @media ${device.laptop} {
147 | width: auto;
148 | }
149 | @media ${device.mobileL} {
150 | padding-top: 0px;
151 | }
152 | `;
153 |
154 | export const ContentFullStyled = styled(ContentStyled)`
155 | width: 100%;
156 | max-width: initial;
157 | background: white;
158 | @media ${device.laptop} {
159 | padding-top: 0;
160 | }
161 | `;
162 |
163 | export const CardStyled = div`
164 | margin: 0 0 10px;
165 | border: 0;
166 | background: #fff;
167 | box-shadow: 0 1px 2px rgba(0,0,0,.1);
168 | position: relative;
169 | width: 1003px;
170 | overflow: hidden;
171 | border: 1px solid #ddd;
172 | background-color: #fff;
173 | padding: 15px;
174 | @media ${device.laptop} {
175 | width: 86%;
176 | margin: 0 auto;
177 | }
178 | `;
179 |
--------------------------------------------------------------------------------
/src/components/types.ts:
--------------------------------------------------------------------------------
1 | export interface IVideoClip {
2 | id: string;
3 | title: string;
4 | endTime: number;
5 | startTime: number;
6 | src: string;
7 | image: string;
8 | tags: string[];
9 | }
10 |
11 | export interface IVideoClipOptional {
12 | id?: string;
13 | title?: string;
14 | endTime?: number;
15 | startTime?: number;
16 | src?: string;
17 | image?: string;
18 | tags?: string[];
19 | }
20 |
--------------------------------------------------------------------------------
/src/container/MobileVideo/MobileVideo.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { RouteComponentProps } from "react-router-dom";
3 | import { withRouter } from "react-router";
4 | import { FullVideo, IFullVideoProps, IVideoClip } from "../../components";
5 | import AppProvider, { IAppProvider } from "../../AppProvider";
6 |
7 | import {
8 | findVideoClipForId,
9 | findVideoClipIndexForId,
10 | getPlaylistActions
11 | } from "../../utils";
12 | import { MobileVideoStyled, MobileVideoCloseStyled } from "./styles";
13 | import { VideoPlayStyled } from "../../components/VideoPlayer/styles";
14 |
15 | interface IProps extends RouteComponentProps, IAppProvider {}
16 |
17 | const MobileVideo = withRouter((props: IProps) => {
18 | const {
19 | playlist,
20 | idVideo,
21 | inFullPlayer,
22 | autoPlaylist,
23 | setAutoPlaylist,
24 | repeat,
25 | random
26 | } = props;
27 | const showControls = inFullPlayer;
28 |
29 | const changeVideo = (id: string) => {
30 | props.history.push("/video/" + id);
31 | };
32 |
33 | const handlePlaylistAction = (videoClip: IVideoClip) => {
34 | changeVideo(videoClip.id);
35 | };
36 |
37 | const handleClick = () => {
38 | if (!showControls) {
39 | changeVideo(props.idVideo);
40 | }
41 | };
42 |
43 | const handleClose = () => {
44 | props.setIdVideo("");
45 | };
46 |
47 | const propsFullVideo: IFullVideoProps = {
48 | autoPlay: true,
49 | onChangeAutoPlaylist: setAutoPlaylist,
50 | onClickPlaylistAction: handlePlaylistAction,
51 | showControls
52 | };
53 |
54 | if (idVideo !== "") {
55 | const index = findVideoClipIndexForId(playlist, idVideo);
56 | const {
57 | nextVideoClip,
58 | backVideoClip,
59 | currentVideoClip
60 | } = getPlaylistActions(index, playlist, repeat, random);
61 |
62 | propsFullVideo.autoPlaylist = nextVideoClip ? autoPlaylist : false;
63 | propsFullVideo.currentVideoClip = currentVideoClip;
64 | propsFullVideo.backVideoClip = backVideoClip;
65 | propsFullVideo.nextVideoClip = nextVideoClip;
66 | }
67 |
68 | return (
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | );
78 | });
79 |
80 | export default () => (
81 |
82 | {(value: IAppProvider) => }
83 |
84 | );
85 |
--------------------------------------------------------------------------------
/src/container/MobileVideo/styles.tsx:
--------------------------------------------------------------------------------
1 | import { StyledFunction } from "styled-components";
2 | import styled, { device, theme } from "../../theme";
3 | import { EditButtonStyled } from "../../components/FullVideo/styles";
4 |
5 | interface IDivStyled {
6 | [x: string]: any;
7 | }
8 | const div: StyledFunction> =
9 | styled.div;
10 |
11 | export const MobileVideoStyled = div`
12 | position: ${({ top }) => (top ? "absolute" : "fixed")};
13 | bottom: 0;
14 | top: ${({ top }) => (top ? "208px" : "100%")};
15 | margin-top: -152px;
16 | height: 152px;
17 | width: ${({ top }) => (top ? "100%" : "270px")};
18 | right: 0;
19 | z-index: 1;
20 | opacity: 1;
21 | transition: all 0.2s;
22 |
23 | ${EditButtonStyled} {
24 | display: none;
25 | }
26 | `;
27 |
28 | export const MobileVideoWrapperStyled = div`
29 | width: 100%;
30 | padding: 0px;
31 | display: flex;
32 | `;
33 |
34 | export const MobileVideoCloseStyled = div`
35 | position: absolute;
36 | left: -26px;
37 | background: white;
38 | font-size: 17px;
39 | width: 26px;
40 | height: 26px;
41 | padding-top: 5px;
42 | color: ${() => theme.primaryColor};
43 | text-align: center;
44 | `;
45 |
--------------------------------------------------------------------------------
/src/container/Navbar/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Link, RouteComponentProps } from "react-router-dom";
3 | import { withRouter } from "react-router";
4 | import {
5 | NavbarStyled,
6 | NavbarLogoStyled,
7 | NavBarLink,
8 | NavbarButtonStyled
9 | } from "./styles";
10 | import SearchBox from "./SearchBox";
11 | import AppProvider, { IAppProvider } from "../../AppProvider";
12 |
13 | interface IProps extends RouteComponentProps, IAppProvider {}
14 |
15 | const Navbar = withRouter(({ history, location, resetDatabase }: IProps) => {
16 | const handleRecover = () => {
17 | resetDatabase();
18 | };
19 |
20 | const isInVideoPage = location.pathname.indexOf("/video") > -1;
21 |
22 | return (
23 |
24 |
25 |
26 |
41 |
42 |
43 |
44 |
45 |
46 | Add videos
47 |
48 |
49 |
50 | Recover localstorage
51 |
52 |
53 | );
54 | });
55 |
56 | export default () => (
57 |
58 | {(value: IAppProvider) => }
59 |
60 | );
61 |
--------------------------------------------------------------------------------
/src/container/Navbar/SearchBox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { History } from "history";
3 | import { InputLabel } from "../../components";
4 | import {
5 | SearchBoxButtonStyled,
6 | SearchBoxStyled,
7 | SearchBoxFormStyled
8 | } from "./styles";
9 |
10 | interface IProps {
11 | history: History;
12 | }
13 |
14 | class SearchBox extends React.Component {
15 | public state = {
16 | value: ""
17 | };
18 |
19 | public handleChange = value => {
20 | this.setState({ value });
21 | };
22 |
23 | public handleSubmit = e => {
24 | const { value } = this.state;
25 | const cleanValue = value.trim()
26 | if (cleanValue === "") {
27 | this.props.history.push("/");
28 | } else {
29 | this.props.history.push("/search/" + cleanValue);
30 | }
31 | e.preventDefault();
32 | };
33 |
34 | public render() {
35 | return (
36 |
37 |
38 |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | }
50 | }
51 |
52 | export default SearchBox;
53 |
--------------------------------------------------------------------------------
/src/container/Navbar/styles.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { StyledFunction } from "styled-components";
3 | import {
4 | InputLabelGeneralStyled,
5 | InputStyled
6 | } from "../../components/Form/styles";
7 | import styled, { theme, device } from "../../theme";
8 | // tslint:disable:no-shadowed-variable
9 |
10 | interface IDivStyled {
11 | [x: string]: any;
12 | }
13 | const div: StyledFunction> =
14 | styled.div;
15 | const button: StyledFunction> =
16 | styled.button;
17 | const form: StyledFunction> =
18 | styled.form;
19 |
20 | export const NavbarStyled: React.SFC = props => (
21 |
22 |
23 |
24 | );
25 |
26 | export const NavbarWrapperStyled = div`
27 | position: fixed;
28 | top: 0;
29 | width: 100%;
30 | z-index: 50;
31 | background: #ffffff;
32 | :after {
33 | bottom: -5px;
34 | box-shadow: inset 0px 4px 8px -3px rgba(17, 17, 17, .06);
35 | content: "";
36 | height: 5px;
37 | left: 0px;
38 | opacity: 1;
39 | position: absolute;
40 | right: 0px;
41 | width: 100%;
42 | }
43 | ${InputLabelGeneralStyled} {
44 | margin: 0;
45 | @media ${device.laptop} {
46 | overflow: hidden;
47 | border-radius: 2px;
48 | }
49 | input {
50 | margin: 0;
51 | font-size: 16px;
52 | line-height: 24px;
53 | background-color: #ffffff;
54 | border: 1px solid #ccc;
55 | border-right: none;
56 | box-shadow: inset 0 1px 2px #eeeeee;
57 | padding: 2px 11px;
58 | border-radius: 2px 0 0 2px;
59 | width: 400px;
60 | @media ${device.laptop} {
61 | width: 95%;
62 | border-radius: 2px;
63 | border-width: 0px;
64 | margin-left: 5px;
65 | }
66 | }
67 | }
68 | `;
69 |
70 | export const NavbarContainerStyled = div`
71 | height: 56px;
72 | padding: 0 16px;
73 | display: flex;
74 | flex-direction: row;
75 | align-items: center;
76 | flex-wrap: wrap;
77 | @media ${device.laptop} {
78 | padding: 0 5px;
79 | background-color: ${({ gray }) =>
80 | gray ? theme.darkHeader : theme.primaryColor}
81 | }
82 | `;
83 |
84 | export const NavbarLogoStyled = div`
85 | height: 24px;
86 | width: 40px;
87 | cursor: pointer;
88 | svg {
89 | width: 100%;
90 | height: 100%;
91 | @media ${device.laptop} {
92 | rect {
93 | fill: white;
94 | }
95 | polygon {
96 | fill: ${({ gray }) => (gray ? theme.darkHeader : theme.primaryColor)}
97 | }
98 | }
99 | }
100 | `;
101 |
102 | export const NavBarLink = div`
103 | margin-right: 5px;
104 | a {
105 | cursor: pointer;
106 | border: 1px solid #d3d3d3;
107 | background-color: #f8f8f8;
108 | color: ${() => theme.darkColor}
109 | border-radius: 2px;
110 | margin: 0;
111 | padding: 6px 20px;
112 | font-size: 13px;
113 | text-align: center;
114 | vertical-align: middle;
115 | display: inline-block;
116 | :hover {
117 | border-color: #c6c6c6;
118 | background-color: #f0f0f0;
119 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.10);
120 | @media ${device.laptop} {
121 | background: transparent;
122 | border-color: transparent;
123 | box-shadow: none;
124 | }
125 | }
126 | @media ${device.laptop} {
127 | background: transparent;
128 | border-color: transparent;
129 | color: white;
130 | padding: 6px 11px;
131 | }
132 | }
133 | @media ${device.laptop} {
134 | span {
135 | display: none;
136 | }
137 | }
138 | `;
139 |
140 | export const NavbarButtonStyled = button`
141 | cursor: pointer;
142 | border: 1px solid #d3d3d3;
143 | background-color: #f8f8f8;
144 | color: ${() => theme.darkColor}
145 | border-radius: 2px;
146 | margin: 0;
147 | padding: 6px 20px;
148 | :hover {
149 | border-color: #c6c6c6;
150 | background-color: #f0f0f0;
151 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.10);
152 | @media ${device.laptop} {
153 | background: transparent;
154 | border-color: transparent;
155 | box-shadow: none;
156 | }
157 | }
158 | @media ${device.laptop} {
159 | background: transparent;
160 | border-color: transparent;
161 | color: white;
162 | padding: 6px 11px;
163 | span {
164 | display: none;
165 | }
166 | }
167 | `;
168 |
169 | export const navbarButton: StyledFunction<
170 | IDivStyled & React.HTMLProps
171 | > = styled(NavbarButtonStyled);
172 |
173 | export const SearchBoxStyled = div`
174 | margin: 0 40px;
175 | display: flex;
176 |
177 | @media ${device.laptop} {
178 | margin: 0;
179 | flex: 1;
180 | padding-right: 7px;
181 | }
182 | `;
183 |
184 | export const SearchBoxFormStyled = form`
185 | display: flex;
186 | flex-direction: row;
187 | `;
188 |
189 | export const SearchBoxButtonStyled = navbarButton`
190 | border-radius: 0 2px 2px 0;
191 | width: 65px;
192 | padding: 0px;
193 | @media ${device.laptop} {
194 | border-color: transparent;
195 | background-color: transparent;
196 | color: white;
197 | width: 43px;
198 | }
199 | `;
200 |
--------------------------------------------------------------------------------
/src/db.ts:
--------------------------------------------------------------------------------
1 | import { IVideoClip } from "./components";
2 | // tslint:disable:object-literal-sort-keys
3 |
4 | interface IData {
5 | description: string;
6 | sources: string;
7 | subtitle: string;
8 | title: string;
9 | thumb: string;
10 | duration: number;
11 | }
12 |
13 | const database: IData[] = [
14 | {
15 | description:
16 | "Big Buck Bunny tells the story of a giant rabbit with a heart bigger than himself. When one sunny day three rodents rudely harass him, something snaps... and the rabbit ain't no bunny anymore! In the typical cartoon tradition he prepares the nasty rodents a comical revenge.\n\nLicensed under the Creative Commons Attribution license\nhttp://www.bigbuckbunny.org",
17 | sources:
18 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
19 | subtitle: "By Blender Foundation",
20 | thumb: "images/BigBuckBunny.jpg",
21 | title: "Big Buck Bunny",
22 | duration: 571
23 | },
24 | {
25 | description: "The first Blender Open Movie from 2006",
26 | sources:
27 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
28 | subtitle: "By Blender Foundation",
29 | thumb: "images/ElephantsDream.jpg",
30 | title: "Elephant Dream",
31 | duration: 600
32 | },
33 | {
34 | description:
35 | "HBO GO now works with Chromecast -- the easiest way to enjoy online video on your TV. For when you want to settle into your Iron Throne to watch the latest episodes. For $35.\nLearn how to use Chromecast with HBO GO and more at google.com/chromecast.",
36 | sources:
37 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4",
38 | subtitle: "By Google",
39 | thumb: "images/ForBiggerBlazes.jpg",
40 | title: "For Bigger Blazes",
41 | duration: 15
42 | },
43 | {
44 | description:
45 | "Introducing Chromecast. The easiest way to enjoy online video and music on your TV—for when Batman's escapes aren't quite big enough. For $35. Learn how to use Chromecast with Google Play Movies and more at google.com/chromecast.",
46 | sources:
47 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4",
48 | subtitle: "By Google",
49 | thumb: "images/ForBiggerEscapes.jpg",
50 | title: "For Bigger Escape",
51 | duration: 15
52 | },
53 | {
54 | description:
55 | "Introducing Chromecast. The easiest way to enjoy online video and music on your TV. For $35. Find out more at google.com/chromecast.",
56 | sources:
57 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4",
58 | subtitle: "By Google",
59 | thumb: "images/ForBiggerFun.jpg",
60 | title: "For Bigger Fun",
61 | duration: 60
62 | },
63 | {
64 | description:
65 | "Introducing Chromecast. The easiest way to enjoy online video and music on your TV—for the times that call for bigger joyrides. For $35. Learn how to use Chromecast with YouTube and more at google.com/chromecast.",
66 | sources:
67 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4",
68 | subtitle: "By Google",
69 | thumb: "images/ForBiggerJoyrides.jpg",
70 | title: "For Bigger Joyrides",
71 | duration: 15
72 | },
73 | {
74 | description:
75 | "Introducing Chromecast. The easiest way to enjoy online video and music on your TV—for when you want to make Buster's big meltdowns even bigger. For $35. Learn how to use Chromecast with Netflix and more at google.com/chromecast.",
76 | sources:
77 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4",
78 | subtitle: "By Google",
79 | thumb: "images/ForBiggerMeltdowns.jpg",
80 | title: "For Bigger Meltdowns",
81 | duration: 15
82 | },
83 | {
84 | description:
85 | "Sintel is an independently produced short film, initiated by the Blender Foundation as a means to further improve and validate the free/open source 3D creation suite Blender. With initial funding provided by 1000s of donations via the internet community, it has again proven to be a viable development model for both open 3D technology as for independent animation film.\nThis 15 minute film has been realized in the studio of the Amsterdam Blender Institute, by an international team of artists and developers. In addition to that, several crucial technical and creative targets have been realized online, by developers and artists and teams all over the world.\nwww.sintel.org",
86 | sources:
87 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
88 | subtitle: "By Blender Foundation",
89 | thumb: "images/Sintel.jpg",
90 | title: "Sintel",
91 | duration: 840
92 | },
93 | {
94 | description:
95 | "Smoking Tire takes the all-new Subaru Outback to the highest point we can find in hopes our customer-appreciation Balloon Launch will get some free T-shirts into the hands of our viewers.",
96 | sources:
97 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4",
98 | subtitle: "By Garage419",
99 | thumb: "images/SubaruOutbackOnStreetAndDirt.jpg",
100 | title: "Subaru Outback On Street And Dirt",
101 | duration: 540
102 | },
103 | {
104 | description:
105 | "Tears of Steel was realized with crowd-funding by users of the open source 3D creation tool Blender. Target was to improve and test a complete open and free pipeline for visual effects in film - and to make a compelling sci-fi film in Amsterdam, the Netherlands. The film itself, and all raw material used for making it, have been released under the Creatieve Commons 3.0 Attribution license. Visit the tearsofsteel.org website to find out more about this, or to purchase the 4-DVD box with a lot of extras. (CC) Blender Foundation - http://www.tearsofsteel.org",
106 | sources:
107 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4",
108 | subtitle: "By Blender Foundation",
109 | thumb: "images/TearsOfSteel.jpg",
110 | title: "Tears of Steel",
111 | duration: 720
112 | },
113 | {
114 | description:
115 | "The Smoking Tire heads out to Adams Motorsports Park in Riverside, CA to test the most requested car of 2010, the Volkswagen GTI. Will it beat the Mazdaspeed3's standard-setting lap time? Watch and see...",
116 | sources:
117 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/VolkswagenGTIReview.mp4",
118 | subtitle: "By Garage419",
119 | thumb: "images/VolkswagenGTIReview.jpg",
120 | title: "Volkswagen GTI Review",
121 | duration: 540
122 | },
123 | {
124 | description:
125 | "The Smoking Tire is going on the 2010 Bullrun Live Rally in a 2011 Shelby GT500, and posting a video from the road every single day! The only place to watch them is by subscribing to The Smoking Tire or watching at BlackMagicShine.com",
126 | sources:
127 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WeAreGoingOnBullrun.mp4",
128 | subtitle: "By Garage419",
129 | thumb: "images/WeAreGoingOnBullrun.jpg",
130 | title: "We Are Going On Bullrun",
131 | duration: 47
132 | },
133 | {
134 | description:
135 | "The Smoking Tire meets up with Chris and Jorge from CarsForAGrand.com to see just how far $1,000 can go when looking for a car.The Smoking Tire meets up with Chris and Jorge from CarsForAGrand.com to see just how far $1,000 can go when looking for a car.",
136 | sources:
137 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4",
138 | subtitle: "By Garage419",
139 | thumb: "images/WhatCarCanYouGetForAGrand.jpg",
140 | title: "What care can you get for a grand?",
141 | duration: 540
142 | }
143 | ];
144 |
145 | const getRandomArbitrary = (min, max) => {
146 | return Math.floor(Math.random() * (max - min)) + min;
147 | };
148 |
149 | const getFolderFromUrl = (url: string) => {
150 | return (
151 | url
152 | .split("/")
153 | .slice(0, -1)
154 | .join("/") + "/"
155 | );
156 | };
157 |
158 | const convertToVideoClip = (data: IData, index: number): IVideoClip => {
159 | const { sources, title, thumb, duration } = data;
160 | const folderUrl = getFolderFromUrl(sources);
161 |
162 | const startTime = getRandomArbitrary(0, Math.floor(duration * 0.7));
163 | const endTime = getRandomArbitrary(
164 | getRandomArbitrary(startTime + 2, duration),
165 | duration
166 | );
167 |
168 | return {
169 | title,
170 | src: sources,
171 | image: folderUrl + thumb,
172 | id: String(index),
173 | endTime,
174 | startTime,
175 | tags: []
176 | };
177 | };
178 |
179 | export default database.map(convertToVideoClip);
180 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import "rc-slider/assets/index.css";
2 | import * as React from "react";
3 | import * as ReactDOM from "react-dom";
4 | import "react-tagsinput/react-tagsinput.css";
5 | import App from "./App";
6 | import "./styles.css";
7 |
8 | import registerServiceWorker from "./registerServiceWorker";
9 |
10 | ReactDOM.render(, document.getElementById("root") as HTMLElement);
11 | registerServiceWorker();
12 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:no-console
2 | // In production, we register a service worker to serve assets from local cache.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on the 'N+1' visit to a page, since previously
7 | // cached resources are updated in the background.
8 |
9 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
10 | // This link also includes instructions on opting out of this behavior.
11 |
12 | const isLocalhost = Boolean(
13 | window.location.hostname === 'localhost' ||
14 | // [::1] is the IPv6 localhost address.
15 | window.location.hostname === '[::1]' ||
16 | // 127.0.0.1/8 is considered localhost for IPv4.
17 | window.location.hostname.match(
18 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
19 | )
20 | );
21 |
22 | export default function register() {
23 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
24 | // The URL constructor is available in all browsers that support SW.
25 | const publicUrl = new URL(
26 | process.env.PUBLIC_URL!,
27 | window.location.toString()
28 | );
29 | if (publicUrl.origin !== window.location.origin) {
30 | // Our service worker won't work if PUBLIC_URL is on a different origin
31 | // from what our page is served on. This might happen if a CDN is used to
32 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
33 | return;
34 | }
35 |
36 | window.addEventListener('load', () => {
37 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
38 |
39 | if (isLocalhost) {
40 | // This is running on localhost. Lets check if a service worker still exists or not.
41 | checkValidServiceWorker(swUrl);
42 |
43 | // Add some additional logging to localhost, pointing developers to the
44 | // service worker/PWA documentation.
45 | navigator.serviceWorker.ready.then(() => {
46 | console.log(
47 | 'This web app is being served cache-first by a service ' +
48 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
49 | );
50 | });
51 | } else {
52 | // Is not local host. Just register service worker
53 | registerValidSW(swUrl);
54 | }
55 | });
56 | }
57 | }
58 |
59 | function registerValidSW(swUrl: string) {
60 | navigator.serviceWorker
61 | .register(swUrl)
62 | .then(registration => {
63 | registration.onupdatefound = () => {
64 | const installingWorker = registration.installing;
65 | if (installingWorker) {
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the old content will have been purged and
70 | // the fresh content will have been added to the cache.
71 | // It's the perfect time to display a 'New content is
72 | // available; please refresh.' message in your web app.
73 | console.log('New content is available; please refresh.');
74 | } else {
75 | // At this point, everything has been precached.
76 | // It's the perfect time to display a
77 | // 'Content is cached for offline use.' message.
78 | console.log('Content is cached for offline use.');
79 | }
80 | }
81 | };
82 | }
83 | };
84 | })
85 | .catch(error => {
86 | console.error('Error during service worker registration:', error);
87 | });
88 | }
89 |
90 | function checkValidServiceWorker(swUrl: string) {
91 | // Check if the service worker can be found. If it can't reload the page.
92 | fetch(swUrl)
93 | .then(response => {
94 | // Ensure service worker exists, and that we really are getting a JS file.
95 | if (
96 | response.status === 404 ||
97 | response.headers.get('content-type')!.indexOf('javascript') === -1
98 | ) {
99 | // No service worker found. Probably a different app. Reload the page.
100 | navigator.serviceWorker.ready.then(registration => {
101 | registration.unregister().then(() => {
102 | window.location.reload();
103 | });
104 | });
105 | } else {
106 | // Service worker found. Proceed as normal.
107 | registerValidSW(swUrl);
108 | }
109 | })
110 | .catch(() => {
111 | console.log(
112 | 'No internet connection found. App is running in offline mode.'
113 | );
114 | });
115 | }
116 |
117 | export function unregister() {
118 | if ('serviceWorker' in navigator) {
119 | navigator.serviceWorker.ready.then(registration => {
120 | registration.unregister();
121 | });
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/screens/Edit/Edit.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { RouteComponentProps } from "react-router-dom";
3 | import EditForm from "./EditForm";
4 | import {
5 | CardStyled,
6 | ContentStyled,
7 | PageStyled,
8 | TitlePageStyled,
9 | IVideoClip
10 | } from "../../components";
11 | import AppProvider, { IAppProvider } from "../../AppProvider";
12 | import { findVideoClipIndexForId } from "../../utils";
13 |
14 | interface IRouterProps {
15 | id?: string;
16 | }
17 |
18 | interface IProps
19 | extends IAppProvider,
20 | RouteComponentProps {}
21 |
22 | class Edit extends React.Component {
23 | public state = {};
24 |
25 | public handleSuccess = (playlist: IVideoClip) => {
26 | const { setVideoClip, addVideoClip } = this.props;
27 | const { id } = this.props.match.params;
28 |
29 | if (id) {
30 | setVideoClip(id, playlist);
31 | } else {
32 | addVideoClip(playlist);
33 | }
34 | };
35 |
36 | public render() {
37 | const { playlist, history } = this.props;
38 | const { id } = this.props.match.params;
39 | let index;
40 | if (id) {
41 | index = findVideoClipIndexForId(playlist, id);
42 | }
43 | const existsIndex = index !== undefined;
44 |
45 | return (
46 |
47 |
48 |
49 |
50 | {id ? "Edit video" : "Add to playlist"}
51 |
52 |
57 |
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 | export default (props: RouteComponentProps) => (
65 |
66 | {(value: IAppProvider) => }
67 |
68 | );
69 |
--------------------------------------------------------------------------------
/src/screens/Edit/EditForm.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Disclaimer: It is also possible using react-final-form
3 | * but I wanted to do a demonstration without using a form composition framework
4 | */
5 |
6 | import * as React from "react";
7 | import {
8 | InputLabel,
9 | IVideoClip,
10 | ButtonPrimaryStyled,
11 | FormStyled
12 | } from "../../components";
13 | import {
14 | validRequired,
15 | validImageURL,
16 | validTime,
17 | validVideoURL,
18 | getTime,
19 | parseFormtoPlaylist
20 | } from "./utils";
21 | import { formatTime } from "../../utils";
22 | import { History } from "history";
23 | import { IForm } from "./type";
24 |
25 | interface IProps {
26 | history: History;
27 | onSuccess: (playlist: IVideoClip) => void;
28 | playlist?: IVideoClip;
29 | onSubmit?: () => void;
30 | }
31 |
32 | export default class EditForm extends React.Component {
33 | public state = {
34 | complete: {},
35 | errors: {},
36 | form: {
37 | endTime: "",
38 | image: "",
39 | startTime: "",
40 | tags: [],
41 | title: "",
42 | video: ""
43 | },
44 | loading: {},
45 | submitted: false,
46 | success: false
47 | };
48 |
49 | constructor(props: IProps) {
50 | super(props);
51 |
52 | const playlist: any = props.playlist || {};
53 |
54 | this.state = {
55 | complete: {},
56 | errors: {},
57 | form: {
58 | endTime: formatTime(playlist.endTime, 3600) || "",
59 | image: playlist.image || "",
60 | startTime: formatTime(playlist.startTime, 3600) || "",
61 | tags: playlist.tags || [],
62 | title: playlist.title || "",
63 | video: playlist.src || ""
64 | },
65 | loading: {},
66 | submitted: false,
67 | success: false
68 | };
69 | }
70 |
71 | public handleChange = (property, value) => {
72 | this.setState(
73 | {
74 | form: {
75 | ...this.state.form,
76 | [property]: value
77 | }
78 | },
79 | () => {
80 | if (this.state.submitted) {
81 | this.validForm();
82 | }
83 | }
84 | );
85 | };
86 |
87 | public validForm = (successCb?: () => void) => {
88 | const form: IForm = this.state.form;
89 | const errors: IForm = {};
90 |
91 | Object.keys(form).forEach(key => {
92 | const value = form[key];
93 | if (typeof value === "string") {
94 | if (!validRequired(value)) {
95 | errors[key] = "This field is requeried";
96 | }
97 | }
98 | });
99 |
100 | const { startTime, endTime, video, image } = form;
101 |
102 | let startTimeSeconds;
103 | let endTimeSeconds;
104 |
105 | if (!errors.startTime) {
106 | if (validTime(startTime)) {
107 | startTimeSeconds = getTime(startTime);
108 | } else {
109 | errors.startTime = "Complete this field correctly";
110 | }
111 | }
112 |
113 | if (!errors.endTime) {
114 | if (validTime(endTime)) {
115 | endTimeSeconds = getTime(endTime);
116 | } else {
117 | errors.endTime = "Complete this field correctly";
118 | }
119 | }
120 |
121 | if (
122 | !errors.startTime &&
123 | !errors.endTime &&
124 | startTimeSeconds >= endTimeSeconds
125 | ) {
126 | errors.endTime = "The end time must be greater than the start time";
127 | }
128 |
129 | if (!errors.image) {
130 | if (!validImageURL(image)) {
131 | errors.image = "It must be a valid image URL (.jpeg .jpg .gif .png)";
132 | }
133 | }
134 |
135 | if (!errors.video) {
136 | if (!validVideoURL(video)) {
137 | errors.video = "It must be a valid video URL (.mp4)";
138 | }
139 | }
140 |
141 | this.setState(
142 | {
143 | errors
144 | },
145 | () => {
146 | if (successCb) {
147 | successCb();
148 | }
149 | }
150 | );
151 | };
152 |
153 | public handleSubmit = e => {
154 | e.preventDefault();
155 |
156 | this.setState({ submitted: true });
157 | this.validForm(() => {
158 | const isThereErrors = Object.keys(this.state.errors).length;
159 | if (!isThereErrors) {
160 | this.setState({
161 | success: true
162 | });
163 | this.props.onSuccess(parseFormtoPlaylist(this.state.form));
164 | }
165 | });
166 | };
167 |
168 | public handleBack = () => {
169 | this.props.history.push("/");
170 | };
171 |
172 | public render() {
173 | const {
174 | form: { title, image, endTime, startTime, video, tags },
175 | loading,
176 | complete,
177 | success,
178 | errors
179 | } = this.state;
180 | const playlist = this.props.playlist;
181 | const errorsForm: any = errors; // Fix typescript
182 | const loadingForm: any = loading; // Fix typescript
183 | const completeForm: any = complete; // Fix typescript
184 |
185 | return success ? (
186 |
187 |
188 | {playlist
189 | ? "The video was updated successfully"
190 | : "The video was added successfully"}
191 |
192 |
193 | Go home
194 |
195 |
196 | ) : (
197 |
198 |
204 |
212 |
220 |
227 |
234 |
241 |
242 | {playlist ? "Save in LocalStorage" : "Add in Localstorage"}
243 |
244 |
245 | );
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/src/screens/Edit/type.ts:
--------------------------------------------------------------------------------
1 | export interface IForm {
2 | endTime?: string;
3 | image?: string;
4 | video?: string;
5 | startTime?: string;
6 | tags?: string[];
7 | title?: string;
8 | }
9 |
10 | export interface IFormSuccess {
11 | endTime: string;
12 | image: string;
13 | video: string;
14 | startTime: string;
15 | tags: string[];
16 | title: string;
17 | }
18 |
--------------------------------------------------------------------------------
/src/screens/Edit/utils.ts:
--------------------------------------------------------------------------------
1 | import { IVideoClip } from "../../components";
2 | import { IFormSuccess } from "./type";
3 |
4 | export const validRequired = (value: string = ""): boolean => {
5 | return !!value.trim().length;
6 | };
7 |
8 | export const validTime = (value: string = ""): boolean => {
9 | return value.replace(/\s|\_/g, '').length - 2 === 6;
10 | };
11 |
12 | export const getTime = (value: string = ""): number => {
13 | const hmsArray = value.split(":");
14 | return +hmsArray[0] * 60 * 60 + +hmsArray[1] * 60 + +hmsArray[2];
15 | };
16 |
17 | export const validImageURL = (url: string = ""): boolean => {
18 | return url.match(/\.(jpeg|jpg|gif|png)/) !== null;
19 | };
20 |
21 | export const validVideoURL = (url: string = ""): boolean => {
22 | return url.match(/\.mp4/) != null;
23 | };
24 |
25 | export const parseFormtoPlaylist = (value: IFormSuccess): IVideoClip => {
26 | return {
27 | endTime: getTime(value.endTime),
28 | id: String(Date.now()),
29 | image: value.image,
30 | src: value.video,
31 | startTime: getTime(value.startTime),
32 | tags: value.tags,
33 | title: value.title
34 | };
35 | };
36 |
--------------------------------------------------------------------------------
/src/screens/FullPlayer/FullPlayer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { RouteComponentProps } from "react-router-dom";
3 | import {
4 | FullVideo,
5 | VideoPlayer,
6 | Playlist,
7 | VideoClipContainer,
8 | ContentFullStyled,
9 | PageStyled,
10 | LayoutFullVideo
11 | } from "../../components";
12 |
13 | import AppProvider, { IAppProvider } from "../../AppProvider";
14 | import { isMobile } from "../../utils";
15 |
16 | interface IRouterProps {
17 | id: string;
18 | }
19 |
20 | interface IProps extends IAppProvider, RouteComponentProps {}
21 |
22 | class FullPlayer extends React.Component {
23 | public componentWillMount() {
24 | document.body.scrollTop = 0;
25 | document.documentElement.scrollTop = 0;
26 | this.props.setInFullPlayer(true);
27 | this.props.setIdVideo(this.props.match.params.id);
28 | }
29 |
30 | public componentWillReceiveProps(nextProps: IProps) {
31 | const idVideo = this.props.match.params.id;
32 | const newIdVideo = nextProps.match.params.id;
33 |
34 | if (idVideo !== newIdVideo) {
35 | this.props.setIdVideo(newIdVideo);
36 | }
37 | }
38 |
39 | public componentWillUnmount() {
40 | this.props.setInFullPlayer(false);
41 | }
42 |
43 | public handleEdit = (idSelected: string) => {
44 | this.props.history.push("/edit/" + idSelected);
45 | };
46 |
47 | public handleUpdateSelected = (idSelected: string) => {
48 | this.props.history.push("/video/" + idSelected);
49 | };
50 |
51 | public render() {
52 | const routerParams = this.props.match.params;
53 |
54 | return (
55 |
56 |
57 |
70 | {isMobile ? : }
71 |
72 |
73 | {idVideoClip => (
74 |
75 | {routerParams.id !== idVideoClip && (
76 |
84 | )}
85 |
88 |
89 | )}
90 |
91 |
92 |
93 |
94 |
95 | );
96 | }
97 | }
98 |
99 | export default (props: RouteComponentProps) => (
100 |
101 | {(value: IAppProvider) => }
102 |
103 | );
104 |
--------------------------------------------------------------------------------
/src/screens/Home/Home.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import {
3 | VideoItem,
4 | SidebarStyled,
5 | ContentDinamicStyled,
6 | VideoItemListStyled,
7 | PageWithSidebarStyled
8 | } from "../../components";
9 |
10 | import AppProvider, { IAppProvider } from "../../AppProvider";
11 |
12 | interface IProps extends IAppProvider {}
13 |
14 | const Home: React.SFC = props => {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | {props.playlist.map((videoClip, index) => (
22 |
23 | ))}
24 |
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default () => (
32 |
33 | {(value: IAppProvider) => }
34 |
35 | );
36 |
--------------------------------------------------------------------------------
/src/screens/Search/Search.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { RouteComponentProps } from "react-router-dom";
3 | import {
4 | SidebarStyled,
5 | ContentDinamicStyled,
6 | PageWithSidebarStyled,
7 | Playlist
8 | } from "../../components";
9 |
10 | import AppProvider, { IAppProvider } from "../../AppProvider";
11 |
12 | interface IRouterProps {
13 | value: string;
14 | }
15 |
16 | interface IProps
17 | extends IAppProvider,
18 | RouteComponentProps {}
19 |
20 | const Search: React.SFC = props => {
21 | const value = props.match.params.value;
22 |
23 | const filteredPlaylist = props.playlist.filter(videoClip => {
24 | return videoClip.title.toLowerCase().indexOf(value.toLowerCase()) > -1;
25 | });
26 |
27 | const handleClickVideo = (id: string) => {
28 | props.history.push("/video/" + id);
29 | };
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default (props: RouteComponentProps) => (
48 |
49 | {(value: IAppProvider) => }
50 |
51 | );
52 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: Roboto, sans-serif;
5 | background: #fafafa;
6 | }
7 |
8 | html {
9 | height: 100%;
10 | }
11 |
12 | a {
13 | text-decoration: none;
14 | }
15 |
16 | #root {
17 | height: auto;
18 | min-height: 100%;
19 | }
20 |
21 | video::-webkit-media-controls-panel,
22 | video::-webkit-media-controls-panel-container,
23 | video::-webkit-media-controls-start-playback-button {
24 | display:none !important;
25 | -webkit-appearance: none;
26 | }
27 |
28 | canvas,
29 | caption,
30 | center,
31 | cite,
32 | code,
33 | dd,
34 | del,
35 | dfn,
36 | div,
37 | dl,
38 | dt,
39 | em,
40 | embed,
41 | fieldset,
42 | font,
43 | form {
44 | margin: 0;
45 | padding: 0;
46 | border: 0;
47 | font-size: 100%;
48 | background: transparent;
49 | }
50 |
51 | button {
52 | background: transparent;
53 | border: 0;
54 | cursor: pointer;
55 | outline: none;
56 | }
57 |
58 | .rc-switch {
59 | position: relative;
60 | display: inline-block;
61 | box-sizing: border-box;
62 | width: 44px;
63 | height: 22px;
64 | line-height: 20px;
65 | vertical-align: middle;
66 | border-radius: 20px 20px;
67 | border: 1px solid #ccc;
68 | background-color: #ccc;
69 | cursor: pointer;
70 | transition: all 0.3s cubic-bezier(0.35, 0, 0.25, 1);
71 | }
72 |
73 | .rc-switch-inner {
74 | color: #fff;
75 | font-size: 12px;
76 | position: absolute;
77 | left: 24px;
78 | }
79 |
80 | .rc-switch:after {
81 | position: absolute;
82 | width: 18px;
83 | height: 18px;
84 | left: 2px;
85 | top: 1px;
86 | border-radius: 50% 50%;
87 | background-color: #fff;
88 | content: " ";
89 | cursor: pointer;
90 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26);
91 | transform: scale(1);
92 | transition: left 0.3s cubic-bezier(0.35, 0, 0.25, 1);
93 | animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1);
94 | animation-duration: 0.3s;
95 | animation-name: rcSwitchOff;
96 | }
97 |
98 | .rc-switch:hover:after {
99 | transform: scale(1.1);
100 | animation-name: rcSwitchOn;
101 | }
102 |
103 | .rc-switch:focus {
104 | box-shadow: 0 0 0 2px #d5f1fd;
105 | outline: none;
106 | }
107 |
108 | .rc-switch-checked {
109 | border: 1px solid #35a9ff;
110 | background-color: #35a9ff;
111 | }
112 |
113 | .rc-switch-checked .rc-switch-inner {
114 | left: 6px;
115 | }
116 |
117 | .rc-switch-checked:after {
118 | left: 22px;
119 | }
120 |
121 | .rc-switch-disabled {
122 | cursor: no-drop;
123 | background: #ccc;
124 | border-color: #ccc;
125 | }
126 |
127 | .rc-switch-disabled:after {
128 | background: #9e9e9e;
129 | animation-name: none;
130 | cursor: no-drop;
131 | }
132 |
133 | .rc-switch-disabled:hover:after {
134 | transform: scale(1);
135 | animation-name: none;
136 | }
137 |
138 | .rc-switch-label {
139 | display: inline-block;
140 | line-height: 20px;
141 | font-size: 14px;
142 | padding-left: 10px;
143 | vertical-align: middle;
144 | white-space: normal;
145 | pointer-events: none;
146 | user-select: text;
147 | }
148 |
149 | @keyframes rcSwitchOn {
150 | 0% {
151 | transform: scale(1);
152 | }
153 | 50% {
154 | transform: scale(1.25);
155 | }
156 | 100% {
157 | transform: scale(1.1);
158 | }
159 | }
160 |
161 | @keyframes rcSwitchOff {
162 | 0% {
163 | transform: scale(1.1);
164 | }
165 | 100% {
166 | transform: scale(1);
167 | }
168 | }
169 |
170 | .mark {
171 | position: absolute;
172 | top: 0;
173 | }
174 |
175 | .mark .rc-slider-track {
176 | display: none;
177 | }
178 |
179 | .mark.mark-start .rc-slider-track {
180 | display: none;
181 | background-color: #a0a0a0;
182 | }
183 |
184 | .mark .rc-slider-rail {
185 | display: none;
186 | }
187 |
188 | .mark .rc-slider-handle {
189 | border-radius: 0;
190 | border: 0 !important;
191 | width: 2px;
192 | margin-left: -2px;
193 | background: #faff00;
194 | }
195 |
196 | .mark-progress,
197 | .mark-volume,
198 | .mark-fragment {
199 | cursor: pointer;
200 | }
201 |
202 | .mark-progress .rc-slider-rail,
203 | .mark-volume .rc-slider-rail,
204 | .mark-fragment .rc-slider-rail {
205 | background-color: #e9e9e93d;
206 | }
207 |
208 | .mark-progress .rc-slider-track {
209 | background-color: #35a9ff;
210 | }
211 |
212 | .mark-progress .rc-slider-handle {
213 | background-color: #35a9ff;
214 | border-color: #35a9ff;
215 | width: 0;
216 | margin-top: 0px;
217 | height: 0px;
218 | transition: box-shadow 0.2s;
219 | box-shadow: 0 0 0 0px #35a9ff;
220 | }
221 |
222 | .mark-progress:hover .rc-slider-handle {
223 | box-shadow: 0 0 0 5px #35a9ff;
224 | }
225 |
226 | .mark-volume .rc-slider-track {
227 | background-color: #f5f5f5;
228 | }
229 |
230 | .mark-volume .rc-slider-handle {
231 | background-color: #f5f5f5;
232 | border-color: #f5f5f5;
233 | box-shadow: 0 0;
234 | }
235 |
236 | .mark-fragment .rc-slider-track {
237 | background-color: #faff00;
238 | }
239 |
240 | .rc-slider-handle {
241 | cursor: pointer;
242 | }
243 |
244 | .mark-fragment .rc-slider-handle {
245 | background-color: #faff00;
246 | border-color: #faff00;
247 | width: 0;
248 | margin-left: -2px;
249 | margin-top: 0px;
250 | height: 0px;
251 | transition: box-shadow 0.2s;
252 | box-shadow: 0 0 0 5px #faff00;
253 | }
254 |
255 | .mark-fragment:hover .rc-slider-handle {
256 | box-shadow: 0 0 0 5px #faff00;
257 | }
258 |
259 | .rc-slider-tooltip {
260 | padding: 0px 0 4px 0;
261 | margin-left: -3px;
262 | z-index: 2;
263 | }
264 |
265 | .rc-slider-tooltip-inner {
266 | padding: 6px 5px;
267 | border-radius: 2px;
268 | background-color: rgba(35, 35, 36);
269 | box-shadow: 0 0 0;
270 | }
271 |
272 | .rc-slider-tooltip-arrow {
273 | display: none;
274 | }
275 |
276 | @-webkit-keyframes spinner-linspin {
277 | to {
278 | -webkit-transform: rotate(360deg)
279 | }
280 | }
281 |
282 | @keyframes spinner-linspin {
283 | to {
284 | transform: rotate(360deg)
285 | }
286 | }
287 |
288 | @-webkit-keyframes spinner-easespin {
289 | 12.5% {
290 | -webkit-transform: rotate(135deg)
291 | }
292 | 25% {
293 | -webkit-transform: rotate(270deg)
294 | }
295 | 37.5% {
296 | -webkit-transform: rotate(405deg)
297 | }
298 | 50% {
299 | -webkit-transform: rotate(540deg)
300 | }
301 | 62.5% {
302 | -webkit-transform: rotate(675deg)
303 | }
304 | 75% {
305 | -webkit-transform: rotate(810deg)
306 | }
307 | 87.5% {
308 | -webkit-transform: rotate(945deg)
309 | }
310 | to {
311 | -webkit-transform: rotate(1080deg)
312 | }
313 | }
314 |
315 | @keyframes spinner-easespin {
316 | 12.5% {
317 | transform: rotate(135deg)
318 | }
319 | 25% {
320 | transform: rotate(270deg)
321 | }
322 | 37.5% {
323 | transform: rotate(405deg)
324 | }
325 | 50% {
326 | transform: rotate(540deg)
327 | }
328 | 62.5% {
329 | transform: rotate(675deg)
330 | }
331 | 75% {
332 | transform: rotate(810deg)
333 | }
334 | 87.5% {
335 | transform: rotate(945deg)
336 | }
337 | to {
338 | transform: rotate(1080deg)
339 | }
340 | }
341 |
342 | @-webkit-keyframes spinner-left-spin {
343 | 0% {
344 | -webkit-transform: rotate(130deg)
345 | }
346 | 50% {
347 | -webkit-transform: rotate(-5deg)
348 | }
349 | to {
350 | -webkit-transform: rotate(130deg)
351 | }
352 | }
353 |
354 | @keyframes spinner-left-spin {
355 | 0% {
356 | transform: rotate(130deg)
357 | }
358 | 50% {
359 | transform: rotate(-5deg)
360 | }
361 | to {
362 | transform: rotate(130deg)
363 | }
364 | }
365 |
366 | @-webkit-keyframes right-spin {
367 | 0% {
368 | -webkit-transform: rotate(-130deg)
369 | }
370 | 50% {
371 | -webkit-transform: rotate(5deg)
372 | }
373 | to {
374 | -webkit-transform: rotate(-130deg)
375 | }
376 | }
377 |
378 | @keyframes right-spin {
379 | 0% {
380 | transform: rotate(-130deg)
381 | }
382 | 50% {
383 | transform: rotate(5deg)
384 | }
385 | to {
386 | transform: rotate(-130deg)
387 | }
388 | }
389 |
390 | .fill-up {
391 | animation: fillup-spin 3.5s;
392 | }
393 |
394 | @keyframes fillup-spin {
395 | to {
396 | stroke-dashoffset: -422;
397 | }
398 | }
--------------------------------------------------------------------------------
/src/theme/index.ts:
--------------------------------------------------------------------------------
1 | import * as styledComponents from "styled-components";
2 |
3 | const {
4 | default: styled,
5 | css,
6 | injectGlobal,
7 | keyframes,
8 | ThemeProvider
9 | } = styledComponents as styledComponents.ThemedStyledComponentsModule<
10 | IThemeInterface
11 | >;
12 |
13 | // tslint:disable:object-literal-sort-keys
14 | export const size = {
15 | mobileS: "320px",
16 | mobileM: "375px",
17 | mobileL: "425px",
18 | tablet: "768px",
19 | laptop: "1024px",
20 | laptopS: "1300px",
21 | laptopL: "1440px",
22 | laptopM: "1600px",
23 | desktop: "2560px"
24 | };
25 |
26 | export const device = {
27 | mobileS: `(max-width: ${size.mobileS})`,
28 | mobileM: `(max-width: ${size.mobileM})`,
29 | mobileL: `(max-width: ${size.mobileL})`,
30 | tablet: `(max-width: ${size.tablet})`,
31 | laptop: `(max-width: ${size.laptop})`,
32 | laptopS: `(max-width: ${size.laptopS})`,
33 | laptopM: `(max-width: ${size.laptopM})`,
34 | laptopL: `(max-width: ${size.laptopL})`,
35 | desktop: `(max-width: ${size.desktop})`,
36 | desktopL: `(max-width: ${size.desktop})`
37 | };
38 |
39 | export interface IThemeInterface {
40 | velocityTransition: string;
41 | activeColor: string;
42 | backgroundColor: string;
43 | darkColor: string;
44 | defaultColor: string;
45 | grayColor: string;
46 | grayDarkColor: string;
47 | grayDarkFocusColor: string;
48 | grayLightColor: string;
49 | primaryColor: string;
50 | }
51 |
52 | export const theme = {
53 | velocityTransition: ".2s",
54 | activeColor: "#35a9ff",
55 | backgroundColor: "#fafafa",
56 | darkHeader: "#333333",
57 | darkColor: "#878687",
58 | defaultColor: "#161616",
59 | grayColor: "#eeeeee",
60 | grayDarkColor: "#e6e6e6",
61 | grayDarkFocusColor: "#e2e2e2",
62 | grayLightColor: "#f5f5f5",
63 | primaryColor: "#35a9ff"
64 | };
65 |
66 | export default styled;
67 | export { css, injectGlobal, keyframes, ThemeProvider };
68 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { IVideoClip } from "../components/types";
2 |
3 | export const isiOS =
4 | !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform);
5 |
6 | export const isMobile = window.innerWidth <= 768;
7 |
8 | export const showSpinnerNextVideo = (
9 | currentTime,
10 | endTime,
11 | duration,
12 | autoPlaylist
13 | ) => {
14 | const fixEndTime = endTime > duration ? duration : endTime;
15 | return currentTime >= fixEndTime && duration !== 0 && autoPlaylist;
16 | };
17 |
18 | export const findVideoClipIndexForId = (
19 | playlist: IVideoClip[],
20 | id: string
21 | ): any => {
22 | for (let i = 0; i < playlist.length; i++) {
23 | if (playlist[i].id === id) {
24 | return i;
25 | }
26 | }
27 | return null;
28 | };
29 |
30 | export const findVideoClipForId = (playlist: IVideoClip[], id): IVideoClip => {
31 | return playlist[findVideoClipIndexForId(playlist, id)];
32 | };
33 |
34 | export const isTypeEqual = (component1, component2) => {
35 | const type1 = component1.type;
36 | const type2 = component2.type;
37 |
38 | if (typeof type1 === "string" || typeof type2 === "string") {
39 | return type1 === type2;
40 | }
41 |
42 | if (typeof type1 === "function") {
43 | return type1 === component2;
44 | }
45 |
46 | return false;
47 | };
48 |
49 | export function formatTime(secondsParam = 0, guide = secondsParam) {
50 | let seconds: any = Math.floor(secondsParam % 60);
51 | let minutes: any = Math.floor((secondsParam / 60) % 60);
52 | let hours: any = Math.floor(secondsParam / 3600);
53 | const guideMinutes = Math.floor((guide / 60) % 60);
54 | const guideHours = Math.floor(guide / 3600);
55 |
56 | if (isNaN(secondsParam) || secondsParam === Infinity) {
57 | hours = minutes = seconds = "-";
58 | }
59 |
60 | hours =
61 | hours > 0 || guideHours > 0
62 | ? hours < 10
63 | ? `0${hours}:`
64 | : `${hours}:`
65 | : "";
66 |
67 | minutes = `${
68 | (hours || guideMinutes >= 10) && minutes < 10 ? `0${minutes}` : minutes
69 | }:`;
70 |
71 | seconds = seconds < 10 ? `0${seconds}` : seconds;
72 |
73 | return hours + minutes + seconds;
74 | }
75 |
76 | export const arrayShuffle = array => {
77 | let currentIndex = array.length;
78 | let temporaryValue;
79 | let randomIndex;
80 |
81 | while (0 !== currentIndex) {
82 | randomIndex = Math.floor(Math.random() * currentIndex);
83 | currentIndex -= 1;
84 |
85 | temporaryValue = array[currentIndex];
86 | array[currentIndex] = array[randomIndex];
87 | array[randomIndex] = temporaryValue;
88 | }
89 |
90 | return array;
91 | };
92 |
93 | export function setStorage(key, value) {
94 | if (key && typeof value !== "undefined") {
95 | window.localStorage[key] = JSON.stringify(value);
96 | }
97 | }
98 |
99 | export function getStorage(key) {
100 | const value = window.localStorage[key];
101 |
102 | return value ? JSON.parse(value) : undefined;
103 | }
104 |
105 | export function cleanDeprecatedStorage(name, version) {
106 | Object.keys(window.localStorage)
107 | .filter(
108 | keyStore =>
109 | keyStore.indexOf(name) > -1 && keyStore.indexOf(version) === -1
110 | )
111 | .forEach((key: any) => {
112 | delete window.localStorage[key];
113 | });
114 | }
115 |
116 | export function copy(ob: any) {
117 | return JSON.parse(JSON.stringify(ob));
118 | }
119 |
120 | export function convertToTimeRange(time, duration) {
121 | if (isNaN(duration) || duration === 0) {
122 | return 0;
123 | }
124 | return (Math.round(time) * 1000) / Math.round(duration);
125 | }
126 |
127 | export function convertToTime(time, duration = 0) {
128 | return Math.round((Math.round(time) * Math.round(duration)) / 1000);
129 | }
130 |
131 | export const formatTooltipRange = (duration: number) => time => {
132 | return formatTime(convertToTime(time, duration));
133 | };
134 |
135 | export const getRandomArray = (arr: any[]) => {
136 | return Math.floor(Math.random() * arr.length);
137 | };
138 |
139 | export const getPlaylistActions = (
140 | index: number,
141 | playlist: IVideoClip[],
142 | repeat: boolean,
143 | random: boolean
144 | ): {
145 | currentVideoClip?: IVideoClip;
146 | backVideoClip?: IVideoClip;
147 | nextVideoClip?: IVideoClip;
148 | } => {
149 | let nextVideoClip;
150 | let backVideoClip;
151 | let currentVideoClip;
152 |
153 | if (index !== null) {
154 | if (random) {
155 | const newPlaylist = playlist.filter(
156 | (videoClip, indexVideo) => index !== indexVideo
157 | );
158 |
159 | nextVideoClip = newPlaylist[getRandomArray(newPlaylist)];
160 | } else {
161 | nextVideoClip = playlist[index + 1];
162 | backVideoClip = playlist[index - 1];
163 |
164 | if (repeat && !nextVideoClip) {
165 | nextVideoClip = playlist[0];
166 | }
167 | }
168 |
169 | currentVideoClip = playlist[index];
170 | return {
171 | backVideoClip,
172 | currentVideoClip,
173 | nextVideoClip
174 | };
175 | }
176 | return {};
177 | };
178 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "outDir": "build/dist",
5 | "module": "esnext",
6 | "target": "es5",
7 | "lib": ["es6", "dom"],
8 | "sourceMap": true,
9 | "allowJs": true,
10 | "jsx": "react",
11 | "moduleResolution": "node",
12 | "rootDir": "src",
13 | "forceConsistentCasingInFileNames": true,
14 | "noImplicitReturns": true,
15 | "noImplicitThis": true,
16 | "noImplicitAny": false,
17 | "strictNullChecks": true,
18 | "suppressImplicitAnyIndexErrors": true,
19 | "noUnusedLocals": false
20 | },
21 | "exclude": [
22 | "node_modules",
23 | "build",
24 | "scripts",
25 | "acceptance-tests",
26 | "webpack",
27 | "jest",
28 | "src/setupTests.ts"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json"
3 | }
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
3 | "compilerOptions": {
4 | "typeRoots": [
5 | "../node_modules/@types"
6 | ]
7 | },
8 | "rules": {
9 | "no-unused-variable": false,
10 | "no-empty-interface": false,
11 | "ordered-imports": false
12 | },
13 | "linterOptions": {
14 | "exclude": [
15 | "config/**/*.js",
16 | "node_modules/**/*.ts"
17 | ]
18 | }
19 | }
--------------------------------------------------------------------------------