├── app ├── img │ ├── icon512.ico │ ├── icon512.icns │ ├── iconMacTemplate.png │ ├── iconWinTemplate.png │ └── iconMacTemplate@2x.png ├── locale │ ├── en.json │ ├── ru.json │ └── uk.json ├── index.html ├── package.json ├── main.js └── src │ ├── menu.js │ ├── services.js │ └── window.js ├── resources ├── old │ ├── app-icon.png │ ├── app-icon.psd │ ├── icon256.icns │ ├── icon256.ico │ ├── app-icon.afphoto │ ├── iconTemplate.afphoto │ ├── iconWinTemplate.png │ └── iconTemplate.svg ├── chromecast-black.png ├── chromecast-app-icon.sketch ├── cast-icon-chromecast.svg └── chromecast-device-vector.svg ├── .eslintignore ├── clean.sh ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── .babelrc ├── .travis.yml ├── .editorconfig ├── jsconfig.json ├── appveyor.yml ├── .gitignore ├── typings └── react │ ├── react-addons-create-fragment.d.ts │ ├── react-addons-pure-render-mixin.d.ts │ ├── react-addons-transition-group.d.ts │ ├── react-global.d.ts │ ├── react-addons-linked-state-mixin.d.ts │ ├── react-addons-update.d.ts │ ├── react-addons-css-transition-group.d.ts │ ├── react-addons-perf.d.ts │ ├── react-dom.d.ts │ └── react-addons-test-utils.d.ts ├── LICENSE ├── jsx ├── index.jsx └── components │ ├── DevicesList.jsx │ ├── App.jsx │ └── Player.jsx ├── package.json ├── README.md ├── webpack.backend.js ├── .eslintrc.json └── gulpfile.js /app/img/icon512.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/app/img/icon512.ico -------------------------------------------------------------------------------- /app/img/icon512.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/app/img/icon512.icns -------------------------------------------------------------------------------- /resources/old/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/resources/old/app-icon.png -------------------------------------------------------------------------------- /resources/old/app-icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/resources/old/app-icon.psd -------------------------------------------------------------------------------- /resources/old/icon256.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/resources/old/icon256.icns -------------------------------------------------------------------------------- /resources/old/icon256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/resources/old/icon256.ico -------------------------------------------------------------------------------- /app/img/iconMacTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/app/img/iconMacTemplate.png -------------------------------------------------------------------------------- /app/img/iconWinTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/app/img/iconWinTemplate.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | app/js 3 | app/node_modules 4 | app/vendor 5 | build 6 | cache 7 | node_modules 8 | typings -------------------------------------------------------------------------------- /app/img/iconMacTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/app/img/iconMacTemplate@2x.png -------------------------------------------------------------------------------- /resources/chromecast-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/resources/chromecast-black.png -------------------------------------------------------------------------------- /resources/old/app-icon.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/resources/old/app-icon.afphoto -------------------------------------------------------------------------------- /resources/old/iconTemplate.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/resources/old/iconTemplate.afphoto -------------------------------------------------------------------------------- /resources/old/iconWinTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/resources/old/iconWinTemplate.png -------------------------------------------------------------------------------- /resources/chromecast-app-icon.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annexare/Chromecast/HEAD/resources/chromecast-app-icon.sketch -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf cache/ 4 | rm -rf build/ 5 | rm -rf node_modules/ 6 | rm -rf app/node_modules/ 7 | rm -rf app/css/ 8 | rm -rf app/js/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.exclude": { 4 | "app/js": true 5 | } 6 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "modern-browsers", 4 | "react" 5 | ], 6 | "plugins": [ 7 | "transform-class-properties" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | os: 5 | - osx 6 | 7 | before_install: 8 | - npm install -g gulp 9 | 10 | script: 11 | - gulp build:osx 12 | 13 | addons: 14 | artifacts: 15 | paths: 16 | - build/*.dmg 17 | -------------------------------------------------------------------------------- /app/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "chooseUrl": "Choose & Send URL", 3 | "file.notSupported": "File codec seems not supported by the Chromecast", 4 | "file.url": "Video file URL", 5 | "deviceList": "Device list", 6 | "lookingForChromecast": "Looking for Chromecast…" 7 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /app/locale/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "chooseUrl": "Укажите ссылку для воспроизведения", 3 | "file.notSupported": "Похоже, формат файла не поддерживается Chromecast", 4 | "file.url": "Ссылка на видео", 5 | "deviceList": "Список устройств", 6 | "lookingForChromecast": "Поиск Chromecast…" 7 | } -------------------------------------------------------------------------------- /app/locale/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "chooseUrl": "Вкажіть посилання для відтворення", 3 | "file.notSupported": "Схоже, формат файлу не підтримується Chromecast", 4 | "file.url": "Посилання на відео", 5 | "deviceList": "Список пристроїв", 6 | "lookingForChromecast": "Пошук Chromecast…" 7 | } -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=759670 3 | // for the documentation about the jsconfig.json format 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "es2015", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "exclude": [ 10 | "node_modules", 11 | "bower_components" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | environment: 3 | matrix: 4 | - nodejs_version: "6" 5 | 6 | #cache: 7 | # - node_modules 8 | 9 | install: 10 | - ps: Install-Product node $env:nodejs_version 11 | - npm install 12 | - npm install -g gulp 13 | 14 | artifacts: 15 | - path: build/*.zip 16 | name: ChromecastApp 17 | 18 | build_script: 19 | - node --version 20 | - npm --version 21 | - gulp build:win 22 | 23 | skip_commits: 24 | message: /\[ci skip\]/ 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea 3 | 4 | # Logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # System files 14 | *~ 15 | .DS_Store 16 | 17 | # node-waf configuration 18 | .lock-wscript 19 | 20 | # Dependency directory 21 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 22 | node_modules 23 | 24 | # Optional npm cache directory 25 | .npm 26 | 27 | # Desktop: Builds 28 | app/css 29 | app/js 30 | cache 31 | build 32 | 33 | # Directory for instrumented libs generated by jscoverage/JSCover 34 | lib-cov 35 | 36 | # Coverage directory used by tools like istanbul 37 | coverage 38 | -------------------------------------------------------------------------------- /typings/react/react-addons-create-fragment.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for React v0.14 (react-addons-create-fragment) 2 | // Project: http://facebook.github.io/react/ 3 | // Definitions by: Asana , AssureSign , Microsoft 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | 8 | declare namespace __React { 9 | namespace __Addons { 10 | export function createFragment(object: { [key: string]: ReactNode }): ReactFragment; 11 | } 12 | } 13 | 14 | declare module "react-addons-create-fragment" { 15 | export = __React.__Addons.createFragment; 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch the App", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/app/main.js", 9 | "stopOnEntry": false, 10 | "args": [], 11 | "cwd": "${workspaceRoot}/app", 12 | "preLaunchTask": "build:ui", 13 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 14 | "runtimeArgs": [], 15 | "env": { 16 | "NODE_ENV": "development" 17 | }, 18 | "externalConsole": false, 19 | "sourceMaps": false, 20 | "outDir": null 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /typings/react/react-addons-pure-render-mixin.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for React v0.14 (react-addons-pure-render-mixin) 2 | // Project: http://facebook.github.io/react/ 3 | // Definitions by: Asana , AssureSign , Microsoft 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | 8 | declare namespace __React { 9 | interface PureRenderMixin extends Mixin {} 10 | 11 | namespace __Addons { 12 | export var PureRenderMixin: PureRenderMixin; 13 | } 14 | } 15 | 16 | declare module "react-addons-pure-render-mixin" { 17 | var PureRenderMixin: __React.PureRenderMixin; 18 | type PureRenderMixin = __React.PureRenderMixin; 19 | export = PureRenderMixin; 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "gulp", 6 | "isShellCommand": true, 7 | "args": [ 8 | "--no-colorWAT" 9 | ], 10 | "tasks": [ 11 | { 12 | "taskName": "build", 13 | "args": [], 14 | "isBuildCommand": true, 15 | "isWatching": false, 16 | "problemMatcher": [ 17 | "$lessCompile", 18 | "$eslint-stylish", 19 | "$msCompile" 20 | ] 21 | }, 22 | { 23 | "taskName": "build:ui", 24 | "args": [], 25 | "isBuildCommand": true, 26 | "isWatching": false, 27 | "problemMatcher": [ 28 | "$lessCompile", 29 | "$eslint-stylish", 30 | "$tsc" 31 | ] 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /typings/react/react-addons-transition-group.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for React v0.14 (react-addons-transition-group) 2 | // Project: http://facebook.github.io/react/ 3 | // Definitions by: Asana , AssureSign , Microsoft 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | 8 | declare namespace __React { 9 | 10 | interface TransitionGroupProps { 11 | component?: ReactType; 12 | childFactory?: (child: ReactElement) => ReactElement; 13 | } 14 | 15 | type TransitionGroup = ComponentClass; 16 | 17 | namespace __Addons { 18 | export var TransitionGroup: __React.TransitionGroup; 19 | } 20 | } 21 | 22 | declare module "react-addons-transition-group" { 23 | var TransitionGroup: __React.TransitionGroup; 24 | type TransitionGroup = __React.TransitionGroup; 25 | export = TransitionGroup; 26 | } 27 | -------------------------------------------------------------------------------- /typings/react/react-global.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for React v0.14 (namespace) 2 | // Project: http://facebook.github.io/react/ 3 | // Definitions by: Asana , AssureSign , Microsoft 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | 17 | import React = __React; 18 | import ReactDOM = __React.__DOM; 19 | 20 | declare namespace __React { 21 | export import addons = __React.__Addons; 22 | } 23 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chromecast App 7 | 8 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Annexare Studio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /typings/react/react-addons-linked-state-mixin.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for React v0.14 (react-addons-linked-state-mixin) 2 | // Project: http://facebook.github.io/react/ 3 | // Definitions by: Asana , AssureSign , Microsoft 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | 8 | declare namespace __React { 9 | interface ReactLink { 10 | value: T; 11 | requestChange(newValue: T): void; 12 | } 13 | 14 | interface LinkedStateMixin extends Mixin { 15 | linkState(key: string): ReactLink; 16 | } 17 | 18 | interface HTMLAttributes { 19 | checkedLink?: ReactLink; 20 | valueLink?: ReactLink; 21 | } 22 | 23 | namespace __Addons { 24 | export var LinkedStateMixin: LinkedStateMixin; 25 | } 26 | } 27 | 28 | declare module "react-addons-linked-state-mixin" { 29 | var LinkedStateMixin: __React.LinkedStateMixin; 30 | type LinkedStateMixin = __React.LinkedStateMixin; 31 | export = LinkedStateMixin; 32 | } 33 | -------------------------------------------------------------------------------- /typings/react/react-addons-update.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for React v0.14 (react-addons-update) 2 | // Project: http://facebook.github.io/react/ 3 | // Definitions by: Asana , AssureSign , Microsoft 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | 8 | declare namespace __React { 9 | interface UpdateSpecCommand { 10 | $set?: any; 11 | $merge?: {}; 12 | $apply?(value: any): any; 13 | } 14 | 15 | interface UpdateSpecPath { 16 | [key: string]: UpdateSpec; 17 | } 18 | 19 | type UpdateSpec = UpdateSpecCommand | UpdateSpecPath; 20 | 21 | interface UpdateArraySpec extends UpdateSpecCommand { 22 | $push?: any[]; 23 | $unshift?: any[]; 24 | $splice?: any[][]; 25 | } 26 | 27 | namespace __Addons { 28 | export function update(value: any[], spec: UpdateArraySpec): any[]; 29 | export function update(value: {}, spec: UpdateSpec): any; 30 | } 31 | } 32 | 33 | declare module "react-addons-update" { 34 | export = __React.__Addons.update; 35 | } 36 | -------------------------------------------------------------------------------- /resources/cast-icon-chromecast.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromecast-app", 3 | "productName": "Chromecast app", 4 | "version": "0.5.1", 5 | "description": "Desktop application for Chromecast, based on Node.js, Electron, React & Material-UI", 6 | "main": "main.js", 7 | "scripts": { 8 | "start": "electron main.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/annexare/Chromecast" 13 | }, 14 | "keywords": [ 15 | "Chromecast", 16 | "App", 17 | "GUI", 18 | "Electron", 19 | "Node", 20 | "React", 21 | "Material", 22 | "Google", 23 | "Annexare" 24 | ], 25 | "author": "Annexare", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/annexare/Chromecast/issues" 29 | }, 30 | "homepage": "", 31 | "dependencies": { 32 | "castv2-client": "*", 33 | "chromecast-scanner": "*", 34 | "material-ui": "~0.15.4", 35 | "react": "~15.3.1", 36 | "react-dom": "~15.3.1", 37 | "react-intl": "^2.1.2", 38 | "react-tap-event-plugin": "*", 39 | "request": "*" 40 | }, 41 | "devDependencies": { 42 | 43 | }, 44 | "electronVersion": "1.3.4", 45 | "engine": "node >= 5.10.0" 46 | } -------------------------------------------------------------------------------- /jsx/index.jsx: -------------------------------------------------------------------------------- 1 | /* global App */ 2 | /* eslint no-unused-vars: 0 */ 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import {IntlProvider, FormattedMessage} from 'react-intl'; 7 | import injectTapEventPlugin from 'react-tap-event-plugin'; 8 | 9 | const DEFAULT_LOCALE = 'en'; 10 | 11 | injectTapEventPlugin(); 12 | 13 | let appElement = document.getElementById('app'), 14 | renderApp = (event, userLocale) => { 15 | let locale = userLocale || DEFAULT_LOCALE, 16 | localeMessages; 17 | 18 | if (locale.length > 2) { 19 | locale = locale.substring(0, 2); 20 | } 21 | 22 | console.log('Render app', locale); 23 | 24 | try { 25 | localeMessages = require('./locale/' + locale + '.json'); 26 | } catch (e) { 27 | console.log(`Locale "${locale}" can\'t be loaded, using default (${DEFAULT_LOCALE}).`); 28 | localeMessages = require('./locale/' + DEFAULT_LOCALE + '.json'); 29 | } 30 | 31 | ReactDOM.render( 32 | 33 | 34 | , 35 | appElement 36 | ); 37 | }; 38 | 39 | // Initial render of the App 40 | renderApp(); 41 | // Locale event from main.js 42 | App.ipc.on('locale', renderApp); 43 | -------------------------------------------------------------------------------- /typings/react/react-addons-css-transition-group.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for React v0.14 (react-addons-css-transition-group) 2 | // Project: http://facebook.github.io/react/ 3 | // Definitions by: Asana , AssureSign , Microsoft 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | /// 8 | 9 | declare namespace __React { 10 | interface CSSTransitionGroupTransitionName { 11 | enter: string; 12 | enterActive?: string; 13 | leave: string; 14 | leaveActive?: string; 15 | appear?: string; 16 | appearActive?: string; 17 | } 18 | 19 | interface CSSTransitionGroupProps extends TransitionGroupProps { 20 | transitionName: string | CSSTransitionGroupTransitionName; 21 | transitionAppear?: boolean; 22 | transitionAppearTimeout?: number; 23 | transitionEnter?: boolean; 24 | transitionEnterTimeout?: number; 25 | transitionLeave?: boolean; 26 | transitionLeaveTimeout?: number; 27 | } 28 | 29 | type CSSTransitionGroup = ComponentClass; 30 | 31 | namespace __Addons { 32 | export var CSSTransitionGroup: __React.CSSTransitionGroup; 33 | } 34 | } 35 | 36 | declare module "react-addons-css-transition-group" { 37 | var CSSTransitionGroup: __React.CSSTransitionGroup; 38 | type CSSTransitionGroup = __React.CSSTransitionGroup; 39 | export = CSSTransitionGroup; 40 | } 41 | -------------------------------------------------------------------------------- /typings/react/react-addons-perf.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for React v0.14 (react-addons-perf) 2 | // Project: http://facebook.github.io/react/ 3 | // Definitions by: Asana , AssureSign , Microsoft 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | 8 | declare namespace __React { 9 | interface ComponentPerfContext { 10 | current: string; 11 | owner: string; 12 | } 13 | 14 | interface NumericPerfContext { 15 | [key: string]: number; 16 | } 17 | 18 | interface Measurements { 19 | exclusive: NumericPerfContext; 20 | inclusive: NumericPerfContext; 21 | render: NumericPerfContext; 22 | counts: NumericPerfContext; 23 | writes: NumericPerfContext; 24 | displayNames: { 25 | [key: string]: ComponentPerfContext; 26 | }; 27 | totalTime: number; 28 | } 29 | 30 | namespace __Addons { 31 | namespace Perf { 32 | export function start(): void; 33 | export function stop(): void; 34 | export function printInclusive(measurements: Measurements[]): void; 35 | export function printExclusive(measurements: Measurements[]): void; 36 | export function printWasted(measurements: Measurements[]): void; 37 | export function printDOM(measurements: Measurements[]): void; 38 | export function getLastMeasurements(): Measurements[]; 39 | } 40 | } 41 | } 42 | 43 | declare module "react-addons-perf" { 44 | import Perf = __React.__Addons.Perf; 45 | export = Perf; 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromecast-app", 3 | "productName": "Chromecast app", 4 | "version": "0.5.1", 5 | "description": "Desktop application for Chromecast, based on Node.js, Electron, React & Material-UI", 6 | "main": "app/main.js", 7 | "scripts": { 8 | "postinstall": "cd ./app && npm install", 9 | "start": "set NODE_ENV=development && gulp build:ui && electron app/main.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/annexare/Chromecast" 14 | }, 15 | "keywords": [ 16 | "Chromecast", 17 | "App", 18 | "GUI", 19 | "Electron", 20 | "Node", 21 | "React", 22 | "Material", 23 | "Google", 24 | "Annexare" 25 | ], 26 | "author": "Annexare", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/annexare/Chromecast/issues" 30 | }, 31 | "homepage": "http://annexare.github.io/Chromecast", 32 | "eslintConfig": ".eslintrc.json", 33 | "devDependencies": { 34 | "babel-eslint": "*", 35 | "babel-plugin-transform-class-properties": "*", 36 | "babel-preset-modern-browsers": "*", 37 | "babel-preset-react": "*", 38 | "electron-prebuilt": "1.3.4", 39 | "electron-packager": "~7.7.0", 40 | "eslint-plugin-react": "*", 41 | "del": "*", 42 | "flexboxgrid": "*", 43 | "gulp": "*", 44 | "gulp-babel": "*", 45 | "gulp-concat": "*", 46 | "gulp-eslint": "*", 47 | "material-design-lite": "*", 48 | "pack-dir": ">=0.6.0" 49 | }, 50 | "xDependencies": { 51 | "babel-loader": "*", 52 | "webpack": "*" 53 | }, 54 | "engine": "node >= 5.10.0" 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Chromecast app 2 | 3 | [![Travis CI](https://travis-ci.org/annexare/Chromecast.svg "Travis CI")](https://travis-ci.org/annexare/Chromecast) 4 | [![AppVeyor CI](https://ci.appveyor.com/api/projects/status/vanxx5rell1yckj8?svg=true "AppVeyor CI")](https://ci.appveyor.com/project/z-ax/Chromecast) 5 | 6 | This is a very basic app that allows sending direct URL (not local file yet) from desktop to Google Chromecast. 7 | 8 | ![icon-1024 0 25x](https://cloud.githubusercontent.com/assets/1391015/18034604/cb0995b0-6d49-11e6-92e7-87d33804379f.png) 9 | 10 | Was implemented using: 11 | 12 | - [Node.js](https://nodejs.org/en/), Atom [Electron](http://electron.atom.io/), [node-castv2-client](https://github.com/thibauts/node-castv2-client) 13 | - Facebook [React](http://facebook.github.io/react/) 14 | - The very basic components from [Material-UI](http://www.material-ui.com/#/) & [Flexbox Grid](http://flexboxgrid.com/) 15 | - [Gulp](http://gulpjs.com/) task runner, [Babel](https://babeljs.io/) 16 | 17 | Mainly, the project may showcase how this may work together. 18 | 19 | ## Environment 20 | 21 | ### Node 22 | 23 | ``` 24 | # local dependencies, may take some time 25 | npm install 26 | 27 | # launch via electron 28 | npm start 29 | 30 | # build binaries for Desktop 31 | gulp build 32 | # build binaries, OS specific only 33 | gulp build:osx 34 | gulp build:win 35 | 36 | # build front-end only 37 | gulp build:ui 38 | ``` 39 | 40 | ### VS Code 41 | 42 | ``` 43 | npm install 44 | npm install -g tsd 45 | tsd install node 46 | tsd install react-global 47 | ``` 48 | 49 | ## TODO 50 | 51 | This stuff seems useful, feel free to contribute with a PR (not sure when will have time to implement): 52 | 53 | - [x] Do not connect immediately, choose from menu 54 | - [x] Seek bar 55 | - [x] Translations 56 | - [ ] Volume control 57 | - [ ] Playlists 58 | - [x] Tray icon: menu items 59 | - [x] Tray icon: Drag'n'Drop URL 60 | - [ ] YouTube links support 61 | -------------------------------------------------------------------------------- /webpack.backend.js: -------------------------------------------------------------------------------- 1 | const 2 | fs = require('fs'), 3 | webpack = require('webpack'), 4 | nodeModules = fs.readdirSync('./app/node_modules') 5 | .filter(function (x) { 6 | return ['.bin'].indexOf(x) === -1; 7 | }), 8 | config = { 9 | context: __dirname + '/app', 10 | entry: [ 11 | './index.js' 12 | ], 13 | target: 'electron', 14 | output: { 15 | path: './app', 16 | publicPath: './app', 17 | filename: 'main.js' 18 | }, 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.js$/, 23 | // exclude: /node_modules/, 24 | loader: 'babel', 25 | query: { 26 | presets: ['modern-browsers'], 27 | plugins: [ 28 | 'transform-class-properties' 29 | ] 30 | } 31 | } 32 | ] 33 | }, 34 | node: { 35 | __dirname: true, 36 | __filename: true 37 | }, 38 | externals: [ 39 | function (context, request, callback) { 40 | let pathStart = request.split('/')[0]; 41 | if (nodeModules.indexOf(pathStart) >= 0) { 42 | return callback(null, 'commonjs2 ' + request); 43 | } 44 | 45 | callback(); 46 | } 47 | ], 48 | // recordsPath: path.join(__dirname, 'build/_records'), 49 | plugins: [ 50 | new webpack.IgnorePlugin(/\.(css|less|html)$/), 51 | // new webpack.BannerPlugin('require("source-map-support").install();', 52 | // { raw: true, entryOnly: false }), 53 | // new webpack.HotModuleReplacementPlugin({ quiet: true }) 54 | ] 55 | }; 56 | 57 | module.exports = config; -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const app = require('electron').app; 4 | const ipcMain = require('electron').ipcMain; 5 | const services = require('./src/services'); 6 | const ui = require('./src/window'); 7 | 8 | app.on('open-file', (e, path) => { 9 | console.log(' <- open-file', path, arguments); 10 | }); 11 | 12 | app.on('open-url', (e, path) => { 13 | console.log(' <- open-url', path, arguments); 14 | }); 15 | 16 | ui.init(() => { 17 | let isPlaying = false; 18 | 19 | // Locale 20 | ui.send('locale', app.getLocale()); 21 | // Window 22 | ui.service = services; 23 | ui.window.on('closed', () => services.close()); 24 | 25 | // Chromecast 26 | services.on('service', () => { 27 | let uiList = []; 28 | services.list.forEach((device) => { 29 | uiList.push({ 30 | host: device.host, 31 | name: device.name 32 | }); 33 | }); 34 | 35 | ui.send('services', uiList); 36 | ui.setMenu(); 37 | }); 38 | services.on('close', () => { 39 | ui.send('close'); 40 | ui.setMenu(); 41 | }); 42 | services.on('connected', (host) => { 43 | ui.send('connected', host); 44 | ui.setMenu(); 45 | }); 46 | services.on('status', (status) => { 47 | ui.send('status', status); 48 | 49 | let statusPlaying = services.isPlaying(); 50 | console.log(' --- STATUS', status, statusPlaying); 51 | if (!status || isPlaying !== statusPlaying) { 52 | isPlaying = statusPlaying; 53 | ui.setMenu(); 54 | } 55 | }); 56 | services.on('unsupported', () => { 57 | ui.send('unsupported'); 58 | }); 59 | services.browse(); 60 | 61 | // IPC 62 | ipcMain.on('do', (event, action, url) => { 63 | if (action !== 'noop') { 64 | console.log(' <- ', action, url); 65 | } 66 | 67 | if (action === 'load') { 68 | services.load(url, event); 69 | } else { 70 | services[action](url); 71 | } 72 | }); 73 | }); 74 | 75 | module.exports = { 76 | app, 77 | ui 78 | }; -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "mocha": true, 6 | "es6": true, 7 | "jest": true 8 | }, 9 | "globals": { 10 | "document": true, 11 | "log": true, 12 | "logRequest": true, 13 | "__SRC__": true, 14 | "React": true, 15 | "ReactDOM": true, 16 | "ReactBootstrap": true 17 | }, 18 | "parserOptions": { 19 | "ecmaFeatures": { 20 | "arrowFunctions": true, 21 | "blockBindings": true, 22 | "classes": true, 23 | "defaultParams": true, 24 | "experimentalObjectRestSpread": true, 25 | "jsx": true, 26 | "modules": true, 27 | "objectLiteralShorthandMethods": true, 28 | "objectLiteralShorthandProperties": true, 29 | "restParams": true, 30 | "superInFunctions": true, 31 | "templateStrings": true 32 | }, 33 | "sourceType": "module" 34 | }, 35 | "plugins": ["react"], 36 | "rules": { 37 | "block-scoped-var": 2, 38 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 39 | "comma-spacing": [2, {"before": false, "after": true}], 40 | "curly": [2, "all"], 41 | "generator-star-spacing": [2, {"before": true, "after": false}], 42 | "dot-notation": [2, {"allowKeywords": true}], 43 | "guard-for-in": 2, 44 | "eqeqeq": [2, "smart"], 45 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 46 | "linebreak-style": [2, "unix"], 47 | "new-cap": 0, 48 | "no-bitwise": 2, 49 | "no-unused-vars": 2, 50 | "no-caller": 2, 51 | "no-const-assign": 2, 52 | "no-empty": 0, 53 | "no-eval": 2, 54 | "no-extra-semi": 2, 55 | "no-loop-func": 0, 56 | "no-mixed-spaces-and-tabs": 2, 57 | "no-new": 2, 58 | "no-trailing-spaces": [2, { "skipBlankLines": false }], 59 | "no-undef": 2, 60 | "no-undef-init": 2, 61 | "no-undefined": 2, 62 | "no-use-before-define": [2, "nofunc"], 63 | "no-unused-expressions": 2, 64 | "no-var": 2, 65 | "semi": [2, "always"], 66 | "quotes": [2, "single", "avoid-escape"], 67 | "wrap-iife": [2, "inside"] 68 | } 69 | } -------------------------------------------------------------------------------- /jsx/components/DevicesList.jsx: -------------------------------------------------------------------------------- 1 | import {List, ListItem} from 'material-ui/List'; 2 | import DeviceIcon from 'material-ui/svg-icons/action/power-settings-new'; 3 | import DeviceIconSelected from 'material-ui/svg-icons/action/check-circle'; 4 | 5 | class DevicesList extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | service: '' 11 | }; 12 | 13 | App.ipc.on('close', this.handleClose); 14 | App.ipc.on('connected', this.handleServiceChange); 15 | App.ipc.on('services', this.handleRemoteServices); 16 | } 17 | 18 | handleChange = (host) => { 19 | console.log('DevicesList.handleChange()', host); 20 | App.do('handleDevice', host); 21 | } 22 | 23 | handleClose = () => { 24 | this.setState({ 25 | service: '' 26 | }); 27 | }; 28 | 29 | handleRemoteServices = (event, list) => { 30 | this.setState({ 31 | services: list 32 | }); 33 | } 34 | 35 | handleServiceChange = (event, service) => { 36 | this.setState({ 37 | service: service 38 | }); 39 | } 40 | 41 | renderServciesList = () => { 42 | return this.state.services.map((service, index) => { 43 | if (!service || !service.host) { 44 | return false; 45 | } 46 | 47 | let isChecked = this.state.service 48 | ? service.host === this.state.service 49 | : false; 50 | 51 | return this.handleChange(service.host)} 54 | leftIcon={ isChecked ? : } 55 | primaryText={ service.name || service.host } 56 | />; 57 | }); 58 | }; 59 | 60 | render() { 61 | return ( 62 |
63 | { 64 | this.state.services && this.state.services.length 65 | ? ( 66 | 67 | { this.renderServciesList() } 68 | 69 | ) 70 | : 71 | } 72 |
73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /typings/react/react-dom.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for React v0.14 (react-dom) 2 | // Project: http://facebook.github.io/react/ 3 | // Definitions by: Asana , AssureSign , Microsoft 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | 8 | declare namespace __React { 9 | namespace __DOM { 10 | function findDOMNode(instance: ReactInstance): E; 11 | function findDOMNode(instance: ReactInstance): Element; 12 | 13 | function render

