├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------