├── .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 | drawing 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 | ![](https://github.com/rafaesc/reactube-client/blob/master/screenshots/fullplayer1.gif?raw=true) 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 | ![](https://github.com/rafaesc/reactube-client/blob/master/screenshots/mobile.gif?raw=true) 49 | ___ 50 | 51 | :scissors: Crop videos 52 | 53 | ![](https://github.com/rafaesc/reactube-client/blob/master/screenshots/fullplayer2.gif?raw=true) 54 | ___ 55 | 56 | :house: Homepage 57 | 58 | ![](https://github.com/rafaesc/reactube-client/blob/master/screenshots/home.png?raw=true) 59 | ___ 60 | 61 | :tv: Video preview 62 | 63 | ![](https://github.com/rafaesc/reactube-client/blob/master/screenshots/video-preview.png?raw=true) 64 | ___ 65 | 66 | :pencil2: Edit video 67 | 68 | ![](https://github.com/rafaesc/reactube-client/blob/master/screenshots/edit.png?raw=true) 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 | 6 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 31 | 34 | 38 | 41 | 42 | 45 | 46 | 51 | 52 | 53 | 54 | 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 | 78 | 79 | 91 | 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 |