( 14 | element: DOMElement

, 15 | container: Element, 16 | callback?: (element: Element) => any): Element; 17 | function render( 18 | element: ClassicElement

, 19 | container: Element, 20 | callback?: (component: ClassicComponent) => any): ClassicComponent; 21 | function render( 22 | element: ReactElement

, 23 | container: Element, 24 | callback?: (component: Component) => any): Component; 25 | 26 | function unmountComponentAtNode(container: Element): boolean; 27 | 28 | var version: string; 29 | 30 | function unstable_batchedUpdates(callback: (a: A, b: B) => any, a: A, b: B): void; 31 | function unstable_batchedUpdates(callback: (a: A) => any, a: A): void; 32 | function unstable_batchedUpdates(callback: () => any): void; 33 | 34 | function unstable_renderSubtreeIntoContainer

( 35 | parentComponent: Component, 36 | nextElement: DOMElement

, 37 | container: Element, 38 | callback?: (element: Element) => any): Element; 39 | function unstable_renderSubtreeIntoContainer( 40 | parentComponent: Component, 41 | nextElement: ClassicElement

, 42 | container: Element, 43 | callback?: (component: ClassicComponent) => any): ClassicComponent; 44 | function unstable_renderSubtreeIntoContainer( 45 | parentComponent: Component, 46 | nextElement: ReactElement

, 47 | container: Element, 48 | callback?: (component: Component) => any): Component; 49 | } 50 | 51 | namespace __DOMServer { 52 | function renderToString(element: ReactElement): string; 53 | function renderToStaticMarkup(element: ReactElement): string; 54 | var version: string; 55 | } 56 | } 57 | 58 | declare module "react-dom" { 59 | import DOM = __React.__DOM; 60 | export = DOM; 61 | } 62 | 63 | declare module "react-dom/server" { 64 | import DOMServer = __React.__DOMServer; 65 | export = DOMServer; 66 | } 67 | -------------------------------------------------------------------------------- /jsx/components/App.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import GetMuiTheme from 'material-ui/styles/getMuiTheme'; 4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 5 | 6 | class App extends React.Component { 7 | static ipc = require('electron').ipcRenderer; 8 | static do(method, param) { 9 | App.ipc.send('do', method, param); 10 | } 11 | 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | deviceName: '', 17 | hasNoDevice: true, 18 | service: '', 19 | services: [] 20 | }; 21 | 22 | // document.addEventListener('drop', this.handleFile); 23 | // document.addEventListener('dragover', this.handleFile); 24 | 25 | App.ipc.on('close', this.handleClose); 26 | App.ipc.on('connected', this.handleServiceChange); 27 | App.ipc.on('services', this.handleRemoteServices); 28 | } 29 | 30 | handleClose = () => { 31 | console.log('App.handleClose()'); 32 | this.setState({ 33 | deviceName: '', 34 | service: '' 35 | }); 36 | }; 37 | 38 | handleFile = (e) => { 39 | console.log('App.handleFile()', arguments); 40 | e.preventDefault(); 41 | 42 | if (!this.props.service) { 43 | e.stopPropagation(); 44 | } 45 | } 46 | 47 | handleRemoteServices = (event, list) => { 48 | console.log('App.handleRemoteServices()', list); 49 | this.setState({ 50 | deviceName: list && list.length 51 | ? (list[0].name || '').replace('.local', '') 52 | : '', 53 | hasNoDevice: false, 54 | services: list 55 | }); 56 | } 57 | 58 | handleServiceChange = (event, service) => { 59 | console.log('App.handleServiceChange()', service); 60 | this.setState({ 61 | service: service 62 | }); 63 | } 64 | 65 | render() { 66 | let hasNoDevice = this.state.hasNoDevice, 67 | title = ; 68 | 69 | return ( 70 | 71 |

