├── .babelrc ├── .gitignore ├── README.md ├── app ├── bootstrap.js ├── browser │ ├── main.js │ └── playlist.js └── client │ ├── components │ ├── application.js │ ├── currently_playing.js │ ├── playback_control_bar.js │ ├── playlist.js │ ├── youtube_player.js │ └── youtube_thumbnail.js │ ├── index.html │ ├── main.js │ ├── redux │ ├── actions.js │ └── reducers.js │ ├── utils.js │ └── youtube_api.js ├── mocks ├── mock.bmml └── mock.png └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | // Don't inherit from higher-level .babelrc files 3 | "breakConfig": true, 4 | "stage": 2, 5 | "optional": [ 6 | "es7.decorators" 7 | ], 8 | "env": { 9 | "production": { 10 | "optional": [ 11 | "optimisation.react.constantElements", 12 | "optimisation.react.inlineElements" 13 | // "minification.removeConsole" // removes `console.log` lines 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Electron YouTube Player 2 | ======================= 3 | 4 | This is a desktop app created using [Electron](http://electron.atom.io/), written using React and Redux. It manages playlists of YouTube videos, and plays them in various ways. 5 | 6 | Installing/Running 7 | ------------------ 8 | 9 | Requires Node.js. Tested on Node.js 4.0. 10 | 11 | ``` 12 | npm install 13 | npm start 14 | ``` 15 | 16 | Mock 17 | ---- 18 | 19 | ![Mockup](mocks/mock.png) 20 | -------------------------------------------------------------------------------- /app/bootstrap.js: -------------------------------------------------------------------------------- 1 | require("babel/register"); 2 | require("./browser/main"); 3 | -------------------------------------------------------------------------------- /app/browser/main.js: -------------------------------------------------------------------------------- 1 | import app from "app"; 2 | import BrowserWindow from "browser-window"; 3 | 4 | let mainWindow = null; 5 | 6 | const launchMainWindow = () => { 7 | mainWindow = new BrowserWindow({width: 800, height: 600}); 8 | mainWindow.maximize(); 9 | mainWindow.loadUrl(`file://${__dirname}/../client/index.html`); 10 | // mainWindow.openDevTools(); 11 | mainWindow.on('closed', () => mainWindow = null); 12 | }; 13 | 14 | app.on('window-all-closed', () => app.quit()); 15 | app.on('ready', launchMainWindow); 16 | -------------------------------------------------------------------------------- /app/browser/playlist.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | import app from "app"; 5 | 6 | const appDataFolder = path.join(app.getPath("userData")); 7 | const appDataFile = path.join(appDataFolder, "playlist.json"); 8 | 9 | export const savePlaylist = (videoIds) => { 10 | console.log("saving to", appDataFolder); 11 | try { 12 | fs.mkdirSync(appDataFolder); 13 | } catch(e) {} 14 | 15 | const data = JSON.stringify(videoIds); 16 | fs.writeFileSync(appDataFile, data); 17 | }; 18 | 19 | export const loadPlaylist = (callback) => { 20 | try { 21 | const buffer = fs.readFileSync(appDataFile); 22 | const data = buffer.toString("utf8"); 23 | const json = JSON.parse(data); 24 | callback(json); 25 | } catch (e) { 26 | console.log("No playlist to load!"); 27 | callback([]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/client/components/application.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { autobind } from "core-decorators"; 3 | import { connect } from "react-redux"; 4 | 5 | import * as actions from "../redux/actions"; 6 | 7 | import CurrentlyPlaying from "./currently_playing"; 8 | import PlaybackControlBar from "./playback_control_bar"; 9 | import Playlist from "./playlist"; 10 | import YoutubePlayer from "./youtube_player"; 11 | import YoutubeThumbnail from "./youtube_thumbnail"; 12 | 13 | 14 | @connect(state => ({ 15 | ...state, 16 | isPlaying: state.playbackStatus === "PLAYING", 17 | currentVideoDuration: state.videoDurations[state.currentVideoId] || 0 18 | }), actions) 19 | export default class Application extends React.Component { 20 | constructor(props) { 21 | super(props); 22 | window.props = props; 23 | this.state = { 24 | currentTime: 0 25 | }; 26 | } 27 | 28 | render() { 29 | return ( 30 |
31 |
32 | 37 | 38 |
39 |
40 | 47 | 52 |
53 |
54 | ); 55 | } 56 | 57 | @autobind 58 | playVideo() { 59 | this.refs.player.play(); 60 | } 61 | 62 | @autobind 63 | pauseVideo() { 64 | this.refs.player.pause(); 65 | } 66 | 67 | @autobind 68 | seekVideo(time, allowSeekAhead = false) { 69 | this.refs.player.seek(time, allowSeekAhead); 70 | this.updateCurrentTime(time); 71 | } 72 | 73 | @autobind 74 | setVolume(volume) { 75 | this.refs.player.setVolume(volume); 76 | } 77 | 78 | @autobind 79 | updateCurrentTime(time) { 80 | this.setState({ currentTime: time }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/client/components/currently_playing.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import YoutubeThumbnail from "./youtube_thumbnail"; 4 | 5 | export default class CurrentlyPlaying extends React.Component { 6 | render() { 7 | return ( 8 |
9 | {this.props.videoId && this.renderThumbnail(this.props.videoId)} 10 |
11 | ); 12 | } 13 | 14 | renderThumbnail(videoId) { 15 | return ; 17 | } 18 | } 19 | 20 | CurrentlyPlaying.propTypes = { 21 | videoId: React.PropTypes.string, 22 | }; 23 | -------------------------------------------------------------------------------- /app/client/components/playback_control_bar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { autobind } from "core-decorators"; 3 | 4 | import { secondsDisplay } from "../utils"; 5 | 6 | 7 | export default class PlaybackControlBar extends React.Component { 8 | constructor() { 9 | super(); 10 | this.state = { 11 | draggingSeekSlider: false, 12 | localCurrentTime: 0, 13 | }; 14 | } 15 | 16 | render() { 17 | const seekSliderValue = this.state.draggingSeekSlider ? this.state.localCurrentTime : this.props.currentTime; 18 | 19 | return ( 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | 35 |
36 |
37 | 40 |
41 |
42 | ); 43 | } 44 | 45 | @autobind 46 | handleMouseDown() { 47 | this.setState({ draggingSeekSlider: true }); 48 | } 49 | 50 | @autobind 51 | handleMouseUp(evt) { 52 | this.setState({ draggingSeekSlider: false }); 53 | const time = parseInt(evt.target.value, 10); 54 | this.setState({ localCurrentTime: time }); 55 | this.props.onSeek(time, true); 56 | } 57 | 58 | @autobind 59 | handleSeekVideo(evt) { 60 | const time = parseInt(evt.target.value, 10); 61 | this.setState({ localCurrentTime: time }); 62 | this.props.onSeek(time, this.state.draggingSeekSlider ? false : true); 63 | } 64 | 65 | @autobind 66 | handleSetVolume(evt) { 67 | const volume = parseInt(evt.target.value, 10); 68 | this.props.onSetVolume(volume); 69 | } 70 | } 71 | 72 | PlaybackControlBar.propTypes = { 73 | videoDuration: React.PropTypes.number, 74 | currentTime: React.PropTypes.number, 75 | playing: React.PropTypes.bool.isRequired, 76 | onSeek: React.PropTypes.func, 77 | onPlay: React.PropTypes.func, 78 | onPause: React.PropTypes.func, 79 | }; 80 | 81 | PlaybackControlBar.defaultProps = { 82 | videoDuration: 0, 83 | currentTime: 0, 84 | playing: false, 85 | onSeek: () => null, 86 | onPlay: () => null, 87 | onPause: () => null, 88 | }; 89 | -------------------------------------------------------------------------------- /app/client/components/playlist.js: -------------------------------------------------------------------------------- 1 | import url from "url"; 2 | import qs from "querystring"; 3 | 4 | import React from "react"; 5 | import { autobind } from "core-decorators"; 6 | 7 | import YoutubeThumbnail from "./youtube_thumbnail"; 8 | 9 | const closeButtonStyle = { 10 | position: "absolute", 11 | top: 5, 12 | right: 5, 13 | color: "white", 14 | backgroundColor: "black", 15 | fontSize: 25, 16 | padding: "5px 10px", 17 | cursor: "pointer", 18 | }; 19 | 20 | const ClickableThumbnail = ({videoId, onClick, onClose}) => 21 |
22 | onClick(videoId)} /> 25 | onClose(evt, videoId)}>× 26 |
27 | 28 | 29 | 30 | class Modal extends React.Component { 31 | render() { 32 | const style = { 33 | color: "black", 34 | width: 400, 35 | backgroundColor: "white", 36 | position: "absolute", 37 | top: "30%", 38 | left: "50%", 39 | marginLeft: -200, 40 | border: "1px solid black", 41 | borderRadius: 10, 42 | padding: 15, 43 | }; 44 | 45 | const backdropStyle = { 46 | display: this.props.isOpen ? "block" : "none", 47 | position: "absolute", 48 | top: 0, 49 | left: 0, 50 | right: 0, 51 | bottom: 0, 52 | backgroundColor: "rgba(50,50,50,0.8)", 53 | }; 54 | 55 | let title; 56 | if (this.props.title) { 57 | const style = { 58 | marginTop: 0, 59 | borderBottom: "1px solid #ccc", 60 | marginBottom: 10, 61 | paddingBottom: 5, 62 | }; 63 | title =

{this.props.title}

; 64 | } 65 | 66 | const closeButton = ( 67 |
75 | × 76 |
77 | ); 78 | 79 | return ( 80 |
81 |
82 | {title} 83 | {closeButton} 84 | {this.props.children} 85 |
86 |
87 | ); 88 | } 89 | 90 | eatClick(e) { 91 | e.stopPropagation(); 92 | } 93 | } 94 | 95 | 96 | export default class Playlist extends React.Component { 97 | constructor() { 98 | super(); 99 | this.state = { 100 | showingAddModal: false, 101 | }; 102 | } 103 | 104 | render() { 105 | return ( 106 |
107 | {this.props.videoIds.map(this.renderVideoId)} 108 | 109 |
110 | 111 | 112 |
113 | 116 | 117 | {/*cheating*/} 118 | 119 | 120 |
121 | ); 122 | } 123 | 124 | @autobind 125 | renderVideoId(videoId) { 126 | return 129 | } 130 | 131 | @autobind 132 | handleShowAddVideoModalClicked() { 133 | this.setState({ showingAddModal: true }); 134 | } 135 | 136 | @autobind 137 | handleCloseModal() { 138 | this.setState({ showingAddModal: false }); 139 | } 140 | 141 | @autobind 142 | handleVideoClicked(videoId) { 143 | this.props.onSelectVideo(videoId); 144 | } 145 | 146 | @autobind 147 | handleVideoCloseClicked(evt, videoId) { 148 | evt.stopPropagation(); 149 | this.props.onRemoveVideo(videoId); 150 | } 151 | 152 | @autobind 153 | handleAddVideoClicked() { 154 | // TODO: get this into a managed input, 155 | // or a modal, etc. 156 | const videoUrl = this.refs.videoId.value; 157 | const parsed = url.parse(videoUrl); 158 | const queryParsed = qs.parse(parsed.query); 159 | if (queryParsed.v) { 160 | this.props.onAddVideo(queryParsed.v); 161 | this.refs.videoId.value = ""; 162 | } 163 | } 164 | 165 | @autobind 166 | handleRemoveVideo(videoId) { 167 | this.props.onRemoveVideo(videoId); 168 | } 169 | 170 | @autobind 171 | handleSavePlaylist() { 172 | require("remote").require("./browser/playlist").savePlaylist( 173 | this.props.videoIds 174 | ); 175 | } 176 | 177 | @autobind 178 | handleLoadPlaylist() { 179 | require("remote").require("./browser/playlist").loadPlaylist(videoIds => { 180 | console.log("got video IDs", videoIds); 181 | this.props.setPlaylistVideos(videoIds); 182 | }); 183 | } 184 | } 185 | 186 | Playlist.propTypes = { 187 | videoIds: React.PropTypes.arrayOf( 188 | React.PropTypes.string 189 | ).isRequired, 190 | onSelectVideo: React.PropTypes.func.isRequired, // fn(videoId) 191 | onAddVideo: React.PropTypes.func.isRequired, // fn(videoId) 192 | onRemoveVideo: React.PropTypes.func.isRequired, // fn(videoId) 193 | setPlaylistVideos: React.PropTypes.func.isRequired, // fn([videoId]) 194 | }; 195 | -------------------------------------------------------------------------------- /app/client/components/youtube_player.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { findDOMNode } from "react-dom"; 3 | import { autobind } from "core-decorators"; 4 | 5 | import loadApi from "../youtube_api"; 6 | 7 | export default class YoutubePlayer extends React.Component { 8 | // not as gross! yay 9 | componentDidMount() { 10 | this.setupPlayer(); 11 | this.updateTimeInterval = setInterval(this.updateCurrentPlayerTime, 100); 12 | } 13 | 14 | componentDidUpdate(prevProps) { 15 | if (this.props.videoId !== prevProps.videoId) { 16 | this.changeVideoId(this.props.videoId); 17 | } 18 | } 19 | 20 | componentWillUnmount() { 21 | this.updateTimeInterval && clearInterval(this.updateTimeInterval); 22 | this.player && this.player.destroy(); 23 | } 24 | 25 | async setupPlayer() { 26 | await loadApi(); 27 | 28 | if (this.player) { 29 | this.player.destroy(); 30 | } 31 | 32 | this.player = await this.createPlayer(this.props.videoId); 33 | } 34 | 35 | async changeVideoId(videoId) { 36 | await loadApi(); 37 | 38 | if (this.player) { 39 | this.player.loadVideoById(videoId); 40 | } 41 | } 42 | 43 | createPlayer(videoId) { 44 | const node = findDOMNode(this); 45 | const player = new YT.Player(node, { 46 | height: '100%', 47 | width: '100%', 48 | videoId: videoId, 49 | playerVars: { 'autoplay': this.props.autoplay, 'controls': 0 }, 50 | events: { 51 | onReady: (event) => { 52 | player.setVolume(this.props.initialVolume); 53 | this.props.onUpdateVideoDuration(this.props.videoId, player.getDuration()); 54 | }, 55 | onStateChange: (event) => { 56 | const state = player.getPlayerState(); 57 | const stateName = state === 1 ? "PLAYING" : "PAUSED"; 58 | this.props.onUpdatePlaybackStatus(stateName); 59 | 60 | if (state === 0) { 61 | // Video ended, go to the next one 62 | this.props.onVideoEnded(); 63 | } 64 | 65 | if (state === 1) { 66 | // video just started playing, we should have metadata 67 | this.props.onUpdateVideoDuration(this.props.videoId, player.getDuration()); 68 | } 69 | } 70 | } 71 | }); 72 | return player; 73 | } 74 | 75 | @autobind 76 | updateCurrentPlayerTime() { 77 | if (this.player && this.player.getCurrentTime) { 78 | const time = Math.floor(this.player.getCurrentTime() * 1000); 79 | this.props.onUpdateCurrentTime(time); 80 | } 81 | } 82 | 83 | render() { 84 | return ( 85 |
video
86 | ); 87 | } 88 | 89 | // imperative public API 90 | play() { 91 | this.player && this.player.playVideo(); 92 | } 93 | 94 | pause() { 95 | this.player && this.player.pauseVideo(); 96 | this.updateCurrentPlayerTime(); // make sure time display is correct right away 97 | } 98 | 99 | seek(time, allowSeekAhead = false) { 100 | this.player && this.player.seekTo(time / 1000, allowSeekAhead); 101 | } 102 | 103 | setVolume(volume) { 104 | this.player && this.player.setVolume(volume); 105 | this.props.onSetVolume(volume); 106 | } 107 | } 108 | 109 | YoutubePlayer.propTypes = { 110 | autoplay: React.PropTypes.bool, 111 | videoId: React.PropTypes.string.isRequired, 112 | onUpdateCurrentTime: React.PropTypes.func, // fn(time) 113 | onUpdatePlaybackStatus: React.PropTypes.func, // fn(status) 114 | onUpdateVideoDuration: React.PropTypes.func, // fn(videoId, duration) 115 | onVideoEnded: React.PropTypes.func, // fn() 116 | }; 117 | 118 | YoutubePlayer.defaultProps = { 119 | autoplay: false, 120 | onUpdateCurrentTime: () => null, 121 | onUpdatePlaybackStatus: () => null, 122 | onUpdateVideoDuration: () => null, 123 | onVideoEnded: () => null, 124 | }; 125 | -------------------------------------------------------------------------------- /app/client/components/youtube_thumbnail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { youtubeIdToThumbnailUrl } from "../utils"; 4 | 5 | export default class YoutubeThumbnail extends React.Component { 6 | render() { 7 | const { videoId, ...props } = this.props; 8 | 9 | return ; 10 | } 11 | } 12 | 13 | YoutubeThumbnail.propTypes = { 14 | videoId: React.PropTypes.string.isRequired, 15 | }; 16 | -------------------------------------------------------------------------------- /app/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 45 | 46 | 47 |
48 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/client/main.js: -------------------------------------------------------------------------------- 1 | process.on("unhandledRejection", (rej) => { 2 | console.error("Detected unhandled rejected promise:"); 3 | console.error(rej); 4 | }); 5 | 6 | import React from "react"; 7 | import ReactDOM from "react-dom"; 8 | 9 | import { compose, createStore, applyMiddleware } from "redux"; 10 | import { devTools, persistState } from 'redux-devtools'; 11 | import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react'; 12 | import { Provider } from "react-redux"; 13 | import reduceReducers from "reduce-reducers"; 14 | 15 | import { playlistReducer, playbackStateReducer } from "./redux/reducers"; 16 | 17 | import Application from "./components/application"; 18 | 19 | 20 | const rootReducer = reduceReducers( 21 | playlistReducer, 22 | playbackStateReducer 23 | ); 24 | 25 | const finalCreateStore = compose( 26 | devTools() 27 | )(createStore); 28 | 29 | const store = finalCreateStore(rootReducer); 30 | 31 | const app = ( 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | ); 41 | 42 | ReactDOM.render(app, document.getElementById("app")); 43 | -------------------------------------------------------------------------------- /app/client/redux/actions.js: -------------------------------------------------------------------------------- 1 | function actionCreator(type, ...argNames) { 2 | return (...args) => { 3 | const action = argNames.reduce((acc, argName, idx) => { 4 | acc[argName] = args[idx]; 5 | return acc; 6 | }, { type: type }); 7 | return action; 8 | }; 9 | } 10 | 11 | export const selectVideo = actionCreator("SELECT_VIDEO", "videoId"); 12 | export const selectNextVideo = actionCreator("SELECT_NEXT_VIDEO"); 13 | export const setVolume = actionCreator("SET_VOLUME", "volume"); 14 | export const updateCurrentTime = actionCreator("UPDATE_CURRENT_TIME", "newTime"); 15 | export const updatePlaybackStatus = actionCreator("UPDATE_PLAYBACK_STATUS", "status"); 16 | export const updateVideoDuration = actionCreator("UPDATE_VIDEO_DURATION", "videoId", "duration"); 17 | export const addVideoToPlaylist = actionCreator("ADD_VIDEO", "videoId"); 18 | export const removeVideoFromPlaylist = actionCreator("REMOVE_VIDEO", "videoId"); 19 | export const setPlaylistVideos = actionCreator("SET_PLAYLIST_VIDEOS", "videoIds"); 20 | -------------------------------------------------------------------------------- /app/client/redux/reducers.js: -------------------------------------------------------------------------------- 1 | const savedVolume = parseInt(window.localStorage.getItem('initialVolume'), 10); 2 | 3 | const initialState = { 4 | playlist: [ 5 | // "VJdi9SDlVhU", 6 | // "h_aKALHPRmU", 7 | "trvnP7EsAHA" 8 | ], 9 | currentVideoId: "trvnP7EsAHA", 10 | playbackStatus: "PAUSED", 11 | videoDurations: {}, // id => duration 12 | volume: Number.isNaN(savedVolume) ? 100 : savedVolume, 13 | }; 14 | 15 | export function playlistReducer(state = initialState, action) { 16 | switch (action.type) { 17 | case "SELECT_VIDEO": 18 | return { 19 | ...state, 20 | currentVideoId: action.videoId 21 | }; 22 | case "SELECT_NEXT_VIDEO": 23 | const thisVideoId = state.currentVideoId; 24 | const index = state.playlist.indexOf(thisVideoId); 25 | let nextVideoIndex = index + 1; 26 | const numVideos = state.playlist.length; 27 | if (nextVideoIndex > numVideos - 1) { 28 | nextVideoIndex = 0; 29 | } 30 | const nextVideoId = state.playlist[nextVideoIndex]; 31 | 32 | return { 33 | ...state, 34 | currentVideoId: nextVideoId 35 | }; 36 | case "SET_VOLUME": 37 | window.localStorage.setItem('initialVolume', action.volume); 38 | return { 39 | ...state, 40 | volume: action.volume 41 | }; 42 | case "ADD_VIDEO": 43 | { 44 | if (state.playlist.includes(action.videoId)) { 45 | return state; 46 | } 47 | 48 | const newVideoId = state.currentVideoId || action.videoId; 49 | 50 | return { 51 | ...state, 52 | playlist: state.playlist.concat([action.videoId]), 53 | currentVideoId: newVideoId 54 | }; 55 | } 56 | case "REMOVE_VIDEO": 57 | const newPlaylist = state.playlist.filter(id => id !== action.videoId) 58 | let newVideoId = state.currentVideoId; 59 | if (state.currentVideoId === action.videoId) { 60 | // Current video is being removed. 61 | newVideoId = newPlaylist[0]; 62 | } 63 | return { 64 | ...state, 65 | playlist: newPlaylist, 66 | currentVideoId: newVideoId 67 | }; 68 | case "SET_PLAYLIST_VIDEOS": 69 | return { 70 | ...state, 71 | playlist: action.videoIds, 72 | currentVideoId: action.videoIds[0] 73 | }; 74 | default: 75 | return state; 76 | } 77 | } 78 | 79 | export function playbackStateReducer(state = initialState, action) { 80 | switch (action.type) { 81 | case "UPDATE_PLAYBACK_STATUS": 82 | return { 83 | ...state, 84 | playbackStatus: action.status 85 | }; 86 | case "UPDATE_VIDEO_DURATION": 87 | return { 88 | ...state, 89 | videoDurations: { 90 | ...state.videoDurations, 91 | [action.videoId]: action.duration 92 | } 93 | }; 94 | default: 95 | return state; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/client/utils.js: -------------------------------------------------------------------------------- 1 | export function youtubeIdToThumbnailUrl(videoId) { 2 | return `http://img.youtube.com/vi/${videoId}/0.jpg`; 3 | } 4 | 5 | export function secondsDisplay(seconds) { 6 | const minutes = Math.floor(seconds / 60); 7 | const remainSeconds = seconds % 60; 8 | 9 | let secondsStr = `${remainSeconds}`; 10 | if (secondsStr.length === 1) { 11 | secondsStr = "0" + secondsStr; 12 | } 13 | 14 | return `${minutes}:${secondsStr}`; 15 | } 16 | -------------------------------------------------------------------------------- /app/client/youtube_api.js: -------------------------------------------------------------------------------- 1 | let apiPromise; 2 | 3 | const api = () => { 4 | if (apiPromise) { 5 | return apiPromise; 6 | } 7 | 8 | apiPromise = new Promise(resolve => { 9 | window.onYouTubeIframeAPIReady = () => { 10 | console.log("YouTube API loaded"); 11 | resolve(); 12 | } 13 | 14 | const tag = document.createElement('script'); 15 | tag.src = "https://www.youtube.com/iframe_api"; 16 | const firstScriptTag = document.getElementsByTagName('script')[0]; 17 | firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 18 | }); 19 | 20 | return apiPromise; 21 | }; 22 | 23 | export default api; 24 | -------------------------------------------------------------------------------- /mocks/mock.bmml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 0 6 | true 7 | true 8 | true 9 | YouTube%20Player 10 | 11 | 12 | 13 | 14 | true 15 | http%3A//i.ytimg.com/vi/nxnx44MyxXA/maxresdefault.jpg 16 | 17 | 18 | 19 | 20 | 21 | 22 | 81 23 | 24 | 25 | 26 | 27 | 5%3A17 28 | 29 | 30 | 31 | 32 | 5%3A31 33 | 34 | 35 | 36 | 37 | 7%3A45 38 | 39 | 40 | 41 | 42 | 43 | 44 | 17 45 | 46 | 47 | 48 | 49 | true 50 | http%3A//i.ytimg.com/vi/_3GJMQa0J8g/maxresdefault.jpg 51 | 52 | 53 | 54 | 55 | 56 | Some%20video 57 | 58 | 59 | 60 | 61 | 1 62 | 14540253 63 | square 64 | 14540253 65 | 66 | 67 | 68 | 69 | 7%3A45 70 | 71 | 72 | 73 | 74 | true 75 | http%3A//i.ytimg.com/vi/nxnx44MyxXA/maxresdefault.jpg 76 | 77 | 78 | 79 | 80 | 81 | Some%20video 82 | 83 | 84 | 85 | 86 | 9%3A54 87 | 88 | 89 | 90 | 91 | true 92 | http%3A//i.ytimg.com/vi/PNsF7UnKABg/maxresdefault.jpg 93 | 94 | 95 | 96 | 97 | 98 | Some%20video 99 | 100 | 101 | 102 | 103 | Playlist%201 104 | 105 | 106 | 107 | 108 | PencilIcon%7Cxsmall 109 | 110 | 111 | 112 | 113 | 0.75 114 | 16777215 115 | none 116 | 117 | 118 | 119 | 120 | Some%20Video 121 | 122 | 123 | 124 | 125 | LinkIcon%7Csmall 126 | 127 | 128 | 129 | 130 | 3355443 131 | Some%20User 132 | 133 | 134 | 135 | 136 | PlusBigIcon%7Cxsmall 137 | Add%20Video 138 | 139 | 140 | 141 | 142 | square 143 | 0 144 | XIcon%7Cxsmall 145 | 146 | 147 | 148 | 149 | 1 150 | 151 | 152 | 153 | 154 | 1 155 | 156 | 157 | 158 | 159 | Playlist%20manager%20supports%20multiple%20playlists%0A%20%0ACan%20add/remove%20videos%2C%20search%20YouTube%2C%20import%20YouTube%20playlists%2C%20etc%0A%20%0APlaylists%20persisted%20locally%20to%20disk 160 | 161 | 162 | 163 | 164 | 2 165 | 166 | 167 | 168 | 169 | 2 170 | 171 | 172 | 173 | 174 | %22Currently%20Playing%22%20allows%20links%20to%20YouTube%20URLs%20for%20video%2C%20user%2C%20etc. 175 | 176 | 177 | 178 | 179 | 3 180 | 181 | 182 | 183 | 184 | 3 185 | 186 | 187 | 188 | 189 | YouTube%20controls%20hidden.%20Custom%20controls%20displayed%20at%20bottom. 190 | 191 | 192 | 193 | 194 | ShuffleIcon%7Csmall 195 | 196 | 197 | 198 | 199 | 10066329 200 | RepeatIcon%7Csmall 201 | 202 | 203 | 204 | 205 | 206 | 394%2C325%2C211%2C366 207 | true 208 | http%3A//www.carlduncker.com/wp-content/themes/wp_pinfinity5-v1.4/images/video-placeholder.png 209 | 210 | 211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /mocks/mock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/electron-youtube-player/6660be2796955be39f66fc5422f04a965d34f535/mocks/mock.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "youtube-player", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "electron ." 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "private": true, 13 | "dependencies": { 14 | "babel": "^5.8.23", 15 | "babel-core": "^5.8.25", 16 | "babel-loader": "^5.3.2", 17 | "babel-runtime": "^5.8.25", 18 | "core-decorators": "^0.4.1", 19 | "react": "^0.14.0-rc1", 20 | "react-dom": "^0.14.0-rc1", 21 | "react-redux": "^3.0.0", 22 | "reduce-reducers": "^0.1.1", 23 | "redux": "^3.0.2", 24 | "redux-devtools": "^2.1.4", 25 | "redux-thunk": "^1.0.0", 26 | "webpack": "^1.12.2" 27 | }, 28 | "devDependencies": { 29 | "babel-plugin-react-transform": "^1.1.1", 30 | "electron-prebuilt": "^0.33.3", 31 | "electron-rebuild": "^1.0.0", 32 | "react-transform-catch-errors": "^1.0.0", 33 | "react-transform-hmr": "^1.0.1", 34 | "redbox-react": "^1.1.1", 35 | "webpack-dev-server": "^1.12.0" 36 | }, 37 | "main": "app/bootstrap.js" 38 | } 39 | --------------------------------------------------------------------------------