├── app ├── utils │ └── .gitkeep ├── mirror │ ├── styles │ │ ├── custom.css │ │ ├── roboto.css │ │ └── main.css │ ├── modules │ │ └── default │ │ │ ├── Compliments │ │ │ ├── Compliments.css │ │ │ ├── core │ │ │ │ └── utils.js │ │ │ ├── README.md │ │ │ └── index.js │ │ │ ├── Snow │ │ │ ├── images │ │ │ │ ├── flake1.png │ │ │ │ ├── flake2.png │ │ │ │ └── flake3.png │ │ │ ├── Snow.css │ │ │ ├── Flake.js │ │ │ └── index.js │ │ │ ├── Jokes │ │ │ ├── core │ │ │ │ └── utils.js │ │ │ └── index.js │ │ │ ├── CurrentWeather │ │ │ ├── CurrentWeather.css │ │ │ ├── WeatherDetail.js │ │ │ ├── core │ │ │ │ └── utils.js │ │ │ └── index.js │ │ │ ├── Calendar │ │ │ ├── Calendar.css │ │ │ ├── core │ │ │ │ └── utils.js │ │ │ └── README.md │ │ │ ├── WeatherForecast │ │ │ ├── WeatherForecast.css │ │ │ ├── core │ │ │ │ └── utils.js │ │ │ └── index.js │ │ │ ├── Clock │ │ │ ├── Clock.css │ │ │ ├── README.md │ │ │ └── index.js │ │ │ └── NewsFeed │ │ │ └── index.js │ ├── installers │ │ ├── rm.sh │ │ ├── postinstall │ │ │ └── postinstall.sh │ │ ├── pm2_ReactiveMirror.json │ │ └── raspberry.sh │ ├── splashscreen │ │ ├── splash.png │ │ ├── splash_halt.png │ │ ├── ReactiveMirror.plymouth │ │ └── ReactiveMirror.script │ ├── core │ │ ├── notifications.js │ │ ├── animations.js │ │ ├── utils.js │ │ └── components.js │ └── config │ │ ├── defaults.js │ │ └── config.js.sample ├── app.icns ├── reducers │ └── index.js ├── containers │ ├── MirrorContainer.js │ ├── Root.js │ └── App.js ├── store │ ├── configureStore.js │ ├── configureStore.prod.js │ └── configureStore.dev.js ├── app.global.css ├── routes.js ├── components │ ├── Mirror.css │ └── Mirror.js ├── package.json ├── index.js ├── .eslintrc ├── app.html ├── main.dev.js └── menu.js ├── internals ├── mocks │ └── fileMock.js ├── flow │ ├── WebpackAsset.js.flow │ └── CSSModule.js.flow ├── img │ ├── js.png │ ├── flow.png │ ├── jest.png │ ├── npm.png │ ├── react.png │ ├── redux.png │ ├── yarn.png │ ├── eslint.png │ ├── webpack.png │ ├── js-padded.png │ ├── flow-padded.png │ ├── jest-padded.png │ ├── react-padded.png │ ├── react-router.png │ ├── redux-padded.png │ ├── yarn-padded.png │ ├── eslint-padded.png │ ├── flow-padded-90.png │ ├── jest-padded-90.png │ ├── react-padded-90.png │ ├── redux-padded-90.png │ ├── webpack-padded.png │ ├── yarn-padded-90.png │ ├── eslint-padded-90.png │ ├── webpack-padded-90.png │ ├── react-router-padded.png │ └── react-router-padded-90.png └── scripts │ ├── CheckNodeEnv.js │ └── CheckBuiltsExist.js ├── .stylelintrc ├── .gitattributes ├── flow-typed └── module_vx.x.x.js ├── resources ├── icon.ico ├── icon.png ├── icon.icns └── icons │ ├── 16x16.png │ ├── 24x24.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 64x64.png │ ├── 96x96.png │ ├── 128x128.png │ ├── 256x256.png │ ├── 512x512.png │ └── 1024x1024.png ├── Reactive Mirror.png ├── assets ├── LogoBlack.png └── ReactiveMirror.png ├── webpack.config.eslint.js ├── test ├── example.js ├── .eslintrc ├── runTests.js └── e2e │ └── e2e.spec.js ├── .editorconfig ├── .vscode └── settings.json ├── appveyor.yml ├── .babelrc ├── .travis.yml ├── .flowconfig ├── .eslintignore ├── .gitignore ├── LICENSE ├── .eslintrc ├── webpack.config.base.js ├── webpack.config.main.prod.js ├── webpack.config.renderer.prod.js ├── webpack.config.renderer.dev.dll.js ├── package.json ├── webpack.config.renderer.dev.js ├── README.md └── CHANGELOG.md /app/utils/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mirror/styles/custom.css: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Compliments/Compliments.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /internals/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /app/mirror/installers/rm.sh: -------------------------------------------------------------------------------- 1 | cd ~/ReactiveMirror 2 | DISPLAY=:0 npm start 3 | -------------------------------------------------------------------------------- /internals/flow/WebpackAsset.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | declare export default string 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.png binary 3 | *.ico binary 4 | *.icns binary 5 | -------------------------------------------------------------------------------- /app/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/app/app.icns -------------------------------------------------------------------------------- /flow-typed/module_vx.x.x.js: -------------------------------------------------------------------------------- 1 | declare module 'module' { 2 | declare module.exports: any; 3 | } 4 | -------------------------------------------------------------------------------- /internals/flow/CSSModule.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare export default { [key: string]: string } -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/resources/icon.png -------------------------------------------------------------------------------- /Reactive Mirror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/Reactive Mirror.png -------------------------------------------------------------------------------- /assets/LogoBlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/assets/LogoBlack.png -------------------------------------------------------------------------------- /internals/img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/js.png -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/resources/icon.icns -------------------------------------------------------------------------------- /internals/img/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/flow.png -------------------------------------------------------------------------------- /internals/img/jest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/jest.png -------------------------------------------------------------------------------- /internals/img/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/npm.png -------------------------------------------------------------------------------- /internals/img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/react.png -------------------------------------------------------------------------------- /internals/img/redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/redux.png -------------------------------------------------------------------------------- /internals/img/yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/yarn.png -------------------------------------------------------------------------------- /assets/ReactiveMirror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/assets/ReactiveMirror.png -------------------------------------------------------------------------------- /internals/img/eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/eslint.png -------------------------------------------------------------------------------- /internals/img/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/webpack.png -------------------------------------------------------------------------------- /resources/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/resources/icons/16x16.png -------------------------------------------------------------------------------- /resources/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/resources/icons/24x24.png -------------------------------------------------------------------------------- /resources/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/resources/icons/32x32.png -------------------------------------------------------------------------------- /resources/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/resources/icons/48x48.png -------------------------------------------------------------------------------- /resources/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/resources/icons/64x64.png -------------------------------------------------------------------------------- /resources/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/resources/icons/96x96.png -------------------------------------------------------------------------------- /app/mirror/installers/postinstall/postinstall.sh: -------------------------------------------------------------------------------- 1 | echo "\033[32mReactiveMirror installation successful!" 2 | exit 0 3 | -------------------------------------------------------------------------------- /internals/img/js-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/js-padded.png -------------------------------------------------------------------------------- /resources/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/resources/icons/128x128.png -------------------------------------------------------------------------------- /resources/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/resources/icons/256x256.png -------------------------------------------------------------------------------- /resources/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/resources/icons/512x512.png -------------------------------------------------------------------------------- /internals/img/flow-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/flow-padded.png -------------------------------------------------------------------------------- /internals/img/jest-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/jest-padded.png -------------------------------------------------------------------------------- /internals/img/react-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/react-padded.png -------------------------------------------------------------------------------- /internals/img/react-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/react-router.png -------------------------------------------------------------------------------- /internals/img/redux-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/redux-padded.png -------------------------------------------------------------------------------- /internals/img/yarn-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/yarn-padded.png -------------------------------------------------------------------------------- /resources/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/resources/icons/1024x1024.png -------------------------------------------------------------------------------- /webpack.config.eslint.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | 3 | module.exports = require('./webpack.config.renderer.dev'); 4 | -------------------------------------------------------------------------------- /internals/img/eslint-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/eslint-padded.png -------------------------------------------------------------------------------- /internals/img/flow-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/flow-padded-90.png -------------------------------------------------------------------------------- /internals/img/jest-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/jest-padded-90.png -------------------------------------------------------------------------------- /internals/img/react-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/react-padded-90.png -------------------------------------------------------------------------------- /internals/img/redux-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/redux-padded-90.png -------------------------------------------------------------------------------- /internals/img/webpack-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/webpack-padded.png -------------------------------------------------------------------------------- /internals/img/yarn-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/yarn-padded-90.png -------------------------------------------------------------------------------- /app/mirror/splashscreen/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/app/mirror/splashscreen/splash.png -------------------------------------------------------------------------------- /internals/img/eslint-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/eslint-padded-90.png -------------------------------------------------------------------------------- /internals/img/webpack-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/webpack-padded-90.png -------------------------------------------------------------------------------- /internals/img/react-router-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/react-router-padded.png -------------------------------------------------------------------------------- /app/mirror/splashscreen/splash_halt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/app/mirror/splashscreen/splash_halt.png -------------------------------------------------------------------------------- /internals/img/react-router-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/internals/img/react-router-padded-90.png -------------------------------------------------------------------------------- /test/example.js: -------------------------------------------------------------------------------- 1 | describe('description', () => { 2 | it('should have description', () => { 3 | expect(1 + 2).toBe(3); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Snow/images/flake1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/app/mirror/modules/default/Snow/images/flake1.png -------------------------------------------------------------------------------- /app/mirror/modules/default/Snow/images/flake2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/app/mirror/modules/default/Snow/images/flake2.png -------------------------------------------------------------------------------- /app/mirror/modules/default/Snow/images/flake3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cristian006/ReactiveMirror/HEAD/app/mirror/modules/default/Snow/images/flake3.png -------------------------------------------------------------------------------- /app/reducers/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { combineReducers } from 'redux'; 3 | import { routerReducer as router } from 'react-router-redux'; 4 | 5 | const rootReducer = combineReducers({ 6 | router, 7 | }); 8 | 9 | export default rootReducer; 10 | -------------------------------------------------------------------------------- /app/mirror/installers/pm2_ReactiveMirror.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps" : [{ 3 | "name" : "ReactiveMirror", 4 | "script" : "/home/pi/ReactiveMirror/app/mirror/installers/rm.sh", 5 | "watch" : ["/home/pi/ReactiveMirror/app/mirror/config/config.js"] 6 | }] 7 | } 8 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest/globals": true 4 | }, 5 | "plugins": [ 6 | "jest" 7 | ], 8 | "rules": { 9 | "jest/no-disabled-tests": "warn", 10 | "jest/no-focused-tests": "error", 11 | "jest/no-identical-title": "error" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/containers/MirrorContainer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import Mirror from '../components/Mirror'; 4 | 5 | export default class MirrorContainer extends Component { 6 | render() { 7 | return ( 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/mirror/splashscreen/ReactiveMirror.plymouth: -------------------------------------------------------------------------------- 1 | [Plymouth Theme] 2 | Name=ReactiveMirror 3 | Description=Mirror Splash 4 | ModuleName=script 5 | 6 | [script] 7 | ImageDir=/usr/share/plymouth/themes/ReactiveMirror 8 | ScriptFile=/usr/share/plymouth/themes/ReactiveMirror/ReactiveMirror.script 9 | -------------------------------------------------------------------------------- /app/store/configureStore.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | if (process.env.NODE_ENV === 'production') { 3 | module.exports = require('./configureStore.prod'); // eslint-disable-line global-require 4 | } else { 5 | module.exports = require('./configureStore.dev'); // eslint-disable-line global-require 6 | } 7 | -------------------------------------------------------------------------------- /app/app.global.css: -------------------------------------------------------------------------------- 1 | @import "~font-awesome/css/font-awesome.css"; 2 | /* @import url('https://fonts.googleapis.com/css?family=Roboto+Condensed:300,400,700');*/ 3 | @import url('./mirror/styles/main.css'); 4 | @import url('./mirror/styles/custom.css'); 5 | @import url('./mirror/styles/roboto.css'); 6 | -------------------------------------------------------------------------------- /app/routes.js: -------------------------------------------------------------------------------- 1 | /* eslint flowtype-errors/show-errors: 0 */ 2 | import React from 'react'; 3 | import { Switch, Route } from 'react-router'; 4 | import App from './containers/App'; 5 | import MirrorContainer from './containers/MirrorContainer'; 6 | 7 | export default () => ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{json,js,jsx,html,css,yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [.eslintrc] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /app/mirror/core/notifications.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | const emitter = new EventEmitter(); 4 | 5 | const removeAllListeners = emitter.removeAllListeners.bind(emitter); 6 | 7 | emitter.removeAllListeners = (...args) => { 8 | if (args.length === 0) { 9 | throw new Error('Specify Name'); 10 | } 11 | removeAllListeners(...args); 12 | }; 13 | 14 | emitter.on('error', () => {}); 15 | 16 | export default emitter; 17 | -------------------------------------------------------------------------------- /internals/scripts/CheckNodeEnv.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import chalk from 'chalk'; 3 | 4 | export default function CheckNodeEnv(expectedEnv: string) { 5 | if (!expectedEnv) { 6 | throw new Error('"expectedEnv" not set'); 7 | } 8 | 9 | if (process.env.NODE_ENV !== expectedEnv) { 10 | console.log(chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 12 | )); 13 | process.exit(2); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/runTests.js: -------------------------------------------------------------------------------- 1 | const spawn = require('cross-spawn'); 2 | const path = require('path'); 3 | 4 | const s = `\\${path.sep}`; 5 | const pattern = process.argv[2] === 'e2e' 6 | ? `test${s}e2e${s}.+\\.spec\\.js` 7 | : `test${s}(?!e2e${s})[^${s}]+${s}.+\\.spec\\.js$`; 8 | 9 | const result = spawn.sync( 10 | path.normalize('./node_modules/.bin/jest'), 11 | [pattern, ...process.argv.slice(2)], 12 | { stdio: 'inherit' } 13 | ); 14 | 15 | process.exit(result.status); 16 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Jokes/core/utils.js: -------------------------------------------------------------------------------- 1 | import request from 'request'; 2 | 3 | export function getJoke(callback) { 4 | request({ 5 | url: 'https://icanhazdadjoke.com/', 6 | headers: { Accept: 'application/json' } 7 | }, (error, response, body) => { 8 | if (!error && response.statusCode === 200) { 9 | const info = JSON.parse(body); 10 | callback(info.joke); 11 | } else { 12 | callback('couldn\'t get any jokes'); 13 | } 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /app/mirror/modules/default/CurrentWeather/CurrentWeather.css: -------------------------------------------------------------------------------- 1 | .currentweather .weathericon, 2 | .currentweather .fa-home { 3 | font-size: 75%; 4 | line-height: 65px; 5 | display: inline-block; 6 | -ms-transform: translate(0, -3px); /* IE 9 */ 7 | -webkit-transform: translate(0, -3px); /* Safari */ 8 | transform: translate(0, -3px); 9 | } 10 | 11 | .currentweather .humidityIcon { 12 | padding-right: 4px; 13 | } 14 | 15 | .currentweather .humidity-padding { 16 | padding-bottom: 6px; 17 | } 18 | -------------------------------------------------------------------------------- /app/containers/Root.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { ConnectedRouter } from 'react-router-redux'; 5 | import Routes from '../routes'; 6 | 7 | type RootType = { 8 | store: {}, 9 | history: {} 10 | }; 11 | 12 | export default function Root({ store, history }: RootType) { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Calendar/Calendar.css: -------------------------------------------------------------------------------- 1 | .calendar .symbol { 2 | padding-left: 0; 3 | padding-right: 10px; 4 | font-size: 80%; 5 | vertical-align: top; 6 | } 7 | 8 | .calendar .symbol span { 9 | display: inline-block; 10 | -ms-transform: translate(0, 2px); /* IE 9 */ 11 | -webkit-transform: translate(0, 2px); /* Safari */ 12 | transform: translate(0, 2px); 13 | } 14 | 15 | .calendar .title { 16 | padding-left: 0; 17 | padding-right: 0; 18 | } 19 | 20 | .calendar .time { 21 | padding-left: 30px; 22 | text-align: right; 23 | vertical-align: top; 24 | } 25 | -------------------------------------------------------------------------------- /app/mirror/modules/default/WeatherForecast/WeatherForecast.css: -------------------------------------------------------------------------------- 1 | .WeatherForecast .day { 2 | padding-left: 0; 3 | padding-right: 25px; 4 | } 5 | 6 | .WeatherForecast .weathericon { 7 | padding-right: 30px; 8 | text-align: center; 9 | } 10 | 11 | .WeatherForecast .minTemp { 12 | padding-left: 20px; 13 | padding-right: 0; 14 | } 15 | 16 | .WeatherForecast .rain { 17 | padding-left: 20px; 18 | padding-right: 0; 19 | } 20 | 21 | .WeatherForecast tr.colored .minTemp { 22 | color: #BCDDFF; 23 | } 24 | 25 | .WeatherForecast tr.colored .maxTemp { 26 | color: #FF8E99; 27 | } 28 | -------------------------------------------------------------------------------- /app/components/Mirror.css: -------------------------------------------------------------------------------- 1 | .mirror { 2 | margin: 60px; 3 | position: absolute; 4 | height: calc(100% - 120px); 5 | width: calc(100% - 120px); 6 | color: #aaa; 7 | font-family: 'Roboto Condensed', sans-serif; 8 | font-weight: 400; 9 | font-size: 2em; 10 | line-height: 1.5em; 11 | -webkit-font-smoothing: antialiased; 12 | } 13 | 14 | .config { 15 | position: absolute; 16 | top: 25; 17 | margin: 0 auto; 18 | padding: 24px; 19 | border-radius: 5px; 20 | border-width: 2px; 21 | border-color: #fff; 22 | background-color: #eeee; 23 | height: 64px; 24 | width: 64px; 25 | } 26 | -------------------------------------------------------------------------------- /app/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | import { createBrowserHistory } from 'history'; 5 | import { routerMiddleware } from 'react-router-redux'; 6 | import rootReducer from '../reducers'; 7 | 8 | const history = createBrowserHistory(); 9 | const router = routerMiddleware(history); 10 | const enhancer = applyMiddleware(thunk, router); 11 | 12 | function configureStore(initialState?) { 13 | return createStore(rootReducer, initialState, enhancer); 14 | } 15 | 16 | export default { configureStore, history }; 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | "flow.useNPMPackagedFlow": true, 4 | "search.exclude": { 5 | ".git": true, 6 | ".eslintcache": true, 7 | "app/dist": true, 8 | "app/main.prod.js": true, 9 | "app/main.prod.js.map": true, 10 | "bower_components": true, 11 | "dll": true, 12 | "flow-typed": true, 13 | "release": true, 14 | "node_modules": true, 15 | "npm-debug.log.*": true, 16 | "test/**/__snapshots__": true, 17 | "yarn.lock": true 18 | }, 19 | "files.eol": "\n", 20 | "editor.tabSize": 2, 21 | "files.insertFinalNewline": true 22 | } 23 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | os: unstable 2 | 3 | environment: 4 | matrix: 5 | - nodejs_version: 8 6 | - nodejs_version: 7 7 | 8 | cache: 9 | - "%LOCALAPPDATA%/Yarn" 10 | - node_modules -> package.json 11 | - app/node_modules -> app/package.json 12 | 13 | matrix: 14 | fast_finish: true 15 | 16 | build: off 17 | 18 | version: '{build}' 19 | 20 | shallow_clone: true 21 | 22 | clone_depth: 1 23 | 24 | install: 25 | - ps: Install-Product node $env:nodejs_version 26 | - set CI=true 27 | - yarn 28 | - cd app && yarn 29 | 30 | test_script: 31 | - node --version 32 | - yarn lint 33 | - yarn package 34 | - yarn test 35 | - yarn test-e2e 36 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { "node": 7 }, 5 | "useBuiltIns": true 6 | }], 7 | "stage-0", 8 | "react" 9 | ], 10 | "plugins": ["add-module-exports", "dynamic-import-webpack"], 11 | "env": { 12 | "production": { 13 | "presets": ["react-optimize"], 14 | "plugins": ["babel-plugin-dev-expression"] 15 | }, 16 | "development": { 17 | "plugins": [ 18 | "transform-class-properties", 19 | "transform-es2015-classes", 20 | ["flow-runtime", { 21 | "assert": true, 22 | "annotate": true 23 | }] 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/mirror/core/animations.js: -------------------------------------------------------------------------------- 1 | /*export function fade(opacity: number) { 2 | let op = opacity; // initial opacity 3 | let timer = setInterval(() => { 4 | if (op <= 0.1) { 5 | clearInterval(timer); 6 | element.style.display = 'none'; 7 | } 8 | element.style.opacity = op; 9 | element.style.filter = 'alpha(opacity=' + op * 100 + ")"; 10 | op -= op * 0.1; 11 | }, 50); 12 | } 13 | 14 | export function unfade(element) { 15 | var op = 0.1; // initial opacity 16 | element.style.display = 'block'; 17 | var timer = setInterval(function () { 18 | if (op >= 1){ 19 | clearInterval(timer); 20 | } 21 | element.style.opacity = op; 22 | element.style.filter = 'alpha(opacity=' + op * 100 + ")"; 23 | op += op * 0.1; 24 | }, 10); 25 | }*/ 26 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactive-mirror", 3 | "productName": "Reactive Mirror", 4 | "version": "0.1.0", 5 | "description": "Electron React Mirror App for Raspberry Pi", 6 | "main": "./main.prod.js", 7 | "author": { 8 | "name": "Cristian Ponce", 9 | "email": "cristianrponce06@gmail.com", 10 | "url": "https://github.com/cristian006" 11 | }, 12 | "scripts": { 13 | "postinstall": "npm rebuild --runtime=electron --target=1.6.6 --disturl=https://atom.io/download/atom-shell --build-from-source" 14 | }, 15 | "license": "MIT", 16 | "dependencies": { 17 | "classnames": "^2.2.5", 18 | "ical": "^0.5.0", 19 | "moment": "^2.19.3", 20 | "moment-timezone": "^0.5.14", 21 | "roboto-fontface": "^0.8.0", 22 | "valid-url": "^1.0.9", 23 | "weathericons": "^2.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import Root from './containers/Root'; 5 | import { configureStore, history } from './store/configureStore'; 6 | import './app.global.css'; 7 | 8 | const store = configureStore(); 9 | 10 | render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | 17 | if (module.hot) { 18 | module.hot.accept('./containers/Root', () => { 19 | const NextRoot = require('./containers/Root'); // eslint-disable-line global-require 20 | render( 21 | 22 | 23 | , 24 | document.getElementById('root') 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /internals/scripts/CheckBuiltsExist.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Check if the renderer and main bundles are built 3 | import path from 'path'; 4 | import chalk from 'chalk'; 5 | import fs from 'fs'; 6 | 7 | function CheckBuildsExist() { 8 | const mainPath = path.join(__dirname, '..', '..', 'app', 'main.prod.js'); 9 | const rendererPath = path.join(__dirname, '..', '..', 'app', 'dist', 'renderer.prod.js'); 10 | 11 | if (!fs.existsSync(mainPath)) { 12 | throw new Error(chalk.whiteBright.bgRed.bold( 13 | 'The main process is not built yet. Build it by running "npm run build-main"' 14 | )); 15 | } 16 | 17 | if (!fs.existsSync(rendererPath)) { 18 | throw new Error(chalk.whiteBright.bgRed.bold( 19 | 'The renderer process is not built yet. Build it by running "npm run build-renderer"' 20 | )); 21 | } 22 | } 23 | 24 | CheckBuildsExist(); 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - 8 7 | - 7 8 | 9 | cache: 10 | yarn: true 11 | directories: 12 | - node_modules 13 | - app/node_modules 14 | 15 | addons: 16 | apt: 17 | sources: 18 | - ubuntu-toolchain-r-test 19 | packages: 20 | - g++-4.8 21 | - icnsutils 22 | - graphicsmagick 23 | - xz-utils 24 | - xorriso 25 | 26 | install: 27 | - export CXX="g++-4.8" 28 | - yarn 29 | - cd app && yarn && cd .. 30 | - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16" 31 | 32 | before_script: 33 | - export DISPLAY=:99.0 34 | - sh -e /etc/init.d/xvfb start & 35 | - sleep 3 36 | 37 | script: 38 | - node --version 39 | - yarn lint 40 | - yarn package 41 | - yarn test 42 | - yarn test-e2e 43 | -------------------------------------------------------------------------------- /app/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "flowtype/boolean-style": [2, "boolean"], 4 | "flowtype/define-flow-type": 1, 5 | "flowtype/delimiter-dangle": [2, "never"], 6 | "flowtype/generic-spacing": [2, "never"], 7 | "flowtype/no-primitive-constructor-types": 2, 8 | "flowtype/no-weak-types": 1, 9 | "flowtype/object-type-delimiter": [2, "comma"], 10 | "flowtype/require-parameter-type": 0, 11 | "flowtype/require-return-type": 0, 12 | "flowtype/require-valid-file-annotation": 0, 13 | "flowtype/semi": [2, "always"], 14 | "flowtype/space-after-type-colon": [2, "always"], 15 | "flowtype/space-before-generic-bracket": [2, "never"], 16 | "flowtype/space-before-type-colon": [2, "never"], 17 | "flowtype/union-intersection-spacing": [2, "always"], 18 | "flowtype/use-flow-type": 2, 19 | "flowtype/valid-syntax": 2, 20 | "flowtype-errors/show-errors": 2 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/mirror/core/utils.js: -------------------------------------------------------------------------------- 1 | import config from '../config/config'; 2 | 3 | export function getConfig(moduleName: string) { 4 | const { modules } = config; 5 | for (let i = 0; i < modules.length; i += 1) { 6 | if (moduleName === modules[i].module) { 7 | return modules[i]; 8 | } 9 | } 10 | return {}; 11 | } 12 | 13 | export function getWrapperStyle(position: string) { 14 | const classes = position.replace('_', ' '); 15 | const parentWrapper = document.getElementsByClassName(classes); 16 | if (parentWrapper.length > 0) { 17 | const wrapper = parentWrapper[0].getElementsByClassName('container'); 18 | if (wrapper.length > 0) { 19 | return wrapper[0]; 20 | } 21 | } 22 | } 23 | 24 | export const positions = [ 25 | 'top_ bar', 26 | 'top_left', 27 | 'top_center', 28 | 'top_right', 29 | 'upper_third', 30 | 'middle_center', 31 | 'lower_third', 32 | 'bottom_left', 33 | 'bottom_center', 34 | 'bottom_right', 35 | 'bottom_bar', 36 | 'fullscreen_above', 37 | 'fullscreen_below' 38 | ]; 39 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /node_modules/* 3 | /app/main.prod.js 4 | /app/main.prod.js.map 5 | /app/dist/.* 6 | /resources/.* 7 | /release/.* 8 | /dll/.* 9 | /release/.* 10 | /git/.* 11 | 12 | [include] 13 | 14 | [libs] 15 | 16 | [options] 17 | esproposal.class_static_fields=enable 18 | esproposal.class_instance_fields=enable 19 | esproposal.export_star_as=enable 20 | module.name_mapper.extension='css' -> '/internals/flow/CSSModule.js.flow' 21 | module.name_mapper.extension='styl' -> '/internals/flow/CSSModule.js.flow' 22 | module.name_mapper.extension='scss' -> '/internals/flow/CSSModule.js.flow' 23 | module.name_mapper.extension='png' -> '/internals/flow/WebpackAsset.js.flow' 24 | module.name_mapper.extension='jpg' -> '/internals/flow/WebpackAsset.js.flow' 25 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe 26 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue 27 | -------------------------------------------------------------------------------- /app/mirror/core/components.js: -------------------------------------------------------------------------------- 1 | import config from '../config/config'; 2 | import React from 'react'; 3 | 4 | const { modules } = config; 5 | const loaderHelp = require.context('common', true, /index.js$/); 6 | 7 | function requireAll(requireContext) { 8 | return requireContext.keys().map(requireContext); 9 | } 10 | 11 | export default function getModules() { 12 | const comps = {}; 13 | const allMods = requireAll(loaderHelp); 14 | modules.filter((mod) => { 15 | return !mod.hide; 16 | }).map((item) => { 17 | for (let j = 0; j < allMods.length; j += 1) { 18 | if (item.name === allMods[j].moduleName) { 19 | const MOD = allMods[j]; 20 | if (item.position in comps) { 21 | comps[item.position] = [ 22 | ...comps[item.position], 23 | 24 | ]; 25 | } else { 26 | comps[item.position] = []; 27 | } 28 | break; 29 | } 30 | } 31 | }); 32 | return comps; 33 | } 34 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Snow/Snow.css: -------------------------------------------------------------------------------- 1 | .Snow .snowFlake { 2 | position: absolute; 3 | top: -20px; 4 | animation-name: snowDrop; 5 | animation-iteration-count: infinite; 6 | } 7 | 8 | .Snow .snowFlake .flake { 9 | width: 20px; 10 | height: 20px; 11 | 12 | background-repeat: no-repeat; 13 | background-size: cover; 14 | 15 | animation-name: snowJiggle; 16 | animation-iteration-count: infinite; 17 | } 18 | 19 | .Snow .flake1 {background-image: url('./images/flake1.png');} 20 | .Snow .flake2 {background-image: url('./images/flake2.png');} 21 | .Snow .flake3 {background-image: url('./images/flake3.png');} 22 | 23 | @keyframes snowDrop { 24 | from {transform: translateY(0vh)} 25 | to {transform: translateY(calc(100vh + 20px))} 26 | } 27 | 28 | @keyframes snowJiggle { 29 | 0% {transform: translateX(0vw)} 30 | 20% {transform: translateX(20vw)} 31 | 30% {transform: translateX(5vw)} 32 | 50% {transform: translateX(25vw)} 33 | 70% {transform: translateX(0vw)} 34 | 85% {transform: translateX(15vw)} 35 | 100% {transform: translateX(0vw)} 36 | } 37 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | app/node_modules 30 | 31 | # OSX 32 | .DS_Store 33 | 34 | # flow-typed 35 | flow-typed/npm/* 36 | !flow-typed/npm/module_vx.x.x.js 37 | 38 | # App packaged 39 | release 40 | app/main.prod.js 41 | app/main.prod.js.map 42 | app/renderer.prod.js 43 | app/renderer.prod.js.map 44 | app/style.css 45 | app/style.css.map 46 | dist 47 | dll 48 | main.js 49 | main.js.map 50 | 51 | .idea 52 | npm-debug.log.* 53 | __snapshots__ 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | app/node_modules 30 | 31 | # OSX 32 | .DS_Store 33 | 34 | # flow-typed 35 | flow-typed/npm/* 36 | !flow-typed/npm/module_vx.x.x.js 37 | 38 | # App packaged 39 | release 40 | app/main.prod.js 41 | app/main.prod.js.map 42 | app/renderer.prod.js 43 | app/renderer.prod.js.map 44 | app/style.css 45 | app/style.css.map 46 | dist 47 | dll 48 | main.js 49 | main.js.map 50 | 51 | .idea 52 | npm-debug.log.* 53 | 54 | # Mirror 55 | app/mirror/config/config.js 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present C. T. Lin 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 | 23 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Snow/Flake.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './Snow.css'; 4 | 5 | export default class Flake extends Component { 6 | render() { 7 | return ( 8 |
16 |
30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/mirror/modules/default/CurrentWeather/WeatherDetail.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class WeatherDetail extends Component { 4 | render() { 5 | let windDir = ''; 6 | if (this.props.showWindDirection) { 7 | if (this.props.showWindDirectionAsArrow && this.props.windDegrees) { 8 | console.log("degrees"); 9 | windDir = (); 10 | } else if (this.props.windDegrees) { 11 | windDir = `${this.props.windDegrees}`; 12 | } else { 13 | windDir = this.props.units === 'imperial' ? 'MPH' : 'KPH'; 14 | } 15 | } 16 | 17 | return ( 18 |
19 | 20 | {this.props.windSpeed} 21 | { 22 | (this.props.showWindDirection && windDir) && 23 |  {windDir}  24 | } 25 |   26 | { 27 | this.props.showHumidity && 28 | {this.props.humidity}   29 | } 30 | 31 | {this.props.sunriseSunsetTime} 32 |
33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Snow/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import styles from './Snow.css'; 5 | import Flake from './Flake'; 6 | 7 | class Snow extends Component { 8 | 9 | constructor(props: any) { 10 | super(props); 11 | this.generateDom = this.generateDom.bind(this); 12 | } 13 | 14 | generateDom() { 15 | return Array.from(new Array(this.props.flakeCount), (item, index) => { 16 | return ( 17 | 26 | ); 27 | }); 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 | {this.generateDom()} 34 |
35 | ); 36 | } 37 | } 38 | 39 | Snow.moduleName = 'Snow'; 40 | 41 | Snow.defaultProps = { 42 | position: 'full_screen_above', 43 | flakeCount: 100, 44 | }; 45 | 46 | Snow.propTypes = { 47 | position: PropTypes.string, 48 | flakeCount: PropTypes.number 49 | }; 50 | 51 | export default Snow; 52 | -------------------------------------------------------------------------------- /app/mirror/config/defaults.js: -------------------------------------------------------------------------------- 1 | export default { 2 | kioskmode: false, 3 | electronOptions: {}, 4 | language: 'en', 5 | timeFormat: 24, 6 | units: 'metric', 7 | zoom: 1, 8 | modules: [ 9 | { 10 | module: 'updatenotification', 11 | position: 'top_center' 12 | }, 13 | { 14 | module: 'helloworld', 15 | position: 'upper_third', 16 | classes: 'large thin', 17 | config: { 18 | text: 'Reactive Mirror' 19 | } 20 | }, 21 | { 22 | module: 'helloworld', 23 | position: 'middle_center', 24 | config: { 25 | text: 'Please create a config file.' 26 | } 27 | }, 28 | { 29 | module: 'helloworld', 30 | position: 'middle_center', 31 | classes: 'small dimmed', 32 | config: { 33 | text: 'See README for more information.' 34 | } 35 | }, 36 | { 37 | module: 'helloworld', 38 | position: 'middle_center', 39 | classes: 'xsmall', 40 | config: { 41 | text: 'If you get this message while your config file is already
created, your config file probably contains an error.
Use a JavaScript linter to validate your file.' 42 | } 43 | }, 44 | { 45 | module: 'helloworld', 46 | position: 'bottom_bar', 47 | classes: 'xsmall dimmed', 48 | config: { 49 | text: 'www.michaelteeuw.nl' 50 | } 51 | }, 52 | ] 53 | }; 54 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Reactive Mirror 6 | 17 | 18 | 19 |
20 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "allowImportExportEverywhere": true 6 | }, 7 | "extends": "airbnb", 8 | "env": { 9 | "browser": true, 10 | "node": true 11 | }, 12 | "rules": { 13 | "arrow-parens": ["off"], 14 | "compat/compat": "error", 15 | "consistent-return": "off", 16 | "comma-dangle": "off", 17 | "flowtype-errors/show-errors": "error", 18 | "generator-star-spacing": "off", 19 | "import/no-unresolved": "error", 20 | "import/no-extraneous-dependencies": "off", 21 | "no-console": "off", 22 | "no-use-before-define": "off", 23 | "no-multi-assign": "off", 24 | "promise/param-names": "error", 25 | "promise/always-return": "error", 26 | "promise/catch-or-return": "error", 27 | "promise/no-native": "off", 28 | "react/sort-comp": ["error", { 29 | "order": ["type-annotations", "static-methods", "lifecycle", "everything-else", "render"] 30 | }], 31 | "react/jsx-no-bind": "off", 32 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }], 33 | "react/prefer-stateless-function": "off" 34 | }, 35 | "plugins": [ 36 | "flowtype", 37 | "flowtype-errors", 38 | "import", 39 | "promise", 40 | "compat", 41 | "react" 42 | ], 43 | "settings": { 44 | "import/resolver": { 45 | "webpack": { 46 | "config": "webpack.config.eslint.js" 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /webpack.config.base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { dependencies as externals } from './app/package.json'; 8 | import defaults from './app/mirror/config/defaults'; 9 | import config from './app/mirror/config/config'; 10 | 11 | export default { 12 | externals: Object.keys(externals || {}), 13 | 14 | module: { 15 | rules: [{ 16 | test: /\.jsx?$/, 17 | exclude: /node_modules/, 18 | use: { 19 | loader: 'babel-loader', 20 | options: { 21 | cacheDirectory: true 22 | } 23 | } 24 | }] 25 | }, 26 | 27 | output: { 28 | path: path.join(__dirname, 'app'), 29 | filename: 'renderer.dev.js', 30 | // https://github.com/webpack/webpack/issues/1114 31 | libraryTarget: 'commonjs2' 32 | }, 33 | 34 | /** 35 | * Determine the array of extensions that should be used to resolve modules. 36 | */ 37 | resolve: { 38 | extensions: ['.js', '.jsx', '.json'], 39 | modules: [ 40 | path.join(__dirname, 'app'), 41 | 'node_modules', 42 | ], 43 | alias: { 44 | common: path.resolve(__dirname, './app/mirror/modules') 45 | } 46 | }, 47 | 48 | plugins: [ 49 | new webpack.DefinePlugin({ 50 | 'process.env.MIRROR_CONFIG': JSON.stringify(process.env.MIRROR_CONFIG || { ...defaults, ...config }), 51 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') 52 | }), 53 | new webpack.NamedModulesPlugin(), 54 | ], 55 | }; 56 | -------------------------------------------------------------------------------- /app/mirror/splashscreen/ReactiveMirror.script: -------------------------------------------------------------------------------- 1 | screen_width = Window.GetWidth(); 2 | screen_height = Window.GetHeight(); 3 | 4 | if (Plymouth.GetMode() != "shutdown") 5 | { 6 | theme_image = Image("splash.png"); 7 | } 8 | else 9 | { 10 | theme_image = Image("splash_halt.png"); 11 | } 12 | 13 | image_width = theme_image.GetWidth(); 14 | image_height = theme_image.GetHeight(); 15 | 16 | scale_x = image_width / screen_width; 17 | scale_y = image_height / screen_height; 18 | 19 | if (scale_x > 1 || scale_y > 1) 20 | { 21 | if (scale_x > scale_y) 22 | { 23 | resized_image = theme_image.Scale (screen_width, image_height / scale_x); 24 | image_x = 0; 25 | image_y = (screen_height - ((image_height * screen_width) / image_width)) / 2; 26 | } 27 | else 28 | { 29 | resized_image = theme_image.Scale (image_width / scale_y, screen_height); 30 | image_x = (screen_width - ((image_width * screen_height) / image_height)) / 2; 31 | image_y = 0; 32 | } 33 | } 34 | else 35 | { 36 | resized_image = theme_image.Scale (image_width, image_height); 37 | image_x = (screen_width - image_width) / 2; 38 | image_y = (screen_height - image_height) / 2; 39 | } 40 | 41 | sprite = Sprite (resized_image); 42 | sprite.SetPosition (image_x, image_y, -100); 43 | 44 | message_sprite = Sprite(); 45 | message_sprite.SetPosition(screen_width * 0.1, screen_height * 0.9, 10000); 46 | 47 | fun message_callback (text) { 48 | my_image = Image.Text(text, 1, 1, 1); 49 | message_sprite.SetImage(my_image); 50 | sprite.SetImage (resized_image); 51 | } 52 | 53 | Plymouth.SetUpdateStatusFunction(message_callback); 54 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Compliments/core/utils.js: -------------------------------------------------------------------------------- 1 | export const defaultCompliments = { 2 | anytime: [ 3 | 'Hey there sexy!' 4 | ], 5 | morning: [ 6 | 'Good morning, beautiful!', 7 | 'Enjoy your day!', 8 | 'How was your sleep?' 9 | ], 10 | afternoon: [ 11 | 'Hello, beauty!', 12 | 'You look gorgeous!', 13 | 'You look sexy!', 14 | 'Looking good today!' 15 | ], 16 | evening: [ 17 | 'Wow, you look great!', 18 | 'Wow, you look hot!', 19 | 'You look nice!', 20 | 'Hi, hot stuff!', 21 | 'Hi, sexy!' 22 | ] 23 | }; 24 | 25 | export function getRandom(length: number) { 26 | return Math.floor(Math.random() * length); 27 | } 28 | 29 | export function complimentFile(remoteFile, callback) { 30 | const xobj = new XMLHttpRequest(); 31 | xobj.overrideMimeType('application/json'); 32 | xobj.open('GET', remoteFile, true); 33 | xobj.onreadystatechange = () => { 34 | if (xobj.readyState === 4 && xobj.status === '200') { 35 | callback(xobj.responseText); 36 | } 37 | }; 38 | xobj.send(null); 39 | } 40 | 41 | export const weatherIconTable = { 42 | '01d': 'day_sunny', 43 | '02d': 'day_cloudy', 44 | '03d': 'cloudy', 45 | '04d': 'cloudy_windy', 46 | '09d': 'showers', 47 | '10d': 'rain', 48 | '11d': 'thunderstorm', 49 | '13d': 'snow', 50 | '50d': 'fog', 51 | '01n': 'night_clear', 52 | '02n': 'night_cloudy', 53 | '03n': 'night_cloudy', 54 | '04n': 'night_cloudy', 55 | '09n': 'night_showers', 56 | '10n': 'night_rain', 57 | '11n': 'night_thunderstorm', 58 | '13n': 'night_snow', 59 | '50n': 'night_alt_cloudy_windy' 60 | }; 61 | -------------------------------------------------------------------------------- /app/mirror/config/config.js.sample: -------------------------------------------------------------------------------- 1 | /* 2 | * Reactive Mirror Config Sample 3 | */ 4 | 5 | export default { 6 | language: 'en', 7 | timeFormat: 24, 8 | units: 'metric', 9 | modules: [ 10 | { 11 | name: 'Clock', 12 | position: 'top_left', 13 | }, 14 | { 15 | name: 'Calendar', 16 | position: 'top_left', 17 | config: { 18 | calendars: [ 19 | { 20 | symbol: 'calendar-check-o ', 21 | url: 'webcal://www.calendarlabs.com/templates/ical/US-Holidays.ics' 22 | } 23 | ] 24 | } 25 | }, 26 | { 27 | name: 'CurrentWeather', 28 | position: 'top_right', 29 | config: { 30 | location: 'New York', 31 | locationID: '5128581', // ID from http://www.openweathermap.org/help/city_list.txt 32 | appid: '' 33 | } 34 | }, 35 | { 36 | name: 'WeatherForecast', 37 | position: 'top_right', 38 | config: { 39 | location: 'New York', 40 | locationID: '5128581', // ID from http://www.openweathermap.org/help/city_list.txt 41 | appid: '' 42 | } 43 | }, 44 | { 45 | name: 'Compliments', 46 | position: 'lower_third', 47 | }, 48 | { 49 | name: 'Snow', 50 | position: 'fullscreen_above', 51 | hide: true, 52 | config: { 53 | flakCount: 100, 54 | } 55 | }, 56 | { 57 | name: 'NewsFeed', 58 | position: 'bottom_bar', 59 | config: { 60 | apiKey: '', 61 | showSourceTitle: true, 62 | showPublishDate: true 63 | } 64 | }, 65 | { 66 | name: 'Jokes', 67 | hide: true, 68 | position: 'bottom_bar', 69 | }, 70 | { 71 | name: 'alert', 72 | hide: true, 73 | } 74 | ] 75 | }; 76 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Clock/Clock.css: -------------------------------------------------------------------------------- 1 | .clockCircle { 2 | margin: 0 auto; 3 | position: relative; 4 | border-radius: 50%; 5 | background-size: 100%; 6 | } 7 | 8 | .clockFace { 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | .clockFace::after { 14 | position: absolute; 15 | top: 50%; 16 | left: 50%; 17 | width: 6px; 18 | height: 6px; 19 | margin: -3px 0 0 -3px; 20 | background: white; 21 | border-radius: 3px; 22 | content: ""; 23 | display: block; 24 | } 25 | 26 | .clockHour { 27 | width: 0; 28 | height: 0; 29 | position: absolute; 30 | top: 50%; 31 | left: 50%; 32 | margin: -2px 0 -2px -25%; /* numbers much match negative length & thickness */ 33 | padding: 2px 0 2px 25%; /* indicator length & thickness */ 34 | background: white; 35 | -webkit-transform-origin: 100% 50%; 36 | -ms-transform-origin: 100% 50%; 37 | transform-origin: 100% 50%; 38 | border-radius: 3px 0 0 3px; 39 | } 40 | 41 | .clockMinute { 42 | width: 0; 43 | height: 0; 44 | position: absolute; 45 | top: 50%; 46 | left: 50%; 47 | margin: -35% -2px 0; /* numbers must match negative length & thickness */ 48 | padding: 35% 2px 0; /* indicator length & thickness */ 49 | background: white; 50 | -webkit-transform-origin: 50% 100%; 51 | -ms-transform-origin: 50% 100%; 52 | transform-origin: 50% 100%; 53 | border-radius: 3px 0 0 3px; 54 | } 55 | 56 | .clockSecond { 57 | width: 0; 58 | height: 0; 59 | position: absolute; 60 | top: 50%; 61 | left: 50%; 62 | margin: -38% -1px 0 0; /* numbers must match negative length & thickness */ 63 | padding: 38% 1px 0 0; /* indicator length & thickness */ 64 | background: #888; 65 | -webkit-transform-origin: 50% 100%; 66 | -ms-transform-origin: 50% 100%; 67 | transform-origin: 50% 100%; 68 | } 69 | -------------------------------------------------------------------------------- /app/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { createHashHistory } from 'history'; 4 | import { routerMiddleware, routerActions } from 'react-router-redux'; 5 | import { createLogger } from 'redux-logger'; 6 | import rootReducer from '../reducers'; 7 | 8 | const history = createHashHistory(); 9 | 10 | const configureStore = (initialState?) => { 11 | // Redux Configuration 12 | const middleware = []; 13 | const enhancers = []; 14 | 15 | // Thunk Middleware 16 | middleware.push(thunk); 17 | 18 | // Logging Middleware 19 | const logger = createLogger({ 20 | level: 'info', 21 | collapsed: true 22 | }); 23 | middleware.push(logger); 24 | 25 | // Router Middleware 26 | const router = routerMiddleware(history); 27 | middleware.push(router); 28 | 29 | // Redux DevTools Configuration 30 | const actionCreators = { 31 | ...routerActions, 32 | }; 33 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose 34 | /* eslint-disable no-underscore-dangle */ 35 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 36 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 37 | // Options: http://zalmoxisus.github.io/redux-devtools-extension/API/Arguments.html 38 | actionCreators, 39 | }) 40 | : compose; 41 | /* eslint-enable no-underscore-dangle */ 42 | 43 | // Apply Middleware & Compose Enhancers 44 | enhancers.push(applyMiddleware(...middleware)); 45 | const enhancer = composeEnhancers(...enhancers); 46 | 47 | // Create Store 48 | const store = createStore(rootReducer, initialState, enhancer); 49 | 50 | if (module.hot) { 51 | module.hot.accept('../reducers', () => 52 | store.replaceReducer(require('../reducers')) // eslint-disable-line global-require 53 | ); 54 | } 55 | 56 | return store; 57 | }; 58 | 59 | export default { configureStore, history }; 60 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Calendar/core/utils.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function shorten(stringToShorten: string, maxLength: number = 25, wrapEvents: boolean) { 4 | if (wrapEvents) { 5 | let temp = ''; 6 | let currentLine = ''; 7 | const words = stringToShorten.split(' '); 8 | 9 | words.forEach((word) => { 10 | if (currentLine.length + word.length < maxLength - 1) { // max - 1 to account for a space 11 | currentLine += `${word} `; 12 | } else { 13 | if (currentLine.length > 0) { 14 | temp += ({currentLine}
{word}
); 15 | } else { 16 | temp += ({word}
); 17 | } 18 | currentLine = ''; 19 | } 20 | }); 21 | return (temp + currentLine).trim(); 22 | } 23 | 24 | if (stringToShorten.length > maxLength) { 25 | return `${stringToShorten.trim().slice(0, maxLength)}${String.fromCharCode(8230)}`; 26 | } 27 | 28 | return stringToShorten.trim(); 29 | } 30 | 31 | export function capFirst(string) { 32 | return string.charAt(0).toUpperCase() + string.slice(1); 33 | } 34 | 35 | export function titleTransform(title, configReplacements, maxTitleLength, wrapEvents) { 36 | for (let needle in configReplacements) { 37 | const replacement = configReplacements[needle]; 38 | 39 | const regParts = needle.match(/^\/(.+)\/([gim]*)$/); 40 | if (regParts) { 41 | // the parsed pattern is a regexp. 42 | needle = new RegExp(regParts[1], regParts[2]); 43 | } 44 | 45 | title = title.replace(needle, replacement); 46 | } 47 | title = shorten(title, maxTitleLength, wrapEvents); 48 | return title; 49 | } 50 | 51 | export function isFullDayEvent(event) { 52 | if (event.start.length === 8) { 53 | return true; 54 | } 55 | 56 | const start = event.start || 0; 57 | const startDate = new Date(start); 58 | const end = event.end || 0; 59 | if (end - start === 24 * 60 * 60 * 1000 && startDate.getHours() === 0 && startDate.getMinutes() === 0) { 60 | // Is 24 hours, and starts on the middle of the night. 61 | return true; 62 | } 63 | 64 | return false; 65 | } 66 | -------------------------------------------------------------------------------- /webpack.config.main.prod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import merge from 'webpack-merge'; 7 | // import BabiliPlugin from 'babili-webpack-plugin'; 8 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 9 | import baseConfig from './webpack.config.base'; 10 | import CheckNodeEnv from './internals/scripts/CheckNodeEnv'; 11 | import defaults from './app/mirror/config/defaults'; 12 | import config from './app/mirror/config/config'; 13 | 14 | CheckNodeEnv('production'); 15 | 16 | export default merge.smart(baseConfig, { 17 | devtool: 'source-map', 18 | 19 | target: 'electron-main', 20 | 21 | entry: './app/main.dev', 22 | 23 | // 'main.js' in root 24 | output: { 25 | path: __dirname, 26 | filename: './app/main.prod.js' 27 | }, 28 | 29 | plugins: [ 30 | /** 31 | * Babli is an ES6+ aware minifier based on the Babel toolchain (beta) 32 | */ 33 | // new BabiliPlugin(), 34 | 35 | new BundleAnalyzerPlugin({ 36 | analyzerMode: process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 37 | openAnalyzer: process.env.OPEN_ANALYZER === 'true' 38 | }), 39 | 40 | /** 41 | * Create global constants which can be configured at compile time. 42 | * 43 | * Useful for allowing different behaviour between development builds and 44 | * release builds 45 | * 46 | * NODE_ENV should be production so that modules do not perform certain 47 | * development checks 48 | */ 49 | new webpack.DefinePlugin({ 50 | 'process.env.MIRROR_CONFIG': JSON.stringify(process.env.MIRROR_CONFIG || { ...defaults, ...config }), 51 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), 52 | 'process.env.DEBUG_PROD': JSON.stringify(process.env.DEBUG_PROD || 'false') 53 | }) 54 | ], 55 | 56 | /** 57 | * Disables webpack processing of __dirname and __filename. 58 | * If you run the bundle in node.js it falls back to these values of node.js. 59 | * https://github.com/webpack/webpack/issues/2010 60 | */ 61 | node: { 62 | __dirname: false, 63 | __filename: false 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /app/containers/App.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import type { Children } from 'react'; 4 | //TODO: REMOVE MOUSE HIDING SINCE IT'S HANDLED BY RASPBERRY PI 5 | export default class App extends Component { 6 | props: { 7 | children: Children 8 | }; 9 | 10 | constructor(props) { 11 | super(props); 12 | this.mouseMove = this.mouseMove.bind(this); 13 | this.state = { 14 | mouseTimer: setTimeout(()=> { 15 | this.setState({ 16 | cursorVisible: false, 17 | forceHide: true, 18 | }); 19 | setTimeout(() => { 20 | this.setState({ 21 | forceHide: false, 22 | }); 23 | }, 200); 24 | }, 4000), 25 | cursorVisible: false, 26 | forceHide: false 27 | }; 28 | } 29 | 30 | mouseMove() { 31 | if (!this.state.forceHide && this.state.mouseTimer){ 32 | this.setState({ 33 | cursorVisible: true, 34 | }); 35 | 36 | clearTimeout(this.state.mouseTimer); 37 | 38 | this.setState({ 39 | mouseTimer: setTimeout(()=> { 40 | this.setState({ 41 | cursorVisible: false, 42 | forceHide: true, 43 | }); 44 | setTimeout(() => { 45 | this.setState({ 46 | forceHide: false, 47 | }); 48 | }, 200); 49 | }, 4000) 50 | }); 51 | } 52 | } 53 | 54 | render() { 55 | const configStyle = { 56 | color: '#fff', 57 | padding: '16px', 58 | borderRadius: '5px', 59 | borderWidth: '2px', 60 | borderColor: '#fff', 61 | backgroundColor: '#eeee', 62 | fontSize: '48px', 63 | width: 'fit-content', 64 | zIndex: '1000', 65 | position: 'fixed', 66 | top: 0, 67 | right: 0, 68 | justifyContent: 'center', 69 | display: this.state.cursorVisible ? 'flex' : 'none' 70 | }; 71 | 72 | return ( 73 |
74 | {this.props.children} 75 |
76 | 77 |
78 |
79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Jokes/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { getJoke } from './core/utils'; 5 | 6 | class Jokes extends Component { 7 | 8 | constructor(props: any) { 9 | super(props); 10 | this.updateModule = this.updateModule.bind(this); 11 | this.hideShowModule = this.hideShowModule.bind(this); 12 | } 13 | 14 | state = { 15 | opacity: 0, 16 | hidden: true, 17 | intervalId: null, 18 | showHideTimer: null, 19 | joke: '' 20 | }; 21 | 22 | componentDidMount() { 23 | this.updateModule(); 24 | this.setState({ 25 | intervalId: setInterval(() => { 26 | this.updateModule(); 27 | }, this.props.updateInterval) 28 | }); 29 | } 30 | 31 | componentWillUnmount() { 32 | clearInterval(this.state.intervalId); 33 | clearTimeout(this.state.showHideTimer); 34 | } 35 | 36 | hideShowModule(hide: boolean, callback: any) { 37 | this.setState({ 38 | opacity: hide ? 0 : 1, 39 | hidden: hide 40 | }); 41 | if (hide && callback) { 42 | this.setState({ 43 | showHideTimer: setTimeout(() => { callback(); }, this.props.fadeSpeed / 2), 44 | }); 45 | } else { 46 | clearTimeout(this.state.showHideTimer); 47 | } 48 | } 49 | 50 | updateModule() { 51 | this.hideShowModule(true, () => { 52 | getJoke((j)=>{ 53 | this.setState({ 54 | joke: j, 55 | }, () => { 56 | this.hideShowModule(false); 57 | }); 58 | }); 59 | }); 60 | } 61 | 62 | render() { 63 | return ( 64 |
71 | {this.state.joke} 72 |
73 | ); 74 | } 75 | } 76 | 77 | Jokes.moduleName = 'Jokes'; 78 | 79 | Jokes.defaultProps = { 80 | fadeSpeed: 4000, 81 | updateInterval: 30000, // update the module to change the current item every 30 seconds 82 | animation: 'ease-in', 83 | wrapJoke: true 84 | }; 85 | 86 | Jokes.propTypes = { 87 | fadeSpeed: PropTypes.number, 88 | updateInterval: PropTypes.number, 89 | animation: PropTypes.string, 90 | wrapJoke: PropTypes.bool 91 | }; 92 | 93 | export default Jokes; 94 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Clock/README.md: -------------------------------------------------------------------------------- 1 | # Clock 2 | The `clock` module is one of the default modules of the ReactiveMirror. 3 | This module displays the current date and time. The information will be updated realtime. 4 | 5 | ## Using the module 6 | 7 | Add it to the modules array in the `config/config.js` file: 8 | 9 | ````javascript 10 | modules: [ 11 | { 12 | name: "Clock", 13 | position: "top_left", // This can be any of the regions. 14 | config: { 15 | // The config property is optional. 16 | // See 'Configuration options' for more information. 17 | } 18 | } 19 | ] 20 | ```` 21 | 22 | ## Configuration options 23 | 24 | The following properties can be configured: 25 | 26 | | Option | Description 27 | | ----------------- | ----------- 28 | | `timeFormat` | Use 12 or 24 hour format.

**Possible values:** `12` or `24`
**Default value:** uses value of _config.timeFormat_ 29 | | `displaySeconds` | Display seconds.

**Possible values:** `true` or `false`
**Default value:** `true` 30 | | `showPeriod` | Show the period (am/pm) with 12 hour format.

**Possible values:** `true` or `false`
**Default value:** `true` 31 | | `showPeriodUpper` | Show the period (AM/PM) with 12 hour format as uppercase.

**Possible values:** `true` or `false`
**Default value:** `false` 32 | | `clockBold` | Remove the colon and bold the minutes to make a more modern look.

**Possible values:** `true` or `false`
**Default value:** `false` 33 | | `showDate` | Turn off or on the Date section.

**Possible values:** `true` or `false`
**Default value:** `true` 34 | | `showWeek` | Turn off or on the Week section.

**Possible values:** `true` or `false`
**Default value:** `false` 35 | | `dateFormat` | Configure the date format as you like.

**Possible values:** [Docs](http://momentjs.com/docs/#/displaying/format/)
**Default value:** `"dddd, LL"` 36 | | `secondsColor` | **Specific to the analog clock.** Specifies what color to make the 'seconds' hand.

**Possible values:** `any HTML RGB Color`
**Default value:** `#888888` 37 | | `timezone` | Specific a timezone to show clock.

**Possible examples values:** `America/New_York`, `America/Santiago`, `Etc/GMT+10`
**Default value:** `none`. See more informations about configuration value [here](https://momentjs.com/timezone/docs/#/data-formats/packed-format/) 38 | -------------------------------------------------------------------------------- /app/components/Mirror.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import getModules from '../mirror/core/components'; 4 | import styles from './Mirror.css'; 5 | 6 | export default class Mirror extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | components: getModules(), 12 | }; 13 | } 14 | 15 | render() { 16 | if (this.state.components) { 17 | return ( 18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {this.state.components.top_bar} 26 |
27 |
28 |
29 | {this.state.components.top_left} 30 |
31 |
32 |
33 |
34 | {this.state.components.top_center} 35 |
36 |
37 |
38 |
39 | {this.state.components.top_right} 40 |
41 |
42 |
43 |
{this.state.components.upper_third}
44 |
{this.state.components.middle_center}
45 |

{this.state.components.lower_third}
46 |
47 |
{this.state.components.bottom_bar}
48 |
{this.state.components.bottom_left}
49 |
{this.state.components.bottom_center}
50 |
{this.state.components.bottom_right}
51 |
52 |
{this.state.components.fullscreen_above}
53 |
54 | ); 55 | } 56 | return ( 57 |
Reactive Mirror
58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/mirror/modules/default/CurrentWeather/core/utils.js: -------------------------------------------------------------------------------- 1 | export const iconTable = { 2 | '01d': 'wi-day-sunny', 3 | '02d': 'wi-day-cloudy', 4 | '03d': 'wi-cloudy', 5 | '04d': 'wi-cloudy-windy', 6 | '09d': 'wi-showers', 7 | '10d': 'wi-rain', 8 | '11d': 'wi-thunderstorm', 9 | '13d': 'wi-snow', 10 | '50d': 'wi-fog', 11 | '01n': 'wi-night-clear', 12 | '02n': 'wi-night-cloudy', 13 | '03n': 'wi-night-cloudy', 14 | '04n': 'wi-night-cloudy', 15 | '09n': 'wi-night-showers', 16 | '10n': 'wi-night-rain', 17 | '11n': 'wi-night-thunderstorm', 18 | '13n': 'wi-night-snow', 19 | '50n': 'wi-night-alt-cloudy-windy' 20 | }; 21 | 22 | /* function(temperature) 23 | * Rounds a temperature to 1 decimal or integer (depending on config.roundTemp). 24 | * 25 | * argument temperature number - Temperature. 26 | * 27 | * return string - Rounded Temperature. 28 | */ 29 | export function roundValue(temperature, roundTemp) { 30 | const decimals = roundTemp ? 0 : 1; 31 | return parseFloat(temperature).toFixed(decimals); 32 | } 33 | 34 | /* ms2Beaufort(ms) 35 | * Converts m2 to beaufort (windspeed). 36 | * 37 | * see: 38 | * http://www.spc.noaa.gov/faq/tornado/beaufort.html 39 | * https://en.wikipedia.org/wiki/Beaufort_scale#Modern_scale 40 | * 41 | * argument ms number - Windspeed in m/s. 42 | * 43 | * return number - Windspeed in beaufort. 44 | */ 45 | export function ms2Beaufort(ms) { 46 | const kmh = ((ms * 60 * 60) / 1000); 47 | const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000]; 48 | 49 | for (let i = 0; i < speeds.length; i += 1) { 50 | const speed = speeds[speeds[i]]; 51 | if (speed > kmh) { 52 | return speeds[i]; 53 | } 54 | } 55 | return 12; 56 | } 57 | 58 | export function deg2Cardinal(deg) { 59 | if (deg > 11.25 && deg <= 33.75) { 60 | return 'NNE'; 61 | } else if (deg > 33.75 && deg <= 56.25) { 62 | return 'NE'; 63 | } else if (deg > 56.25 && deg <= 78.75) { 64 | return 'ENE'; 65 | } else if (deg > 78.75 && deg <= 101.25) { 66 | return 'E'; 67 | } else if (deg > 101.25 && deg <= 123.75) { 68 | return 'ESE'; 69 | } else if (deg > 123.75 && deg <= 146.25) { 70 | return 'SE'; 71 | } else if (deg > 146.25 && deg <= 168.75) { 72 | return 'SSE'; 73 | } else if (deg > 168.75 && deg <= 191.25) { 74 | return 'S'; 75 | } else if (deg > 191.25 && deg <= 213.75) { 76 | return 'SSW'; 77 | } else if (deg > 213.75 && deg <= 236.25) { 78 | return 'SW'; 79 | } else if (deg > 236.25 && deg <= 258.75) { 80 | return 'WSW'; 81 | } else if (deg > 258.75 && deg <= 281.25) { 82 | return 'W'; 83 | } else if (deg > 281.25 && deg <= 303.75) { 84 | return 'WNW'; 85 | } else if (deg > 303.75 && deg <= 326.25) { 86 | return 'NW'; 87 | } else if (deg > 326.25 && deg <= 348.75) { 88 | return 'NNW'; 89 | } else { 90 | return 'N'; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Clock/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import moment from 'moment'; 5 | import classNames from 'classnames'; 6 | import styles from './Clock.css'; 7 | 8 | class Clock extends Component { 9 | 10 | constructor(props: any) { 11 | super(props); 12 | this.updateClock = this.updateClock.bind(this); 13 | moment.locale('en'); 14 | } 15 | 16 | state = { 17 | date: '', 18 | week: '', 19 | time: '', 20 | period: '', 21 | seconds: '', 22 | } 23 | 24 | componentDidMount() { 25 | this.updateClock(); 26 | this.setState({ 27 | timeIntervalID: setInterval(() => { 28 | this.updateClock(); 29 | }, 1000) 30 | }); 31 | } 32 | 33 | componentWillUnmount() { 34 | clearInterval(this.state.timeIntervalID); 35 | } 36 | 37 | updateClock() { 38 | let now = moment(); 39 | if (this.props.timezone) { 40 | now.tz(this.props.timezone); 41 | } 42 | 43 | let hourSymbol = this.props.timeFormat === 24 ? 'HH' : 'h'; 44 | this.setState({ 45 | hour: now.format(hourSymbol), 46 | minute: now.format('mm'), 47 | date: now.format(this.props.dateFormat), 48 | week: `${'WEEK'} ${now.week()}`, 49 | seconds: now.format('ss'), 50 | period: this.props.showPeriodUpper ? now.format('A') : now.format('a'), 51 | }); 52 | } 53 | 54 | render() { 55 | return ( 56 |
57 |
{this.state.date}
61 |
65 | {this.state.hour} 66 | {this.props.clockBold ? '' : ':'}{this.state.minute} 67 | {this.state.seconds} 68 | { 69 | (this.props.showPeriod && this.props.timeFormat !== 24) && 70 | {this.state.period} 71 | } 72 |
73 | { 74 | this.props.showWeek && 75 |
{this.state.week}
79 | } 80 |
81 | ); 82 | } 83 | } 84 | 85 | Clock.moduleName = 'Clock'; 86 | 87 | Clock.defaultProps = { 88 | position: 'top_right', 89 | displayType: 'digital', 90 | timeFormat: 12, 91 | timezone: null, 92 | displaySeconds: true, 93 | showPeriod: true, 94 | showPeriodUpper: true, 95 | clockBold: false, 96 | showDate: true, 97 | showWeek: false, 98 | dateFormat: "dddd, LL", 99 | }; 100 | 101 | Clock.propTypes = { 102 | position: PropTypes.string, 103 | displayType: PropTypes.string, 104 | dateFormat: PropTypes.string, 105 | timeFormat: PropTypes.number, 106 | displaySeconds: PropTypes.bool, 107 | showPeriod: PropTypes.bool, 108 | showPeriodUpper: PropTypes.bool, 109 | clockBold: PropTypes.bool, 110 | showDate: PropTypes.bool, 111 | showWeek: PropTypes.bool, 112 | timezone: PropTypes.string, 113 | }; 114 | 115 | export default Clock; 116 | -------------------------------------------------------------------------------- /app/main.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: 1, flowtype-errors/show-errors: 0 */ 2 | 3 | /** 4 | * This module executes inside of electron's main process. You can start 5 | * electron renderer process from here and communicate with the other processes 6 | * through IPC. 7 | * 8 | * When running `npm run build` or `npm run build-main`, this file is compiled to 9 | * `./app/main.prod.js` using webpack. This gives us some performance wins. 10 | * 11 | * @flow 12 | */ 13 | import { app, BrowserWindow } from 'electron'; 14 | import MenuBuilder from './menu'; 15 | 16 | const config = process.env.MIRROR_CONFIG ? { ...process.env.MIRROR_CONFIG } : {}; 17 | let mainWindow = null; 18 | 19 | if (process.env.NODE_ENV === 'production') { 20 | const sourceMapSupport = require('source-map-support'); 21 | sourceMapSupport.install(); 22 | } 23 | 24 | if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { 25 | require('electron-debug')(); 26 | const path = require('path'); 27 | const p = path.join(__dirname, '..', 'app', 'node_modules'); 28 | require('module').globalPaths.push(p); 29 | } 30 | 31 | const installExtensions = async () => { 32 | const installer = require('electron-devtools-installer'); 33 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS; 34 | const extensions = [ 35 | 'REACT_DEVELOPER_TOOLS', 36 | 'REDUX_DEVTOOLS' 37 | ]; 38 | 39 | return Promise 40 | .all(extensions.map(name => installer.default(installer[name], forceDownload))) 41 | .catch(console.log); 42 | }; 43 | 44 | 45 | /** 46 | * Add event listeners... 47 | */ 48 | 49 | app.on('window-all-closed', () => { 50 | // Respect the OSX convention of having the application in memory even 51 | // after all windows have been closed 52 | if (process.platform !== 'darwin') { 53 | app.quit(); 54 | } 55 | }); 56 | 57 | 58 | app.on('ready', async () => { 59 | if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { 60 | await installExtensions(); 61 | } 62 | 63 | const productionWindow = { 64 | frameless: true, 65 | fullscreen: true, 66 | autoHideMenuBar: true 67 | }; 68 | 69 | mainWindow = new BrowserWindow({ 70 | x: 0, 71 | y: 0, 72 | show: false, 73 | width: 800, 74 | height: 600, 75 | darkTheme: true, 76 | backgroundColor: '#000000', 77 | webPreferences: { 78 | zoomFactor: config.zoom 79 | }, 80 | ...(process.env.NODE_ENV === 'production' ? productionWindow : {}), 81 | ...config.electronOptions 82 | }); 83 | 84 | mainWindow.loadURL(`file://${__dirname}/app.html`); 85 | 86 | // @TODO: Use 'ready-to-show' event 87 | // https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event 88 | mainWindow.webContents.on('did-finish-load', () => { 89 | if (!mainWindow) { 90 | throw new Error('\'mainWindow\' is not defined'); 91 | } 92 | mainWindow.show(); 93 | mainWindow.focus(); 94 | }); 95 | 96 | mainWindow.on('closed', () => { 97 | mainWindow = null; 98 | }); 99 | 100 | if (config.kiosk) { 101 | mainWindow.on('blur', () => { 102 | mainWindow.focus(); 103 | }); 104 | 105 | mainWindow.on('leave-full-screen', () => { 106 | mainWindow.setFullScreen(true); 107 | }); 108 | 109 | mainWindow.on('resize', () => { 110 | setTimeout(() => { 111 | mainWindow.reload(); 112 | }, 1000); 113 | }); 114 | } 115 | 116 | const menuBuilder = new MenuBuilder(mainWindow); 117 | menuBuilder.buildMenu(); 118 | }); 119 | -------------------------------------------------------------------------------- /test/e2e/e2e.spec.js: -------------------------------------------------------------------------------- 1 | import { Application } from 'spectron'; 2 | import electronPath from 'electron'; 3 | import path from 'path'; 4 | 5 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; 6 | 7 | const delay = time => new Promise(resolve => setTimeout(resolve, time)); 8 | 9 | describe('main window', function spec() { 10 | beforeAll(async () => { 11 | this.app = new Application({ 12 | path: electronPath, 13 | args: [path.join(__dirname, '..', '..', 'app')], 14 | }); 15 | 16 | return this.app.start(); 17 | }); 18 | 19 | afterAll(() => { 20 | if (this.app && this.app.isRunning()) { 21 | return this.app.stop(); 22 | } 23 | }); 24 | 25 | const findCounter = () => this.app.client.element('[data-tid="counter"]'); 26 | 27 | const findButtons = async () => { 28 | const { value } = await this.app.client.elements('[data-tclass="btn"]'); 29 | return value.map(btn => btn.ELEMENT); 30 | }; 31 | 32 | it('should open window', async () => { 33 | const { client, browserWindow } = this.app; 34 | 35 | await client.waitUntilWindowLoaded(); 36 | await delay(500); 37 | const title = await browserWindow.getTitle(); 38 | expect(title).toBe('Hello Electron React!'); 39 | }); 40 | 41 | it('should haven\'t any logs in console of main window', async () => { 42 | const { client } = this.app; 43 | const logs = await client.getRenderProcessLogs(); 44 | // Print renderer process logs 45 | logs.forEach(log => { 46 | console.log(log.message); 47 | console.log(log.source); 48 | console.log(log.level); 49 | }); 50 | expect(logs).toHaveLength(0); 51 | }); 52 | 53 | it('should to Counter with click "to Counter" link', async () => { 54 | const { client } = this.app; 55 | 56 | await client.click('[data-tid=container] > a'); 57 | expect(await findCounter().getText()).toBe('0'); 58 | }); 59 | 60 | it('should display updated count after increment button click', async () => { 61 | const { client } = this.app; 62 | 63 | const buttons = await findButtons(); 64 | await client.elementIdClick(buttons[0]); // + 65 | expect(await findCounter().getText()).toBe('1'); 66 | }); 67 | 68 | it('should display updated count after descrement button click', async () => { 69 | const { client } = this.app; 70 | 71 | const buttons = await findButtons(); 72 | await client.elementIdClick(buttons[1]); // - 73 | expect(await findCounter().getText()).toBe('0'); 74 | }); 75 | 76 | it('shouldnt change if even and if odd button clicked', async () => { 77 | const { client } = this.app; 78 | 79 | const buttons = await findButtons(); 80 | await client.elementIdClick(buttons[2]); // odd 81 | expect(await findCounter().getText()).toBe('0'); 82 | }); 83 | 84 | it('should change if odd and if odd button clicked', async () => { 85 | const { client } = this.app; 86 | 87 | const buttons = await findButtons(); 88 | await client.elementIdClick(buttons[0]); // + 89 | await client.elementIdClick(buttons[2]); // odd 90 | expect(await findCounter().getText()).toBe('2'); 91 | }); 92 | 93 | it('should change if async button clicked and a second later', async () => { 94 | const { client } = this.app; 95 | 96 | const buttons = await findButtons(); 97 | await client.elementIdClick(buttons[3]); // async 98 | expect(await findCounter().getText()).toBe('2'); 99 | await delay(1500); 100 | expect(await findCounter().getText()).toBe('3'); 101 | }); 102 | 103 | it('should back to home if back button clicked', async () => { 104 | const { client } = this.app; 105 | await client.element( 106 | '[data-tid="backButton"] > a' 107 | ).click(); 108 | 109 | expect( 110 | await client.isExisting('[data-tid="container"]') 111 | ).toBe(true); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /app/mirror/styles/roboto.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Roboto; 3 | font-style: normal; 4 | font-weight: 100; 5 | src: 6 | local("Roboto Thin"), 7 | local("Roboto-Thin"), 8 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff2") format("woff2"), 9 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff") format("woff"), 10 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.ttf") format("truetype"); 11 | } 12 | 13 | @font-face { 14 | font-family: "Roboto Condensed"; 15 | font-style: normal; 16 | font-weight: 300; 17 | src: 18 | local("Roboto Condensed Light"), 19 | local("RobotoCondensed-Light"), 20 | url("../../node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff2") format("woff2"), 21 | url("../../node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff") format("woff"), 22 | url("../../node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.ttf") format("truetype"); 23 | } 24 | 25 | @font-face { 26 | font-family: "Roboto Condensed"; 27 | font-style: normal; 28 | font-weight: 400; 29 | src: 30 | local("Roboto Condensed"), 31 | local("RobotoCondensed-Regular"), 32 | url("../../node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff2") format("woff2"), 33 | url("../../node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff") format("woff"), 34 | url("../../node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.ttf") format("truetype"); 35 | } 36 | 37 | @font-face { 38 | font-family: "Roboto Condensed"; 39 | font-style: normal; 40 | font-weight: 700; 41 | src: 42 | local("Roboto Condensed Bold"), 43 | local("RobotoCondensed-Bold"), 44 | url("../../node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff2") format("woff2"), 45 | url("../../node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff") format("woff"), 46 | url("../../node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.ttf") format("truetype"); 47 | } 48 | 49 | @font-face { 50 | font-family: Roboto; 51 | font-style: normal; 52 | font-weight: 400; 53 | src: 54 | local("Roboto"), 55 | local("Roboto-Regular"), 56 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2") format("woff2"), 57 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff") format("woff"), 58 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.ttf") format("truetype"); 59 | } 60 | 61 | @font-face { 62 | font-family: Roboto; 63 | font-style: normal; 64 | font-weight: 500; 65 | src: 66 | local("Roboto Medium"), 67 | local("Roboto-Medium"), 68 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2") format("woff2"), 69 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff") format("woff"), 70 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.ttf") format("truetype"); 71 | } 72 | 73 | @font-face { 74 | font-family: Roboto; 75 | font-style: normal; 76 | font-weight: 700; 77 | src: 78 | local("Roboto Bold"), 79 | local("Roboto-Bold"), 80 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2") format("woff2"), 81 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff") format("woff"), 82 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.ttf") format("truetype"); 83 | } 84 | 85 | @font-face { 86 | font-family: Roboto; 87 | font-style: normal; 88 | font-weight: 300; 89 | src: 90 | local("Roboto Light"), 91 | local("Roboto-Light"), 92 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2") format("woff2"), 93 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff") format("woff"), 94 | url("../../node_modules/roboto-fontface/fonts/roboto/Roboto-Light.ttf") format("truetype"); 95 | } 96 | -------------------------------------------------------------------------------- /app/mirror/styles/main.css: -------------------------------------------------------------------------------- 1 | @import "~weathericons/css/weather-icons.css"; 2 | html { 3 | overflow: hidden; 4 | background: #000; 5 | } 6 | 7 | body{ 8 | margin: 0; 9 | width: 100vw; 10 | height: 100vh; 11 | } 12 | 13 | :not(input):not(textarea) { 14 | -webkit-user-select: none; 15 | user-select: none; 16 | } 17 | 18 | ::-webkit-scrollbar { 19 | display: none; 20 | } 21 | 22 | /** 23 | * Default styles. 24 | */ 25 | 26 | .dimmed { 27 | color: #666; 28 | } 29 | 30 | .normal { 31 | color: #999; 32 | } 33 | 34 | .bright { 35 | color: #fff; 36 | } 37 | 38 | .xsmall { 39 | font-size: 15px; 40 | line-height: 20px; 41 | } 42 | 43 | .small { 44 | font-size: 20px; 45 | line-height: 25px; 46 | } 47 | 48 | .medium { 49 | font-size: 30px; 50 | line-height: 35px; 51 | } 52 | 53 | .large { 54 | font-size: 65px; 55 | line-height: 65px; 56 | } 57 | 58 | .xlarge { 59 | font-size: 75px; 60 | line-height: 75px; 61 | letter-spacing: -3px; 62 | } 63 | 64 | .thin { 65 | font-family: Roboto, sans-serif; 66 | font-weight: 100; 67 | } 68 | 69 | .light { 70 | font-family: 'Roboto Condensed', sans-serif; 71 | font-weight: 300; 72 | } 73 | 74 | .regular { 75 | font-family: 'Roboto Condensed', sans-serif; 76 | font-weight: 400; 77 | } 78 | 79 | .bold { 80 | font-family: 'Roboto Condensed', sans-serif; 81 | font-weight: 700; 82 | } 83 | 84 | .align-right { 85 | text-align: right; 86 | } 87 | 88 | .align-left { 89 | text-align: left; 90 | } 91 | 92 | header { 93 | text-transform: uppercase; 94 | font-size: 15px; 95 | font-family: 'Roboto Condensed'; 96 | font-weight: 400; 97 | border-bottom: 1px solid #666; 98 | line-height: 15px; 99 | padding-bottom: 5px; 100 | margin-bottom: 10px; 101 | color: #999; 102 | } 103 | 104 | sup { 105 | font-size: 50%; 106 | line-height: 50%; 107 | } 108 | 109 | /** 110 | * Module styles. 111 | */ 112 | 113 | .module { 114 | margin-bottom: 30px; 115 | } 116 | 117 | .region.bottom .module { 118 | margin-top: 30px; 119 | margin-bottom: 0; 120 | } 121 | 122 | .no-wrap { 123 | white-space: nowrap; 124 | overflow: hidden; 125 | text-overflow: ellipsis; 126 | } 127 | 128 | /** 129 | * Region Definitions. 130 | */ 131 | 132 | .region { 133 | position: absolute; 134 | } 135 | 136 | .region.fullscreen { 137 | position: absolute; 138 | top: -60px; 139 | left: -60px; 140 | right: -60px; 141 | bottom: -60px; 142 | pointer-events: none; 143 | } 144 | 145 | .region.fullscreen * { 146 | pointer-events: auto; 147 | } 148 | 149 | .region.right { 150 | right: 0; 151 | } 152 | 153 | .region.top { 154 | top: 0; 155 | } 156 | 157 | .region.top .container { 158 | margin-bottom: 25px; 159 | } 160 | 161 | .region.top .container:empty { 162 | margin-bottom: 0; 163 | } 164 | 165 | .region.top.center, 166 | .region.bottom.center { 167 | left: 50%; 168 | -moz-transform: translateX(-50%); 169 | -o-transform: translateX(-50%); 170 | -webkit-transform: translateX(-50%); 171 | -ms-transform: translateX(-50%); 172 | transform: translateX(-50%); 173 | } 174 | 175 | .region.top.right, 176 | .region.top.left, 177 | .region.top.center { 178 | top: 100%; 179 | } 180 | 181 | .region.bottom { 182 | bottom: 0; 183 | } 184 | 185 | .region.bottom .container { 186 | margin-top: 25px; 187 | } 188 | 189 | .region.bottom .container:empty { 190 | margin-top: 0; 191 | } 192 | 193 | .region.bottom.right, 194 | .region.bottom.center, 195 | .region.bottom.left { 196 | bottom: 100%; 197 | } 198 | 199 | .region.bar { 200 | width: 100%; 201 | text-align: center; 202 | } 203 | 204 | .region.third, 205 | .region.middle.center { 206 | width: 100%; 207 | text-align: center; 208 | -moz-transform: translateY(-50%); 209 | -o-transform: translateY(-50%); 210 | -webkit-transform: translateY(-50%); 211 | -ms-transform: translateY(-50%); 212 | transform: translateY(-50%); 213 | } 214 | 215 | .region.upper.third { 216 | top: 33%; 217 | } 218 | 219 | .region.middle.center { 220 | top: 50%; 221 | } 222 | 223 | .region.lower.third { 224 | top: 66%; 225 | } 226 | 227 | .region.left { 228 | text-align: left; 229 | } 230 | 231 | .region.right { 232 | text-align: right; 233 | } 234 | 235 | .region table { 236 | width: 100%; 237 | border-spacing: 0; 238 | border-collapse: separate; 239 | } 240 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Compliments/README.md: -------------------------------------------------------------------------------- 1 | # Compliments 2 | The `compliments` module is one of the default modules of the MagicMirror. 3 | This module displays a random compliment. 4 | 5 | ## Using the module 6 | 7 | Add it to the modules array in the `config/config.js` file: 8 | 9 | ````javascript 10 | modules: [ 11 | { 12 | module: "Compliments", 13 | position: "lower_third", // This can be any of the regions. 14 | // Best results in one of the middle regions like: lower_third 15 | config: { 16 | // The config property is optional. 17 | // If no config is set, the default compliments are shown. 18 | // See 'Configuration options' for more information. 19 | } 20 | } 21 | ] 22 | ```` 23 | 24 | ## Configuration options 25 | 26 | The following properties can be configured: 27 | 28 | 29 | | Option | Description 30 | | ---------------- | ----------- 31 | | `updateInterval` | How often does the compliment have to change? (Milliseconds)

**Possible values:** `1000` - `86400000`
**Default value:** `30000` (30 seconds) 32 | | `fadeSpeed` | Speed of the update animation. (Milliseconds)

**Possible values:**`0` - `5000`
**Default value:** `4000` (4 seconds) 33 | | `compliments` | The list of compliments.

**Possible values:** An object with four arrays: `morning`, `afternoon`, `evening` and `anytime`. See _compliment configuration_ below.
**Default value:** See _compliment configuration_ below. 34 | | `remoteFile` | External file from which to load the compliments

**Possible values:** Path to a JSON file containing compliments, configured as per the value of the _compliments configuration_ (see below). An object with four arrays: `morning`, `afternoon`, `evening` and `anytime`. - `compliments.json`
**Default value:** `null` (Do not load from file) 35 | | `classes` | Override the CSS classes of the div showing the compliments

**Default value:** `thin xlarge bright` 36 | 37 | ### Compliment configuration 38 | 39 | The `compliments` property contains an object with four arrays: morning, afternoon, evening and anytime. Based on the time of the day, the compliments will be picked out of one of these arrays. The arrays contain one or multiple compliments. 40 | 41 | 42 | If use the currentweather is possible use a actual weather for set compliments. The availables properties are: 43 | * `day_sunny` 44 | * `day_cloudy` 45 | * `cloudy` 46 | * `cloudy_windy` 47 | * `showers` 48 | * `rain` 49 | * `thunderstorm` 50 | * `snow` 51 | * `fog` 52 | * `night_clear` 53 | * `night_cloudy` 54 | * `night_showers` 55 | * `night_rain` 56 | * `night_thunderstorm` 57 | * `night_snow` 58 | * `night_alt_cloudy_windy` 59 | 60 | #### Example use with CurrentWeather module 61 | ````javascript 62 | config: { 63 | compliments: { 64 | day_sunny: [ 65 | "Today is a sunny day", 66 | "It's a beautiful day" 67 | ], 68 | snow: [ 69 | "Snowball battle!" 70 | ], 71 | rain: [ 72 | "Don't forget your umbrella" 73 | ] 74 | } 75 | } 76 | ```` 77 | 78 | 79 | #### Default value: 80 | ````javascript 81 | config: { 82 | compliments: { 83 | anytime: [ 84 | "Hey there sexy!" 85 | ], 86 | morning: [ 87 | "Good morning, handsome!", 88 | "Enjoy your day!", 89 | "How was your sleep?" 90 | ], 91 | afternoon: [ 92 | "Hello, beauty!", 93 | "You look sexy!", 94 | "Looking good today!" 95 | ], 96 | evening: [ 97 | "Wow, you look hot!", 98 | "You look nice!", 99 | "Hi, sexy!" 100 | ] 101 | } 102 | } 103 | ```` 104 | 105 | ### External Compliment File 106 | You may specify an external file that contains the three compliment arrays. This is particularly useful if you have a 107 | large number of compliments and do not wish to crowd your `config.js` file with a large array of compliments. 108 | Adding the `remoteFile` variable will override an array you specify in the configuration file. 109 | 110 | This file must be straight JSON. Note that the array names need quotes 111 | around them ("morning", "afternoon", "evening", "snow", "rain", etc.). 112 | #### Example compliments.json file: 113 | ````json 114 | { 115 | "anytime" : [ 116 | "Hey there sexy!" 117 | ], 118 | "morning" : [ 119 | "Good morning, sunshine!", 120 | "Who needs coffee when you have your smile?", 121 | "Go get 'em, Tiger!" 122 | ], 123 | "afternoon" : [ 124 | "Hitting your stride!", 125 | "You are making a difference!", 126 | "You're more fun than bubble wrap!" 127 | ], 128 | "evening" : [ 129 | "You made someone smile today, I know it.", 130 | "You are making a difference.", 131 | "The day was better for your efforts." 132 | ] 133 | } 134 | ```` 135 | -------------------------------------------------------------------------------- /app/mirror/modules/default/WeatherForecast/core/utils.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | export function parseWeather(forecast) { 4 | let t; 5 | if (forecast.main) { 6 | t = { min: forecast.main.temp_min, max: forecast.main.temp_max }; 7 | } 8 | return { ...forecast, temp: t, timeOfDay: moment(forecast.dt, 'X').format('h A') }; 9 | } 10 | 11 | export function processWeather(data) { 12 | if (!data) { 13 | // Did not receive usable new data. 14 | // Maybe this needs a better check? 15 | console.log('didnt work'); 16 | return; 17 | } 18 | 19 | const getForecast = () => { 20 | const forecastData = {}; 21 | let lastDay = null; 22 | return data.list.map((f) => { 23 | const forecast = { ...parseWeather(f) }; 24 | const fDay = moment(forecast.dt, 'X').format('ddd'); 25 | const hour = moment(forecast.dt, 'X').format('H'); 26 | if (fDay !== lastDay) { 27 | const fData = { 28 | day: fDay, 29 | icon: iconTable[forecast.weather[0].icon], 30 | maxTemp: roundValue(forecast.temp.max), 31 | minTemp: roundValue(forecast.temp.min), 32 | rain: roundValue(forecast.rain), 33 | timeOfDay: forecast.timeOfDay 34 | }; 35 | 36 | lastDay = fData; 37 | return fData; 38 | } 39 | forecastData.maxTemp = (forecast.temp.max > parseFloat(forecastData.maxTemp)) ? roundValue(forecast.temp.max) : forecastData.maxTemp; 40 | forecastData.minTemp = (forecast.temp.min < parseFloat(forecastData.minTemp)) ? roundValue(forecast.temp.min) : forecastData.minTemp; 41 | if (hour >= 8 && hour <= 17) { 42 | forecastData.icon = iconTable[forecast.weather[0].icon]; 43 | } 44 | }); 45 | }; 46 | 47 | const weatherObj = { 48 | fetchedLocationName: `${data.city.name}, ${data.city.country}`, 49 | forecast: getForecast(), 50 | loading: false, 51 | }; 52 | 53 | return weatherObj; 54 | } 55 | 56 | export const iconTable = { 57 | '01d': 'wi-day-sunny', 58 | '02d': 'wi-day-cloudy', 59 | '03d': 'wi-cloudy', 60 | '04d': 'wi-cloudy-windy', 61 | '09d': 'wi-showers', 62 | '10d': 'wi-rain', 63 | '11d': 'wi-thunderstorm', 64 | '13d': 'wi-snow', 65 | '50d': 'wi-fog', 66 | '01n': 'wi-night-clear', 67 | '02n': 'wi-night-cloudy', 68 | '03n': 'wi-night-cloudy', 69 | '04n': 'wi-night-cloudy', 70 | '09n': 'wi-night-showers', 71 | '10n': 'wi-night-rain', 72 | '11n': 'wi-night-thunderstorm', 73 | '13n': 'wi-night-snow', 74 | '50n': 'wi-night-alt-cloudy-windy' 75 | }; 76 | 77 | /* function(temperature) 78 | * Rounds a temperature to 1 decimal or integer (depending on config.roundTemp). 79 | * 80 | * argument temperature number - Temperature. 81 | * 82 | * return string - Rounded Temperature. 83 | */ 84 | export function roundValue(temperature, roundTemp) { 85 | const decimals = roundTemp ? 0 : 1; 86 | return parseFloat(temperature).toFixed(decimals); 87 | } 88 | 89 | /* ms2Beaufort(ms) 90 | * Converts m2 to beaufort (windspeed). 91 | * 92 | * see: 93 | * http://www.spc.noaa.gov/faq/tornado/beaufort.html 94 | * https://en.wikipedia.org/wiki/Beaufort_scale#Modern_scale 95 | * 96 | * argument ms number - Windspeed in m/s. 97 | * 98 | * return number - Windspeed in beaufort. 99 | */ 100 | export function ms2Beaufort(ms) { 101 | const kmh = ((ms * 60 * 60) / 1000); 102 | const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000]; 103 | 104 | for (let i = 0; i < speeds.length; i += 1) { 105 | const speed = speeds[speeds[i]]; 106 | if (speed > kmh) { 107 | return speeds[i]; 108 | } 109 | } 110 | return 12; 111 | } 112 | 113 | export function deg2Cardinal(deg) { 114 | if (deg > 11.25 && deg <= 33.75) { 115 | return 'NNE'; 116 | } else if (deg > 33.75 && deg <= 56.25) { 117 | return 'NE'; 118 | } else if (deg > 56.25 && deg <= 78.75) { 119 | return 'ENE'; 120 | } else if (deg > 78.75 && deg <= 101.25) { 121 | return 'E'; 122 | } else if (deg > 101.25 && deg <= 123.75) { 123 | return 'ESE'; 124 | } else if (deg > 123.75 && deg <= 146.25) { 125 | return 'SE'; 126 | } else if (deg > 146.25 && deg <= 168.75) { 127 | return 'SSE'; 128 | } else if (deg > 168.75 && deg <= 191.25) { 129 | return 'S'; 130 | } else if (deg > 191.25 && deg <= 213.75) { 131 | return 'SSW'; 132 | } else if (deg > 213.75 && deg <= 236.25) { 133 | return 'SW'; 134 | } else if (deg > 236.25 && deg <= 258.75) { 135 | return 'WSW'; 136 | } else if (deg > 258.75 && deg <= 281.25) { 137 | return 'W'; 138 | } else if (deg > 281.25 && deg <= 303.75) { 139 | return 'WNW'; 140 | } else if (deg > 303.75 && deg <= 326.25) { 141 | return 'NW'; 142 | } else if (deg > 326.25 && deg <= 348.75) { 143 | return 'NNW'; 144 | } else { 145 | return 'N'; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /app/mirror/modules/default/Compliments/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import moment from 'moment'; 5 | import classNames from 'classnames'; 6 | import styles from './Compliments.css'; 7 | import notifications from '../../../core/notifications'; 8 | import { defaultCompliments, getRandom, complimentFile, weatherIconTable } from './core/utils'; 9 | 10 | class Compliments extends Component { 11 | 12 | constructor(props: any) { 13 | super(props); 14 | if (this.props.config && this.props.config.remoteFile) { 15 | complimentFile(this.props.config.remoteFile, (response) => { 16 | this.setState({ 17 | compliments: JSON.parse(response) 18 | }); 19 | }); 20 | } 21 | 22 | this.updateModule = this.updateModule.bind(this); 23 | this.hideShowModule = this.hideShowModule.bind(this); 24 | this.randomIndex = this.randomIndex.bind(this); 25 | this.complimentArray = this.complimentArray.bind(this); 26 | } 27 | 28 | state = { 29 | opacity: 0, 30 | intervalId: null, 31 | showHideTimer: null, 32 | complimentIndex: 0, 33 | currentWeatherType: '', 34 | lastComplimentIndex: -1, 35 | compliments: defaultCompliments, 36 | currentCompliments: [], 37 | hidden: true, 38 | }; 39 | 40 | componentDidMount() { 41 | this.updateModule(); 42 | this.setState({ 43 | intervalId: setInterval(() => { 44 | this.updateModule(); 45 | }, this.props.updateInterval), 46 | }); 47 | 48 | notifications.on('NOTIFICATION', (arg) => { 49 | switch (arg.type) { 50 | case 'CURRENTWEATHER_DATA': 51 | this.setState({ 52 | currentWeatherType: weatherIconTable[arg.payload.weather[0].icon] 53 | }); 54 | break; 55 | } 56 | }); 57 | } 58 | 59 | componentWillUnmount() { 60 | clearInterval(this.state.intervalId); 61 | clearTimeout(this.state.showHideTimer); 62 | } 63 | 64 | hideShowModule(hide: boolean, callback: any) { 65 | this.setState({ 66 | opacity: hide ? 0 : 1, 67 | hidden: hide 68 | }); 69 | if (hide && callback) { 70 | this.setState({ 71 | showHideTimer: setTimeout(() => { callback(); }, this.props.fadeSpeed / 2), 72 | }); 73 | } else { 74 | clearTimeout(this.state.showHideTimer); 75 | } 76 | } 77 | 78 | randomIndex() { 79 | const len = this.state.currentCompliments.length; 80 | 81 | if (len === 1) { 82 | return 0; 83 | } 84 | 85 | let compIndex = getRandom(len); 86 | 87 | while (compIndex === this.state.lastComplimentIndex) { 88 | compIndex = getRandom(len); 89 | } 90 | 91 | this.setState({ 92 | lastComplimentIndex: compIndex, 93 | }); 94 | 95 | return compIndex; 96 | } 97 | 98 | complimentArray() { 99 | const hour = moment().hour(); 100 | let compliments = []; 101 | 102 | if (hour >= 3 && hour < 12 && this.state.compliments.hasOwnProperty('morning')) { 103 | compliments = this.state.compliments.morning; 104 | } else if (hour >= 12 && hour < 17 && this.state.compliments.hasOwnProperty('afternoon')) { 105 | compliments = this.state.compliments.afternoon; 106 | } else if (this.state.compliments.hasOwnProperty('evening')) { 107 | compliments = this.state.compliments.evening; 108 | } 109 | 110 | if (this.state.currentWeatherType in this.state.compliments) { 111 | compliments = [...compliments, this.state.compliments[this.state.currentWeatherType]]; 112 | } 113 | 114 | compliments = [...compliments, ...this.state.compliments.anytime]; 115 | 116 | return compliments; 117 | } 118 | 119 | updateModule() { 120 | this.hideShowModule(true, () => { 121 | this.setState({ 122 | complimentIndex: this.randomIndex(), 123 | currentCompliments: this.complimentArray(), 124 | }, () => { 125 | this.hideShowModule(false); 126 | }); 127 | }); 128 | } 129 | 130 | render() { 131 | //console.log(this.state.opacity); 132 | return ( 133 |
140 | {this.state.currentCompliments[this.state.complimentIndex]} 141 |
142 | ); 143 | } 144 | } 145 | 146 | Compliments.moduleName = 'Compliments'; 147 | 148 | Compliments.defaultProps = { 149 | fadeSpeed: 4000, 150 | updateInterval: 30000, 151 | animation: 'ease-in', 152 | position: 'lower_third', 153 | }; 154 | 155 | Compliments.propTypes = { 156 | fadeSpeed: PropTypes.number, 157 | updateInterval: PropTypes.number, 158 | animation: PropTypes.string, 159 | position: PropTypes.string 160 | }; 161 | 162 | export default Compliments; 163 | -------------------------------------------------------------------------------- /webpack.config.renderer.prod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron renderer process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 8 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 9 | import merge from 'webpack-merge'; 10 | // import BabiliPlugin from 'babili-webpack-plugin'; 11 | import baseConfig from './webpack.config.base'; 12 | import CheckNodeEnv from './internals/scripts/CheckNodeEnv'; 13 | import defaults from './app/mirror/config/defaults'; 14 | import config from './app/mirror/config/config'; 15 | 16 | CheckNodeEnv('production'); 17 | 18 | export default merge.smart(baseConfig, { 19 | devtool: 'source-map', 20 | 21 | target: 'electron-renderer', 22 | 23 | entry: './app/index', 24 | 25 | output: { 26 | path: path.join(__dirname, 'app/dist'), 27 | publicPath: '../dist/', 28 | filename: 'renderer.prod.js' 29 | }, 30 | 31 | module: { 32 | rules: [ 33 | // Extract all .global.css to style.css as is 34 | { 35 | test: /\.global\.css$/, 36 | use: ExtractTextPlugin.extract({ 37 | use: 'css-loader', 38 | fallback: 'style-loader', 39 | }) 40 | }, 41 | // Pipe other styles through css modules and append to style.css 42 | { 43 | test: /^((?!\.global).)*\.css$/, 44 | use: ExtractTextPlugin.extract({ 45 | use: { 46 | loader: 'css-loader', 47 | options: { 48 | modules: true, 49 | importLoaders: 1, 50 | localIdentName: '[name]__[local]__[hash:base64:5]', 51 | } 52 | } 53 | }), 54 | }, 55 | // Add SASS support - compile all .global.scss files and pipe it to style.css 56 | { 57 | test: /\.global\.scss$/, 58 | use: ExtractTextPlugin.extract({ 59 | use: [ 60 | { 61 | loader: 'css-loader' 62 | }, 63 | { 64 | loader: 'sass-loader' 65 | } 66 | ], 67 | fallback: 'style-loader', 68 | }) 69 | }, 70 | // Add SASS support - compile all other .scss files and pipe it to style.css 71 | { 72 | test: /^((?!\.global).)*\.scss$/, 73 | use: ExtractTextPlugin.extract({ 74 | use: [{ 75 | loader: 'css-loader', 76 | options: { 77 | modules: true, 78 | importLoaders: 1, 79 | localIdentName: '[name]__[local]__[hash:base64:5]', 80 | } 81 | }, 82 | { 83 | loader: 'sass-loader' 84 | }] 85 | }), 86 | }, 87 | // WOFF Font 88 | { 89 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 90 | use: { 91 | loader: 'url-loader', 92 | options: { 93 | limit: 10000, 94 | mimetype: 'application/font-woff', 95 | } 96 | }, 97 | }, 98 | // WOFF2 Font 99 | { 100 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 101 | use: { 102 | loader: 'url-loader', 103 | options: { 104 | limit: 10000, 105 | mimetype: 'application/font-woff', 106 | } 107 | } 108 | }, 109 | // TTF Font 110 | { 111 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 112 | use: { 113 | loader: 'url-loader', 114 | options: { 115 | limit: 10000, 116 | mimetype: 'application/octet-stream' 117 | } 118 | } 119 | }, 120 | // EOT Font 121 | { 122 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 123 | use: 'file-loader', 124 | }, 125 | // SVG Font 126 | { 127 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 128 | use: { 129 | loader: 'url-loader', 130 | options: { 131 | limit: 10000, 132 | mimetype: 'image/svg+xml', 133 | } 134 | } 135 | }, 136 | // Common Image Formats 137 | { 138 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 139 | use: 'url-loader', 140 | } 141 | ] 142 | }, 143 | 144 | plugins: [ 145 | /** 146 | * Create global constants which can be configured at compile time. 147 | * 148 | * Useful for allowing different behaviour between development builds and 149 | * release builds 150 | * 151 | * NODE_ENV should be production so that modules do not perform certain 152 | * development checks 153 | */ 154 | new webpack.DefinePlugin({ 155 | 'process.env.MIRROR_CONFIG': JSON.stringify(process.env.MIRROR_CONFIG || { ...defaults, ...config }), 156 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') 157 | }), 158 | 159 | /** 160 | * Babli is an ES6+ aware minifier based on the Babel toolchain (beta) 161 | */ 162 | // new BabiliPlugin(), 163 | 164 | new ExtractTextPlugin('style.css'), 165 | 166 | new BundleAnalyzerPlugin({ 167 | analyzerMode: process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 168 | openAnalyzer: process.env.OPEN_ANALYZER === 'true' 169 | }), 170 | ], 171 | }); 172 | -------------------------------------------------------------------------------- /webpack.config.renderer.dev.dll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import path from 'path'; 7 | import merge from 'webpack-merge'; 8 | import baseConfig from './webpack.config.base'; 9 | import { dependencies } from './package.json'; 10 | import CheckNodeEnv from './internals/scripts/CheckNodeEnv'; 11 | 12 | CheckNodeEnv('development'); 13 | 14 | const dist = path.resolve(process.cwd(), 'dll'); 15 | 16 | export default merge.smart(baseConfig, { 17 | context: process.cwd(), 18 | 19 | devtool: 'eval', 20 | 21 | target: 'electron-renderer', 22 | 23 | externals: ['fsevents', 'crypto-browserify'], 24 | 25 | /** 26 | * @HACK: Copy and pasted from renderer dev config. Consider merging these 27 | * rules into the base config. May cause breaking changes. 28 | */ 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.global\.css$/, 33 | use: [ 34 | { 35 | loader: 'style-loader' 36 | }, 37 | { 38 | loader: 'css-loader', 39 | options: { 40 | sourceMap: true, 41 | }, 42 | } 43 | ] 44 | }, 45 | { 46 | test: /^((?!\.global).)*\.css$/, 47 | use: [ 48 | { 49 | loader: 'style-loader' 50 | }, 51 | { 52 | loader: 'css-loader', 53 | options: { 54 | modules: true, 55 | sourceMap: true, 56 | importLoaders: 1, 57 | localIdentName: '[name]__[local]__[hash:base64:5]', 58 | } 59 | }, 60 | ] 61 | }, 62 | // Add SASS support - compile all .global.scss files and pipe it to style.css 63 | { 64 | test: /\.global\.scss$/, 65 | use: [ 66 | { 67 | loader: 'style-loader' 68 | }, 69 | { 70 | loader: 'css-loader', 71 | options: { 72 | sourceMap: true, 73 | }, 74 | }, 75 | { 76 | loader: 'sass-loader' 77 | } 78 | ] 79 | }, 80 | // Add SASS support - compile all other .scss files and pipe it to style.css 81 | { 82 | test: /^((?!\.global).)*\.scss$/, 83 | use: [ 84 | { 85 | loader: 'style-loader' 86 | }, 87 | { 88 | loader: 'css-loader', 89 | options: { 90 | modules: true, 91 | sourceMap: true, 92 | importLoaders: 1, 93 | localIdentName: '[name]__[local]__[hash:base64:5]', 94 | } 95 | }, 96 | { 97 | loader: 'sass-loader' 98 | } 99 | ] 100 | }, 101 | // WOFF Font 102 | { 103 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 104 | use: { 105 | loader: 'url-loader', 106 | options: { 107 | limit: 10000, 108 | mimetype: 'application/font-woff', 109 | } 110 | }, 111 | }, 112 | // WOFF2 Font 113 | { 114 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 115 | use: { 116 | loader: 'url-loader', 117 | options: { 118 | limit: 10000, 119 | mimetype: 'application/font-woff', 120 | } 121 | } 122 | }, 123 | // TTF Font 124 | { 125 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 126 | use: { 127 | loader: 'url-loader', 128 | options: { 129 | limit: 10000, 130 | mimetype: 'application/octet-stream' 131 | } 132 | } 133 | }, 134 | // EOT Font 135 | { 136 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 137 | use: 'file-loader', 138 | }, 139 | // SVG Font 140 | { 141 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 142 | use: { 143 | loader: 'url-loader', 144 | options: { 145 | limit: 10000, 146 | mimetype: 'image/svg+xml', 147 | } 148 | } 149 | }, 150 | // Common Image Formats 151 | { 152 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 153 | use: 'url-loader', 154 | } 155 | ] 156 | }, 157 | 158 | resolve: { 159 | modules: [ 160 | 'app', 161 | ], 162 | }, 163 | 164 | entry: { 165 | renderer: ( 166 | Object 167 | .keys(dependencies || {}) 168 | .filter(dependency => dependency !== 'font-awesome') 169 | ) 170 | }, 171 | 172 | output: { 173 | library: 'renderer', 174 | path: dist, 175 | filename: '[name].dev.dll.js', 176 | libraryTarget: 'var' 177 | }, 178 | 179 | plugins: [ 180 | new webpack.DllPlugin({ 181 | path: path.join(dist, '[name].json'), 182 | name: '[name]', 183 | }), 184 | 185 | /** 186 | * Create global constants which can be configured at compile time. 187 | * 188 | * Useful for allowing different behaviour between development builds and 189 | * release builds 190 | * 191 | * NODE_ENV should be production so that modules do not perform certain 192 | * development checks 193 | */ 194 | new webpack.DefinePlugin({ 195 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') 196 | }), 197 | 198 | new webpack.LoaderOptionsPlugin({ 199 | debug: true, 200 | options: { 201 | context: path.resolve(process.cwd(), 'app'), 202 | output: { 203 | path: path.resolve(process.cwd(), 'dll'), 204 | }, 205 | }, 206 | }) 207 | ], 208 | }); 209 | -------------------------------------------------------------------------------- /app/menu.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { app, Menu, shell, BrowserWindow } from 'electron'; 3 | 4 | export default class MenuBuilder { 5 | mainWindow: BrowserWindow; 6 | 7 | constructor(mainWindow: BrowserWindow) { 8 | this.mainWindow = mainWindow; 9 | } 10 | 11 | buildMenu() { 12 | if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { 13 | this.setupDevelopmentEnvironment(); 14 | } 15 | 16 | let template; 17 | 18 | if (process.platform === 'darwin') { 19 | template = this.buildDarwinTemplate(); 20 | } else { 21 | template = this.buildDefaultTemplate(); 22 | } 23 | 24 | const menu = Menu.buildFromTemplate(template); 25 | Menu.setApplicationMenu(menu); 26 | 27 | return menu; 28 | } 29 | 30 | setupDevelopmentEnvironment() { 31 | this.mainWindow.openDevTools(); 32 | this.mainWindow.webContents.on('context-menu', (e, props) => { 33 | const { x, y } = props; 34 | 35 | Menu 36 | .buildFromTemplate([{ 37 | label: 'Inspect element', 38 | click: () => { 39 | this.mainWindow.inspectElement(x, y); 40 | } 41 | }]) 42 | .popup(this.mainWindow); 43 | }); 44 | } 45 | 46 | buildDarwinTemplate() { 47 | const subMenuAbout = { 48 | label: 'Electron', 49 | submenu: [ 50 | { label: 'About ElectronReact', selector: 'orderFrontStandardAboutPanel:' }, 51 | { type: 'separator' }, 52 | { label: 'Services', submenu: [] }, 53 | { type: 'separator' }, 54 | { label: 'Hide ElectronReact', accelerator: 'Command+H', selector: 'hide:' }, 55 | { label: 'Hide Others', accelerator: 'Command+Shift+H', selector: 'hideOtherApplications:' }, 56 | { label: 'Show All', selector: 'unhideAllApplications:' }, 57 | { type: 'separator' }, 58 | { label: 'Quit', accelerator: 'Command+Q', click: () => { app.quit(); } } 59 | ] 60 | }; 61 | const subMenuEdit = { 62 | label: 'Edit', 63 | submenu: [ 64 | { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' }, 65 | { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' }, 66 | { type: 'separator' }, 67 | { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' }, 68 | { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' }, 69 | { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' }, 70 | { label: 'Select All', accelerator: 'Command+A', selector: 'selectAll:' } 71 | ] 72 | }; 73 | const subMenuViewDev = { 74 | label: 'View', 75 | submenu: [ 76 | { label: 'Reload', accelerator: 'Command+R', click: () => { this.mainWindow.webContents.reload(); } }, 77 | { label: 'Toggle Full Screen', accelerator: 'Ctrl+Command+F', click: () => { this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); } }, 78 | { label: 'Toggle Developer Tools', accelerator: 'Alt+Command+I', click: () => { this.mainWindow.toggleDevTools(); } } 79 | ] 80 | }; 81 | const subMenuViewProd = { 82 | label: 'View', 83 | submenu: [ 84 | { label: 'Toggle Full Screen', accelerator: 'Ctrl+Command+F', click: () => { this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); } } 85 | ] 86 | }; 87 | const subMenuWindow = { 88 | label: 'Window', 89 | submenu: [ 90 | { label: 'Minimize', accelerator: 'Command+M', selector: 'performMiniaturize:' }, 91 | { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }, 92 | { type: 'separator' }, 93 | { label: 'Bring All to Front', selector: 'arrangeInFront:' } 94 | ] 95 | }; 96 | const subMenuHelp = { 97 | label: 'Help', 98 | submenu: [ 99 | { label: 'Learn More', click() { shell.openExternal('http://electron.atom.io'); } }, 100 | { label: 'Documentation', click() { shell.openExternal('https://github.com/atom/electron/tree/master/docs#readme'); } }, 101 | { label: 'Community Discussions', click() { shell.openExternal('https://discuss.atom.io/c/electron'); } }, 102 | { label: 'Search Issues', click() { shell.openExternal('https://github.com/atom/electron/issues'); } } 103 | ] 104 | }; 105 | 106 | const subMenuView = process.env.NODE_ENV === 'development' 107 | ? subMenuViewDev 108 | : subMenuViewProd; 109 | 110 | return [ 111 | subMenuAbout, 112 | subMenuEdit, 113 | subMenuView, 114 | subMenuWindow, 115 | subMenuHelp 116 | ]; 117 | } 118 | 119 | buildDefaultTemplate() { 120 | const templateDefault = [{ 121 | label: '&File', 122 | submenu: [{ 123 | label: '&Open', 124 | accelerator: 'Ctrl+O' 125 | }, { 126 | label: '&Close', 127 | accelerator: 'Ctrl+W', 128 | click: () => { 129 | this.mainWindow.close(); 130 | } 131 | }] 132 | }, { 133 | label: '&View', 134 | submenu: (process.env.NODE_ENV === 'development') ? [{ 135 | label: '&Reload', 136 | accelerator: 'Ctrl+R', 137 | click: () => { 138 | this.mainWindow.webContents.reload(); 139 | } 140 | }, { 141 | label: 'Toggle &Full Screen', 142 | accelerator: 'F11', 143 | click: () => { 144 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 145 | } 146 | }, { 147 | label: 'Toggle &Developer Tools', 148 | accelerator: 'Alt+Ctrl+I', 149 | click: () => { 150 | this.mainWindow.toggleDevTools(); 151 | } 152 | }] : [{ 153 | label: 'Toggle &Full Screen', 154 | accelerator: 'F11', 155 | click: () => { 156 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 157 | } 158 | }] 159 | }, { 160 | label: 'Help', 161 | submenu: [{ 162 | label: 'Learn More', 163 | click() { 164 | shell.openExternal('http://electron.atom.io'); 165 | } 166 | }, { 167 | label: 'Documentation', 168 | click() { 169 | shell.openExternal('https://github.com/atom/electron/tree/master/docs#readme'); 170 | } 171 | }, { 172 | label: 'Community Discussions', 173 | click() { 174 | shell.openExternal('https://discuss.atom.io/c/electron'); 175 | } 176 | }, { 177 | label: 'Search Issues', 178 | click() { 179 | shell.openExternal('https://github.com/atom/electron/issues'); 180 | } 181 | }] 182 | }]; 183 | 184 | return templateDefault; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /app/mirror/modules/default/NewsFeed/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import moment from 'moment'; 5 | import classNames from 'classnames'; 6 | import request from 'request'; 7 | 8 | class NewsFeed extends Component { 9 | 10 | constructor(props: any) { 11 | super(props); 12 | this.updateModule = this.updateModule.bind(this); 13 | this.hideShowModule = this.hideShowModule.bind(this); 14 | this.foramtUrlParams = this.foramtUrlParams.bind(this); 15 | this.fetchNewsItems = this.fetchNewsItems.bind(this); 16 | } 17 | 18 | state = { 19 | opacity: 0, 20 | hidden: true, 21 | intervalId: null, 22 | refreshId: null, 23 | showHideTimer: null, 24 | newsItems: [], 25 | loaded: false, 26 | activeItem: -1, 27 | }; 28 | 29 | componentDidMount() { 30 | this.fetchNewsItems((items) => { 31 | this.setState({ 32 | loaded: true, 33 | newsItems: items, 34 | intervalId: setInterval(() => { 35 | this.updateModule(); 36 | }, this.props.updateInterval), 37 | refreshId: setInterval(() => { 38 | this.fetchNewsItems((items)=>{ 39 | this.setState({ 40 | newsItems: items 41 | }) 42 | }); 43 | }, this.props.refetchInterval), 44 | }); 45 | this.updateModule(); 46 | }); 47 | } 48 | 49 | componentWillUnmount() { 50 | clearInterval(this.state.intervalId); 51 | clearInterval(this.state.refreshId); 52 | clearTimeout(this.state.showHideTimer); 53 | } 54 | 55 | hideShowModule(hide: boolean, callback: any) { 56 | this.setState({ 57 | opacity: hide ? 0 : 1, 58 | hidden: hide 59 | }); 60 | if (hide && callback) { 61 | this.setState({ 62 | showHideTimer: setTimeout(() => { callback(); }, this.props.fadeSpeed / 2), 63 | }); 64 | } else { 65 | clearTimeout(this.state.showHideTimer); 66 | } 67 | } 68 | 69 | updateModule() { 70 | if (this.state.newsItems.length > 0) { 71 | this.hideShowModule(true, () => { 72 | let index = this.state.activeItem; 73 | if (this.state.activeItem < this.state.newsItems.length - 1) { 74 | index += 1; 75 | } else { 76 | index = 0; 77 | } 78 | this.setState({ 79 | activeItem: index, 80 | }, () => { 81 | this.hideShowModule(false); 82 | }); 83 | }); 84 | } 85 | } 86 | 87 | foramtUrlParams() { 88 | let url = `${this.props.apiBase}${this.props.apiVersion}${this.props.apiEndpoint}sources=`; 89 | url += this.props.sources.join(); 90 | url += `&apiKey=${this.props.apiKey}`; 91 | return url; 92 | } 93 | 94 | fetchNewsItems(callback) { 95 | if (!this.props.apiKey) { 96 | console.log('There is no api Key'); 97 | return; 98 | } 99 | 100 | const URL = this.foramtUrlParams(); 101 | request({ 102 | method: 'GET', 103 | url: URL, 104 | }, (error, response, body) => { 105 | if (!error && response.statusCode === 200) { 106 | callback(JSON.parse(body).articles); 107 | } else { 108 | callback({ error: `${NewsFeed.moduleName}: Could not load news items.` }); 109 | } 110 | }); 111 | } 112 | 113 | render() { 114 | if (!this.props.apiKey) { 115 | return ( 116 |
117 | Their is no api key in the configuration options for the newsfeed module. 118 |
119 | Please check the documentation 120 |
121 | ); 122 | } 123 | 124 | if (!this.state.loaded || this.state.activeItem < 0) { 125 | return ( 126 |
127 | {this.props.hideLoading ? '' : 'Loading'} 128 |
129 | ); 130 | } 131 | 132 | let sourceTitle; 133 | let newsTitle; 134 | let description; 135 | let fullArticle; 136 | if (this.state.newsItems.length > 0 && this.state.loaded) { 137 | if (this.props.showSourceTitle) { 138 | sourceTitle = ( 139 |
140 | {this.state.newsItems[this.state.activeItem].source.name} 141 | { 142 | this.props.showPublishDate && 143 | `, ${moment(new Date(this.state.newsItems[this.state.activeItem].publishedAt)).fromNow()}` 144 | }: 145 |
146 | ); 147 | } 148 | 149 | if (this.props.showFullArticle) { 150 | fullArticle = ( 151 |