95 | 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /resources/chromecast-device-vector.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gulp tasks. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | let app = (path) => { 8 | return './app/' + (path || ''); 9 | }; 10 | 11 | let src = (path) => { 12 | return './jsx/' + (path || ''); 13 | }; 14 | 15 | let vendor = (path) => { 16 | return './node_modules/' + (path || ''); 17 | }; 18 | 19 | const 20 | gulp = require('gulp'), 21 | concat = require('gulp-concat'), 22 | babel = require('gulp-babel'), 23 | eslint = require('gulp-eslint'), 24 | packager = require('electron-packager'), 25 | // packBackend = require('./webpack.backend.js'), 26 | packDir = require('pack-dir'), 27 | // webpack = require('webpack'), 28 | 29 | paths = { 30 | desktopApp: './build', 31 | css: app('css/'), 32 | js: app('js/'), 33 | src: '', 34 | srcBackend: [ 35 | app('*.js'), 36 | 'gulpfile.js' 37 | ], 38 | srcFiles: [ 39 | src('components/*.jsx'), 40 | src('index.jsx') 41 | ], 42 | reloadOn: [ 43 | app('css/*.css'), 44 | app('js/*.js'), 45 | app('main*.js'), 46 | app('index.html') 47 | ] 48 | }; 49 | 50 | let getPackagerParams = (platform) => { 51 | let pkg = require(app('package.json')), 52 | params = { 53 | arch: 'all', 54 | asar: true, 55 | dir: app(), 56 | icon: app('img/icon512'), 57 | name: pkg.productName, 58 | out: paths.desktopApp, 59 | overwrite: true, 60 | platform: platform, 61 | prune: true, 62 | version: pkg.electronVersion, 63 | 'app-version': pkg.version, 64 | 'app-copyright': pkg.author, 65 | 'build-version': pkg.version 66 | }; 67 | 68 | if (platform === 'darwin') { 69 | params['app-category-type'] = 'public.app-category.video'; 70 | } else if (platform === 'win32') { 71 | params.arch = 'ia32'; 72 | params['version-string'] = { 73 | CompanyName: pkg.author, 74 | FileDescription: pkg.description, 75 | InternalName: pkg.productName, 76 | OriginalFilename: pkg.productName + '.exe', 77 | 78 | // Deprecated 79 | // FileVersion: pkg.version, 80 | // ProductVersion: pkg.version, 81 | ProductName: pkg.productName 82 | }; 83 | } 84 | 85 | return params; 86 | }; 87 | 88 | let packagingDone = (err, appPaths, cb) => { 89 | if (err) { 90 | console.log('Error: ', err); 91 | 92 | return cb(); 93 | } 94 | 95 | if (Array.isArray(appPaths)) { 96 | appPaths.forEach(appPath => packDir.path(appPath)); 97 | } 98 | 99 | cb(); 100 | }; 101 | 102 | let packaging = (cb, platform) => { 103 | packager( 104 | getPackagerParams(platform), 105 | (err, appPaths) => packagingDone(err, appPaths, cb) 106 | ); 107 | }; 108 | 109 | // let onWebPack = (done) => { 110 | // return function (err, stats) { 111 | // if (err) { 112 | // console.log('Error', err); 113 | // } else { 114 | // console.log(stats.toString()); 115 | // } 116 | 117 | // if (done) { 118 | // done(); 119 | // } 120 | // }; 121 | // }; 122 | 123 | gulp.task('build:ui-vendor-css', () => { 124 | return gulp.src([ 125 | vendor('flexboxgrid/dist/flexboxgrid.min.css'), 126 | vendor('material-design-lite/dist/material-grid.min.css') 127 | ]) 128 | .pipe(concat('vendor.css')) 129 | .pipe(gulp.dest(paths.css)); 130 | }); 131 | gulp.task('build:ui-vendor-js', () => { 132 | return gulp.src([ 133 | vendor('react/dist/react-with-addons.js'), 134 | vendor('react-dom/dist/react-dom.js') 135 | ]) 136 | .pipe(concat('vendor.js')) 137 | .pipe(gulp.dest(paths.js)); 138 | }); 139 | gulp.task('build:ui-js', () => { 140 | // process.env.NODE_ENV = 'production'; 141 | 142 | return gulp.src(paths.srcFiles) 143 | .pipe(concat('index.js')) 144 | .pipe(babel({ 145 | sourceRoot: paths.src 146 | })) 147 | .pipe(gulp.dest(paths.js)); 148 | }); 149 | 150 | // gulp.task('build:backend', function (done) { 151 | // webpack(packBackend).run(onWebPack(done)); 152 | // }); 153 | 154 | gulp.task('build:ui-vendor', ['build:ui-vendor-css', 'build:ui-vendor-js']); 155 | gulp.task('build:ui', ['build:ui-vendor', 'build:ui-js']); 156 | 157 | gulp.task('clean:app', () => { 158 | let del = require('del'); 159 | 160 | return del([ 161 | paths.desktopApp + '/**/*' 162 | ]); 163 | }); 164 | 165 | gulp.task('lint', ['build:ui-js'], () => { 166 | return gulp.src(['app/js/index.js']) 167 | .pipe(eslint()) 168 | .pipe(eslint.format()) 169 | ; 170 | }); 171 | 172 | gulp.task('build:app:osx', (cb) => packaging(cb, 'darwin')); 173 | gulp.task('build:app:win', (cb) => packaging(cb, 'win32')); 174 | gulp.task('build:app', ['clean:app', 'build:app:osx', 'build:app:win']); 175 | gulp.task('build', ['build:ui', 'build:app']); 176 | gulp.task('build:osx', ['clean:app', 'build:ui', 'build:app:osx']); 177 | gulp.task('build:win', ['clean:app', 'build:ui', 'build:app:win']); 178 | 179 | gulp.task('default', ['build']); 180 | -------------------------------------------------------------------------------- /app/src/menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Electron = require('electron'); 4 | const Menu = Electron.Menu; 5 | const clipboard = require('electron').clipboard; 6 | 7 | const isOSX = (process.platform === 'darwin'); 8 | 9 | let separator = () => ({ type: 'separator' }); 10 | let templateWindowSubmenu = [ 11 | { 12 | label: 'Minimize', 13 | accelerator: 'CmdOrCtrl+M', 14 | role: 'minimize' 15 | }, 16 | { 17 | label: 'Close', 18 | accelerator: 'CmdOrCtrl+W', 19 | role: 'close' 20 | }, 21 | ]; 22 | 23 | if (isOSX) { 24 | templateWindowSubmenu.push( 25 | separator(), 26 | { 27 | label: 'Bring All to Front', 28 | role: 'front' 29 | } 30 | ); 31 | } 32 | 33 | let template = [ 34 | { 35 | label: 'Edit', 36 | submenu: [ 37 | { 38 | label: 'Undo', 39 | accelerator: 'CmdOrCtrl+Z', 40 | role: 'undo' 41 | }, 42 | { 43 | label: 'Redo', 44 | accelerator: 'Shift+CmdOrCtrl+Z', 45 | role: 'redo' 46 | }, 47 | separator(), 48 | { 49 | label: 'Cut', 50 | accelerator: 'CmdOrCtrl+X', 51 | role: 'cut' 52 | }, 53 | { 54 | label: 'Copy', 55 | accelerator: 'CmdOrCtrl+C', 56 | role: 'copy' 57 | }, 58 | { 59 | label: 'Paste', 60 | accelerator: 'CmdOrCtrl+V', 61 | // role: 'paste', 62 | click: (item, focusedWindow) => { 63 | console.log(' <- paste', clipboard.readText()); 64 | focusedWindow.webContents.send('url', clipboard.readText()); 65 | } 66 | }, 67 | { 68 | label: 'Select All', 69 | accelerator: 'CmdOrCtrl+A', 70 | role: 'selectall' 71 | }, 72 | ] 73 | }, 74 | { 75 | label: 'View', 76 | submenu: [ 77 | // { 78 | // label: 'Reload', 79 | // accelerator: 'CmdOrCtrl+R', 80 | // click: function (item, focusedWindow) { 81 | // if (focusedWindow) { 82 | // focusedWindow.reload(); 83 | // } 84 | // } 85 | // }, 86 | // { 87 | // label: 'Toggle Full Screen', 88 | // accelerator: (function () { 89 | // if (process.platform === 'darwin') { 90 | // return 'Ctrl+Command+F'; 91 | // } else { 92 | // return 'F11'; 93 | // } 94 | // })(), 95 | // click: function (item, focusedWindow) { 96 | // if (focusedWindow) { 97 | // focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); 98 | // } 99 | // } 100 | // }, 101 | { 102 | label: 'Toggle Developer Tools', 103 | accelerator: (() => { 104 | if (isOSX) { 105 | return 'Alt+Command+I'; 106 | } else { 107 | return 'Ctrl+Shift+I'; 108 | } 109 | })(), 110 | click: (item, focusedWindow) => { 111 | if (focusedWindow) { 112 | focusedWindow.toggleDevTools(); 113 | } 114 | } 115 | }, 116 | ] 117 | }, 118 | { 119 | label: 'Window', 120 | role: 'window', 121 | submenu: templateWindowSubmenu 122 | }, 123 | { 124 | label: 'Help', 125 | role: 'help', 126 | submenu: [ 127 | { 128 | label: 'Learn More', 129 | click: function () { 130 | Electron.shell.openExternal('http://annexare.github.io/Chromecast/'); 131 | } 132 | }, 133 | ] 134 | }, 135 | ]; 136 | 137 | if (isOSX) { 138 | let name = Electron.app.getName(); 139 | let templateAbout = { 140 | label: name, 141 | submenu: [ 142 | { 143 | label: 'About ' + name, 144 | role: 'about' 145 | }, 146 | separator(), 147 | { 148 | label: 'Services', 149 | role: 'services', 150 | submenu: [] 151 | }, 152 | separator(), 153 | { 154 | label: 'Hide ' + name, 155 | accelerator: 'Command+H', 156 | role: 'hide' 157 | }, 158 | { 159 | label: 'Hide Others', 160 | accelerator: 'Command+Shift+H', 161 | role: 'hideothers' 162 | }, 163 | { 164 | label: 'Show All', 165 | role: 'unhide' 166 | }, 167 | separator(), 168 | { 169 | label: 'Quit', 170 | accelerator: 'Command+Q', 171 | click: () => { 172 | Electron.app.quit(); 173 | } 174 | }, 175 | ] 176 | }; 177 | template.unshift(templateAbout); 178 | } 179 | 180 | let menu = Menu.buildFromTemplate(template); 181 | 182 | module.exports = Menu.setApplicationMenu(menu); -------------------------------------------------------------------------------- /typings/react/react-addons-test-utils.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for React v0.14 (react-addons-test-utils) 2 | // Project: http://facebook.github.io/react/ 3 | // Definitions by: Asana , AssureSign , Microsoft 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | 8 | declare namespace __React { 9 | interface SyntheticEventData { 10 | altKey?: boolean; 11 | button?: number; 12 | buttons?: number; 13 | clientX?: number; 14 | clientY?: number; 15 | changedTouches?: TouchList; 16 | charCode?: boolean; 17 | clipboardData?: DataTransfer; 18 | ctrlKey?: boolean; 19 | deltaMode?: number; 20 | deltaX?: number; 21 | deltaY?: number; 22 | deltaZ?: number; 23 | detail?: number; 24 | getModifierState?(key: string): boolean; 25 | key?: string; 26 | keyCode?: number; 27 | locale?: string; 28 | location?: number; 29 | metaKey?: boolean; 30 | pageX?: number; 31 | pageY?: number; 32 | relatedTarget?: EventTarget; 33 | repeat?: boolean; 34 | screenX?: number; 35 | screenY?: number; 36 | shiftKey?: boolean; 37 | targetTouches?: TouchList; 38 | touches?: TouchList; 39 | view?: AbstractView; 40 | which?: number; 41 | } 42 | 43 | interface EventSimulator { 44 | (element: Element, eventData?: SyntheticEventData): void; 45 | (component: Component, eventData?: SyntheticEventData): void; 46 | } 47 | 48 | interface MockedComponentClass { 49 | new(): any; 50 | } 51 | 52 | class ShallowRenderer { 53 | getRenderOutput>(): E; 54 | getRenderOutput(): ReactElement; 55 | render(element: ReactElement, context?: any): void; 56 | unmount(): void; 57 | } 58 | 59 | namespace __Addons { 60 | namespace TestUtils { 61 | namespace Simulate { 62 | export var blur: EventSimulator; 63 | export var change: EventSimulator; 64 | export var click: EventSimulator; 65 | export var cut: EventSimulator; 66 | export var doubleClick: EventSimulator; 67 | export var drag: EventSimulator; 68 | export var dragEnd: EventSimulator; 69 | export var dragEnter: EventSimulator; 70 | export var dragExit: EventSimulator; 71 | export var dragLeave: EventSimulator; 72 | export var dragOver: EventSimulator; 73 | export var dragStart: EventSimulator; 74 | export var drop: EventSimulator; 75 | export var focus: EventSimulator; 76 | export var input: EventSimulator; 77 | export var keyDown: EventSimulator; 78 | export var keyPress: EventSimulator; 79 | export var keyUp: EventSimulator; 80 | export var mouseDown: EventSimulator; 81 | export var mouseEnter: EventSimulator; 82 | export var mouseLeave: EventSimulator; 83 | export var mouseMove: EventSimulator; 84 | export var mouseOut: EventSimulator; 85 | export var mouseOver: EventSimulator; 86 | export var mouseUp: EventSimulator; 87 | export var paste: EventSimulator; 88 | export var scroll: EventSimulator; 89 | export var submit: EventSimulator; 90 | export var touchCancel: EventSimulator; 91 | export var touchEnd: EventSimulator; 92 | export var touchMove: EventSimulator; 93 | export var touchStart: EventSimulator; 94 | export var wheel: EventSimulator; 95 | } 96 | 97 | export function renderIntoDocument( 98 | element: DOMElement): Element; 99 | export function renderIntoDocument

( 100 | element: ReactElement

): Component; 101 | export function renderIntoDocument>( 102 | element: ReactElement): C; 103 | 104 | export function mockComponent( 105 | mocked: MockedComponentClass, mockTagName?: string): typeof TestUtils; 106 | 107 | export function isElementOfType( 108 | element: ReactElement, type: ReactType): boolean; 109 | export function isDOMComponent(instance: ReactInstance): boolean; 110 | export function isCompositeComponent(instance: ReactInstance): boolean; 111 | export function isCompositeComponentWithType( 112 | instance: ReactInstance, 113 | type: ComponentClass): boolean; 114 | 115 | export function findAllInRenderedTree( 116 | root: Component, 117 | fn: (i: ReactInstance) => boolean): ReactInstance[]; 118 | 119 | export function scryRenderedDOMComponentsWithClass( 120 | root: Component, 121 | className: string): Element[]; 122 | export function findRenderedDOMComponentWithClass( 123 | root: Component, 124 | className: string): Element; 125 | 126 | export function scryRenderedDOMComponentsWithTag( 127 | root: Component, 128 | tagName: string): Element[]; 129 | export function findRenderedDOMComponentWithTag( 130 | root: Component, 131 | tagName: string): Element; 132 | 133 | export function scryRenderedComponentsWithType

( 134 | root: Component, 135 | type: ComponentClass

): Component[]; 136 | export function scryRenderedComponentsWithType>( 137 | root: Component, 138 | type: ComponentClass): C[]; 139 | 140 | export function findRenderedComponentWithType

( 141 | root: Component, 142 | type: ComponentClass

): Component; 143 | export function findRenderedComponentWithType>( 144 | root: Component, 145 | type: ComponentClass): C; 146 | 147 | export function createRenderer(): ShallowRenderer; 148 | } 149 | } 150 | } 151 | 152 | declare module "react-addons-test-utils" { 153 | import TestUtils = __React.__Addons.TestUtils; 154 | export = TestUtils; 155 | } 156 | -------------------------------------------------------------------------------- /app/src/services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CastClient = require('castv2-client').Client; 4 | const DefaultMediaReceiver = require('castv2-client').DefaultMediaReceiver; 5 | const EventEmitter = require('events').EventEmitter; 6 | const scanner = require('chromecast-scanner'); 7 | const request = require('request'); 8 | 9 | // const PREVIEW_ICON_URL = 'https://www.google.com/chromecast/static/images/tv/what-is-chromecast.jpg'; 10 | const PREVIEW_ICON_URL = 'https://www.google.com/chromecast/static/images/tv/chromecast.jpg'; 11 | const PREVIEW_TITLE = 'Chromecast Stream'; 12 | 13 | const STATE_PLAYING = 'PLAYING'; 14 | // BUFFERING = 'BUFFERING', 15 | // PAUSED = 'PAUSED', 16 | // REPEAT_OFF = 'REPEAT_OFF', 17 | // REPEAT_ON = 'REPEAT_ON' 18 | // ; 19 | 20 | class Services extends EventEmitter { 21 | constructor() { 22 | super(); 23 | 24 | this.client = null; 25 | // Latest IPC event 26 | this.event = null; 27 | this.host = null; 28 | this.list = new Map(); 29 | this.isReconnect = false; 30 | this.status = false; 31 | this.url = ''; 32 | 33 | this.isPlaying = this.isPlaying.bind(this); 34 | } 35 | 36 | handleDevice(host) { 37 | this.close(); 38 | 39 | this.host = host; 40 | this.client = new CastClient(); 41 | this.client.on('error', (error) => { 42 | console.log('Client Error: %s', error.message); 43 | console.log(error); 44 | console.trace(); 45 | this.close(); 46 | }); 47 | this.client.connect(host, this.handleConnect.bind(this)); 48 | } 49 | 50 | handleConnect() { 51 | console.log('Connected, launching...'); 52 | this.client.launch(DefaultMediaReceiver, this.handleLaunch.bind(this)); 53 | } 54 | 55 | handleHeaders(err, res) { 56 | if (err || !res) { 57 | console.log(' - ERROR HEAD ', err, res); 58 | return; 59 | } 60 | 61 | console.log(' - HEAD ', res.headers); 62 | 63 | if (!this.player || !this.player.session) { 64 | console.log('Player session was closed'); 65 | if (this.host) { 66 | this.isReconnect = true; 67 | this.handleDevice(this.host); 68 | } 69 | 70 | return; 71 | } 72 | 73 | let contentType = res.headers['content-type'] || '', 74 | media = { 75 | // Here you can plug an URL to any mp4, webm, mp3 or jpg file with the proper contentType. 76 | contentId: this.url, 77 | contentType: contentType, 78 | streamType: 'BUFFERED', // or LIVE 79 | // Title and cover displayed while buffering 80 | metadata: { 81 | type: 0, 82 | metadataType: 0, 83 | title: `${PREVIEW_TITLE}: ${contentType}.`, 84 | images: [{ url: PREVIEW_ICON_URL }] 85 | } 86 | }; 87 | 88 | console.log(`App "${this.player.session.displayName}" is launched, loading media: "${this.url}"`); 89 | this.player.load(media, { autoplay: true }, this.handleLoadFile.bind(this)); 90 | } 91 | 92 | handleLaunch(err, player) { 93 | player.on('status', this.handleStatus.bind(this)); 94 | this.emit('connected', this.host); 95 | this.player = player; 96 | 97 | if (this.isReconnect && this.url) { 98 | console.log('Loading media after reconnect: ', this.url); 99 | this.isReconnect = false; 100 | this.load(); 101 | } 102 | } 103 | 104 | handleLoadFile(err, status) { 105 | console.log('Media loaded playerState=%s', status ? status.playerState : 'UNDEFINED'); 106 | 107 | if (!status) { 108 | this.emit('unsupported'); 109 | } 110 | 111 | // if (this.event) { 112 | // this.event.sender.send('playing', this.url); 113 | // } 114 | 115 | // Seek to 2 minutes after 15 seconds playing. 116 | // setTimeout(function() { 117 | // player.seek(2*60, function(err, status) { 118 | // // 119 | // }); 120 | // }, 15000); 121 | } 122 | 123 | handleService(err, service) { 124 | if (err || !service || !service.data) { 125 | if (err) { 126 | console.log('handleService() Error: ', err); 127 | } 128 | 129 | console.log('handleService() No services found.'); 130 | 131 | return; 132 | } 133 | 134 | let host = service.data, 135 | name = (service.name || 'Chromecast').replace('.local', ''); 136 | 137 | service.host = host; 138 | 139 | console.log('Found device "%s" at %s', name, host); 140 | console.log(service); 141 | 142 | this.list.set(service.host, service); 143 | // this.handleDevice.call(this, host); 144 | this.emit('service', service); 145 | } 146 | 147 | handleStatus(status) { 148 | this.status = status; 149 | this.emit('status', status); 150 | } 151 | 152 | handleStatusCallback() { 153 | console.log('handleStatusCallback()', arguments); 154 | } 155 | 156 | browse() { 157 | try { 158 | scanner(this.handleService.bind(this)); 159 | } catch (e) { 160 | // On contents reload, mdns is already running 161 | console.error('Scanner Error', e); 162 | } 163 | } 164 | 165 | isPlaying() { 166 | return this.status 167 | ? this.status.playerState === STATE_PLAYING 168 | : false; 169 | } 170 | 171 | load(url, event) { 172 | this.url = url || this.url; 173 | this.event = event || this.event; 174 | // this.player.getStatus(this.handleStatusCallback); 175 | 176 | if (!this.url) { 177 | console.log('Empty URL to load()'); 178 | return; 179 | } 180 | 181 | request(this.url, { method: 'HEAD' }, this.handleHeaders.bind(this)); 182 | } 183 | 184 | noop() { 185 | if (this.player) { 186 | this.player.getStatus((nothing, status) => { 187 | this.handleStatus(status); 188 | }); 189 | } 190 | } 191 | 192 | pause() { 193 | if (this.player) { 194 | this.player.pause(this.handleStatusCallback); 195 | } 196 | } 197 | 198 | play() { 199 | if (this.player) { 200 | this.player.play(this.handleStatusCallback); 201 | } 202 | } 203 | 204 | seek(seconds) { 205 | if (this.player) { 206 | this.player.seek(seconds, this.handleStatusCallback); 207 | } 208 | } 209 | 210 | stop() { 211 | if (this.player) { 212 | this.player.stop(this.handleStatusCallback); 213 | } 214 | this.handleStatus(false); 215 | } 216 | 217 | close() { 218 | this.host = null; 219 | this.status = false; 220 | 221 | if (this.client) { 222 | this.client.emit('close'); 223 | console.log('Client: Closed.'); 224 | 225 | this.client = null; 226 | this.emit('close'); 227 | } 228 | } 229 | } 230 | 231 | module.exports = new Services(); -------------------------------------------------------------------------------- /jsx/components/Player.jsx: -------------------------------------------------------------------------------- 1 | import RefreshIndicator from 'material-ui/RefreshIndicator'; 2 | import RaisedButton from 'material-ui/RaisedButton'; 3 | import Slider from 'material-ui/Slider'; 4 | import Snackbar from 'material-ui/Snackbar'; 5 | import TextField from 'material-ui/TextField'; 6 | 7 | // const URL = 'http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4'; 8 | 9 | const TIMER = 500; 10 | 11 | class Player extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | contentType: '', 17 | currentTime: 0, 18 | duration: 1, 19 | isFileSupported: true, 20 | isLoading: false, 21 | isPaused: false, 22 | isPlaying: false, 23 | hasFile: false, 24 | timer: null, 25 | url: '' 26 | }; 27 | 28 | // document.addEventListener('drop', this.handleFile); 29 | // document.addEventListener('dragover', this.handleFile); 30 | 31 | App.ipc.on('status', this.handleRemoteStatus); 32 | App.ipc.on('unsupported', this.handleUnsupported); 33 | App.ipc.on('url', this.handleFile); 34 | } 35 | 36 | checkURL = () => { 37 | return /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/ 38 | .test(this.state.url); 39 | } 40 | 41 | getDurationString = (time) => { 42 | const duration = time * 1000; 43 | if (duration <= 1000) { 44 | return '00:00:00'; 45 | } 46 | 47 | let seconds = parseInt((duration/1000)%60) 48 | , minutes = parseInt((duration/(1000*60))%60) 49 | , hours = parseInt((duration/(1000*60*60))%24); 50 | 51 | hours = (hours < 10) ? '0' + hours : hours; 52 | minutes = (minutes < 10) ? '0' + minutes : minutes; 53 | seconds = (seconds < 10) ? '0' + seconds : seconds; 54 | 55 | return hours + ':' + minutes + ':' + seconds; 56 | } 57 | 58 | handleChangeURL = (e) => { 59 | this.handleFocus(); 60 | this.setState({ 61 | url: e.target.value, 62 | }); 63 | } 64 | 65 | handleFile = (e, url) => { 66 | console.log('handleFile()', url); 67 | 68 | this.setState({ 69 | url: url 70 | }); 71 | 72 | this.handleQueue(); 73 | }; 74 | 75 | handleFocus = () => { 76 | // This part doesn't work :/ 77 | ReactDOM.findDOMNode(this.refs.urlField).focus(); 78 | }; 79 | 80 | handleKeyDown = (e) => { 81 | if (e && e.keyCode === 13) { 82 | this.handleQueue(e); 83 | } 84 | }; 85 | 86 | handleLoad = (e) => { 87 | if (e) { 88 | e.preventDefault(); 89 | e.stopPropagation(); 90 | } 91 | 92 | this.setState({ 93 | isLoading: true 94 | }); 95 | 96 | App.ipc.send('do', 'load', this.state.url); 97 | } 98 | 99 | handleQueue = (e) => { 100 | if (!this.state.hasFile) { 101 | this.handleLoad(e); 102 | } 103 | } 104 | 105 | handleRemoteStatus = (event, status) => { 106 | console.log('handleRemoteState()', status); 107 | let playerState = status ? status.playerState : 'IDLE', 108 | isPlaying = playerState === 'PLAYING' || playerState === 'BUFFERING', 109 | isPaused = playerState === 'PAUSED', 110 | isIDLE = playerState === 'IDLE', 111 | contentType = '', 112 | currentTime = status ? status.currentTime : 0, 113 | duration = this.state.duration 114 | ; 115 | 116 | if (status) { 117 | if (status.activeTrackIds && status.activeTrackIds.length) { 118 | console.log(' - activeTrackIds', status.activeTrackIds); 119 | } 120 | 121 | if (status.media) { 122 | console.log(' - media', status.media); 123 | duration = status.media.duration; 124 | } 125 | 126 | if (status.playerState === 'IDLE') { 127 | if (status.idleReason === 'ERROR') { 128 | status = false; 129 | } 130 | if (status.idleReason === 'FINISHED') { 131 | // TODO Play next 132 | App.ipc.send('do', 'close'); 133 | } 134 | } 135 | } 136 | 137 | this.setState({ 138 | contentType: contentType, 139 | currentTime: currentTime, 140 | duration: duration, 141 | isFileSupported: status !== false, 142 | isLoading: !isPlaying && !isPaused && !isIDLE, 143 | isPaused: isPaused, 144 | isPlaying: isPlaying, 145 | hasFile: !isIDLE, 146 | status: status, 147 | timer: this.handlePlay() 148 | }); 149 | } 150 | 151 | handlePlay = () => { 152 | if (this.state.timer) { 153 | clearTimeout(this.state.timer); 154 | } 155 | 156 | if (!this.state.hasFile) { 157 | return; 158 | } 159 | 160 | return setTimeout(() => { 161 | App.ipc.send('do', 'noop'); 162 | }, TIMER); 163 | } 164 | 165 | handleUnsupported = () => { 166 | this.handleRemoteStatus(false, false); 167 | } 168 | 169 | seek = (event, value) => { 170 | console.log('seek()', value); 171 | App.ipc.send('do', 'seek', value); 172 | this.setState({ 173 | currentTime: value 174 | }); 175 | } 176 | 177 | pause = () => { 178 | App.ipc.send('do', 'pause'); 179 | } 180 | 181 | play = () => { 182 | App.ipc.send('do', 'play'); 183 | } 184 | 185 | stop = () => { 186 | this.setState({ 187 | isLoading: true, 188 | hasFile: false 189 | }); 190 | App.ipc.send('do', 'stop'); 191 | } 192 | 193 | render() { 194 | let isURL = this.checkURL(), 195 | duration = this.state.duration, 196 | currentTime = this.state.currentTime; 197 | 198 | if (currentTime >= duration) { 199 | currentTime = 0; 200 | } 201 | 202 | return ( 203 |

204 | } 207 | /> 208 | 209 | } 214 | fullWidth={true} 215 | hintText="https://" 216 | multiLine={true} 217 | value={this.state.url} 218 | onChange={this.handleChangeURL} 219 | onKeyDown={this.handleKeyDown} 220 | /> 221 |
222 |
223 | 224 | 226 | 227 | 229 | 230 | { this.state.hasFile 231 | ? ( 232 | 233 | { this.state.isPlaying 234 | ? '' 235 | : 236 | } 237 | { this.state.isPaused 238 | ? '' 239 | : 240 | } 241 | 243 | 244 |
245 | 246 |
247 | 251 |
252 |
) 253 | : (this.state.isLoading ? : '') 257 | } 258 |
259 | ); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /app/src/window.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Electron = require('electron'); 4 | const EventEmitter = require('events').EventEmitter; 5 | const BrowserWindow = Electron.BrowserWindow; 6 | const Menu = Electron.Menu; 7 | const Tray = Electron.Tray; 8 | const path = require('path'); 9 | const url = require('url'); 10 | 11 | const APP_PATH = path.normalize(__dirname + '/..'); 12 | const ICO_PATH = path.normalize(APP_PATH + '/img/icon'); 13 | const isDev = (process.env.npm_package_scripts_start && /gulp\sbuild/.test(process.env.npm_package_scripts_start)) 14 | || process.env.NODE_ENV === 'development'; 15 | const isOSX = (process.platform === 'darwin'); 16 | const appParams = { 17 | width: 900, 18 | height: 480, 19 | margin: 11, 20 | icon: { 21 | main: ICO_PATH + '512' + (isOSX ? '.icns' : '.ico'), 22 | tray: ICO_PATH + (isOSX ? 'Mac' : 'Win') + 'Template.png' 23 | } 24 | }; 25 | 26 | class MainWindow extends EventEmitter { 27 | constructor() { 28 | super(); 29 | this.isWindowReady = false; 30 | this.service = null; 31 | this.tray = null; 32 | this.window = null; 33 | } 34 | 35 | createWindow(callback) { 36 | // App main Window 37 | this.window = new BrowserWindow({ 38 | // alwaysOnTop: true, 39 | autoHideMenuBar: true, 40 | dir: APP_PATH, 41 | // frame: false, 42 | // fullscreenable: false, 43 | // maximizable: false, 44 | // minimizable: false, 45 | movable: true, 46 | resizable: false, 47 | transparent: false, 48 | // hasShadow: false, 49 | icon: appParams.icon.main, 50 | show: false, 51 | showDockIcon: true, 52 | titleBarStyle: 'hidden-inset', 53 | width: appParams.width + appParams.margin * 2, 54 | height: appParams.height + appParams.margin * 2, 55 | webPreferences: { 56 | defaultEncoding: 'utf-8', 57 | webgl: false, 58 | webaudio: false 59 | } 60 | }); 61 | 62 | // Load the index.html of the app. 63 | let indexFile = url.format({ 64 | protocol: 'file', 65 | slashes: true, 66 | pathname: path.resolve(path.join(APP_PATH, 'index.html')) 67 | }); 68 | this.window.loadURL(indexFile); 69 | this.window.webContents.once('did-finish-load', () => { 70 | if (callback) { 71 | callback.call(this); 72 | } 73 | 74 | this.window.show(); 75 | }); 76 | 77 | this.window.webContents.on('will-navigate', (e, url) => { 78 | e.preventDefault(); 79 | this.send('url', url); 80 | }); 81 | 82 | // Menu 83 | require('./menu'); 84 | 85 | // Open the DevTools. 86 | if (isDev) { 87 | this.window.webContents.openDevTools({ 88 | detach: true 89 | }); 90 | } 91 | 92 | // Emitted when the window is closed. 93 | this.window.on('closed', () => { 94 | // Dereference the window object, usually you would store windows 95 | // in an array if your app supports multi windows, this is the time 96 | // when you should delete the corresponding element. 97 | this.window = null; 98 | this.tray = null; 99 | }); 100 | this.window.setVisibleOnAllWorkspaces(true); 101 | 102 | // Set window position 103 | this.setWindowPosition(); 104 | 105 | // Tray icon 106 | try { 107 | this.tray = new Tray(appParams.icon.tray); 108 | this.tray.on('drop-text', (event, text) => this.send('url', text)); 109 | this.tray.on('right-click', () => this.showWindow()); 110 | this.setMenu(); 111 | // this.tray.on('click', this.handleClick.bind(this)); 112 | } catch (e) { 113 | console.error(e); 114 | } finally { 115 | 116 | } 117 | } 118 | 119 | close() { 120 | this.window.close(); 121 | } 122 | 123 | execute(code) { 124 | this.window.webContents.executeJavaScript(code); 125 | } 126 | 127 | getScreenSize() { 128 | let screen = Electron.screen; 129 | 130 | return screen.getDisplayNearestPoint(screen.getCursorScreenPoint()) 131 | .workArea; 132 | } 133 | 134 | getWindowSize() { 135 | return this.window.getSize(); 136 | } 137 | 138 | handleClick(event, bounds) { 139 | let e = event || {}; 140 | 141 | if (this.toggleWindow(e.altKey || e.shiftKey || e.ctrlKey || e.metaKey)) { 142 | this.setWindowPosition(bounds); 143 | } 144 | } 145 | 146 | init(callback) { 147 | if (this.isWindowReady) { 148 | this.createWindow(callback); 149 | } else { 150 | // This method will be called when Electron has finished 151 | // initialization and is ready to create browser windows. 152 | Electron.app.once('ready', () => { 153 | this.createWindow(callback); 154 | }); 155 | } 156 | } 157 | 158 | send() { 159 | if (this.window && this.window.webContents) { 160 | this.window.webContents.send.apply(this.window.webContents, arguments); 161 | } 162 | } 163 | 164 | setMenu() { 165 | if (!this.tray) { 166 | console.error('Tray doesn\'t exist.'); 167 | return; 168 | } 169 | 170 | let menuItems = [], 171 | services = this.service.list, 172 | current = this.service.host; 173 | 174 | menuItems.push({ 175 | label: 'Open', 176 | click: () => { 177 | this.showWindow(); 178 | } 179 | }, { type: 'separator' }); 180 | 181 | if (services && services.size) { 182 | services.forEach((device) => { 183 | menuItems.push({ 184 | checked: device.host === current, 185 | enabled: device.host !== current, 186 | label: device.name, 187 | type: 'checkbox', 188 | click: () => { 189 | if (this.service) { 190 | this.service.handleDevice.call(this.service, device.host); 191 | } 192 | } 193 | }); 194 | }); 195 | 196 | menuItems.push({ 197 | enabled: !!current, 198 | label: 'Disconnect', 199 | click: () => { 200 | if (this.service) { 201 | this.service.close.call(this.service); 202 | } 203 | } 204 | }); 205 | 206 | menuItems.push({ type: 'separator' }); 207 | menuItems.push({ 208 | label: 'Play', 209 | enabled: !!current && !!this.service.url && !this.service.isPlaying(), 210 | click: () => { 211 | if (this.service) { 212 | if (this.service.status) { 213 | this.service.play.call(this.service); 214 | } else if (this.service.url) { 215 | this.service.load.call(this.service, this.service.url); 216 | } 217 | } 218 | } 219 | }); 220 | menuItems.push({ 221 | label: 'Pause', 222 | enabled: this.service.isPlaying(), 223 | click: () => { 224 | if (this.service) { 225 | this.service.pause.call(this.service); 226 | } 227 | } 228 | }); 229 | menuItems.push({ 230 | label: 'Stop', 231 | enabled: this.service.isPlaying(), 232 | click: () => { 233 | if (this.service) { 234 | this.service.stop.call(this.service); 235 | } 236 | } 237 | }); 238 | menuItems.push({ type: 'separator' }); 239 | menuItems.push({ label: 'Next', enabled: false }); 240 | menuItems.push({ label: 'Previous', enabled: false }); 241 | } else { 242 | menuItems.push({ 243 | label: 'Discover Devices', 244 | enabled: false 245 | }); 246 | } 247 | 248 | menuItems.push({ type: 'separator' }); 249 | menuItems.push({ 250 | label: 'Quit', 251 | accelerator: isOSX ? 'Command+Q' : 'Alt+F4', 252 | click: this.close.bind(this) 253 | }); 254 | 255 | console.log('menuItems', menuItems); 256 | 257 | this.tray.setContextMenu( 258 | Menu.buildFromTemplate(menuItems) 259 | ); 260 | } 261 | 262 | setWindowPosition(bounds) { 263 | this.showWindow(); 264 | 265 | let x = 0, y = 0, 266 | screenSize = this.getScreenSize(), 267 | windowSize = this.getWindowSize(); 268 | 269 | if (bounds && (bounds.x || bounds.y)) { 270 | let trayCenter = { 271 | x: isOSX 272 | ? Math.floor(bounds.x - windowSize[0] / 2 + bounds.width / 2) 273 | : Math.floor(screenSize.x + (screenSize.width - windowSize[0])), 274 | y: isOSX 275 | ? screenSize.y 276 | : Math.floor(screenSize.height - (windowSize[1] - screenSize.y)) 277 | }; 278 | 279 | // According to the Tray bounds 280 | x = bounds.x < 100 281 | // Left 282 | ? screenSize.x 283 | // Right 284 | : trayCenter.x; 285 | y = bounds.y < 100 286 | // Top 287 | ? screenSize.y 288 | // Bottom 289 | : trayCenter.y; 290 | } else { 291 | // Center of the Screen 292 | x = Math.floor(screenSize.x + (screenSize.width / 2 - windowSize[0] / 2)); 293 | y = Math.floor((screenSize.height + screenSize.y) / 2 - windowSize[1] / 2); 294 | } 295 | 296 | this.window.setPosition(x, y, false); 297 | } 298 | 299 | showWindow() { 300 | this.window.show(); 301 | this.window.focus(); 302 | } 303 | 304 | toggleWindow(ifHide) { 305 | let isVisibleAndFocused = (process.platform === 'darwin') 306 | ? this.window.isVisible() && this.window.isFocused() 307 | : this.window.isVisible(); 308 | 309 | if (isVisibleAndFocused || ifHide) { 310 | this.window.hide(); 311 | return false; 312 | } 313 | 314 | this.showWindow(); 315 | 316 | return true; 317 | } 318 | } 319 | 320 | let main = new MainWindow(); 321 | 322 | // This method will be called when Electron has finished 323 | // initialization and is ready to create browser windows. 324 | Electron.app.once('ready', () => { 325 | main.isWindowReady = true; 326 | }); 327 | 328 | // Quit when all windows are closed. 329 | Electron.app.on('window-all-closed', function () { 330 | // On OS X it is common for applications and their menu bar 331 | // to stay active until the user quits explicitly with Cmd + Q 332 | //if (process.platform !== 'darwin') { 333 | Electron.app.quit(); 334 | //} 335 | }); 336 | 337 | Electron.app.on('activate', (/*event, hasVisibleWindows*/) => { 338 | if (isOSX && main.window && !main.window.isVisible()) { 339 | main.showWindow(); 340 | } 341 | }); 342 | 343 | module.exports = main; 344 | -------------------------------------------------------------------------------- /resources/old/iconTemplate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 21 | 30 | 39 | 48 | 57 | 66 | 75 | 84 | 93 | 94 | 95 | 104 | 105 | 114 | 115 | 116 | 125 | 134 | 143 | 152 | 161 | 170 | 179 | 188 | 197 | 206 | 207 | 208 | 217 | 226 | 235 | 244 | 253 | 262 | 271 | 280 | 289 | 298 | 299 | 300 | 309 | 318 | 327 | 336 | 345 | 354 | 363 | 372 | 381 | 390 | 391 | 392 | 401 | 410 | 419 | 428 | 437 | 446 | 455 | 464 | 473 | 482 | 483 | 484 | 493 | 502 | 511 | 520 | 529 | 538 | 547 | 556 | 565 | 574 | 575 | 576 | 585 | 594 | 603 | 612 | 621 | 630 | 639 | 648 | 657 | 666 | 667 | 668 | 677 | 686 | 695 | 704 | 713 | 722 | 731 | 740 | 749 | 758 | 759 | 760 | 769 | 778 | 787 | 796 | 805 | 814 | 823 | 832 | 841 | 850 | 851 | 852 | 853 | 862 | 871 | 880 | 889 | 898 | 907 | 916 | 925 | 934 | 943 | 944 | 945 | 946 | 947 | 949 | 950 | 951 | 952 | 953 | 954 | --------------------------------------------------------------------------------