├── app ├── utils │ ├── .gitkeep │ ├── types.js │ ├── notification.js │ ├── helper.js │ ├── storage.js │ ├── index.js │ └── recorder.js ├── app.icns ├── main.prod.js.LICENSE ├── components │ ├── DeviceSelect │ │ ├── style.scss │ │ └── index.js │ ├── CustomButton │ │ ├── style.scss │ │ └── index.js │ ├── RecordingBar │ │ ├── style.scss │ │ └── index.js │ └── RecordingPreview │ │ ├── style.scss │ │ └── index.js ├── .testcafe-electron-rc ├── containers │ ├── Content │ │ ├── Recording │ │ │ ├── style.scss │ │ │ └── index.js │ │ ├── Settings │ │ │ ├── style.scss │ │ │ ├── index.js │ │ │ └── setting-form.js │ │ └── index.js │ ├── Header │ │ ├── style.scss │ │ └── index.js │ ├── Footer │ │ └── index.js │ ├── contents.js │ ├── style.scss │ ├── Sider │ │ └── index.js │ └── index.js ├── event.js ├── Routes.js ├── reducers │ ├── index.js │ ├── types.js │ └── recorder.js ├── store │ ├── configureStore.js │ ├── configureStore.prod.js │ └── configureStore.dev.js ├── actions │ ├── recorder.js │ └── counter.js ├── package.json ├── index.js ├── app.html ├── app.global.css ├── main.dev.js └── menu.js ├── renovate.json ├── 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 │ ├── erb-banner.png │ ├── erb-logo.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 │ ├── BabelRegister.js │ ├── CheckYarn.js │ ├── CheckNodeEnv.js │ ├── CheckPortInUse.js │ ├── ElectronRebuild.js │ ├── CheckBuildsExist.js │ └── CheckNativeDep.js ├── resources ├── icon.ico ├── icon.png ├── icon.icns └── icons │ ├── 16x16.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 64x64.png │ ├── 128x128.png │ └── 256x256.png ├── flow-typed └── module_vx.x.x.js ├── .stylelintrc ├── screenshots ├── recording.png └── settings.png ├── .gitattributes ├── test ├── example.js ├── e2e │ ├── helpers.js │ └── HomePage.e2e.js ├── .eslintrc.json ├── actions │ ├── __snapshots__ │ │ └── counter.spec.js.snap │ └── counter.spec.js ├── reducers │ ├── __snapshots__ │ │ └── counter.spec.js.snap │ └── counter.spec.js ├── components │ ├── __snapshots__ │ │ └── Counter.spec.js.snap │ └── Counter.spec.js └── containers │ └── CounterPage.spec.js ├── configs ├── webpack.config.eslint.js ├── webpack.config.base.js ├── webpack.config.renderer.dev.dll.babel.js ├── webpack.config.main.prod.babel.js ├── webpack.config.renderer.prod.babel.js └── webpack.config.renderer.dev.babel.js ├── .vscode ├── extensions.json └── settings.json ├── .eslintrc.js ├── .prettierrc.json ├── .editorconfig ├── appveyor.yml ├── .flowconfig ├── .dockerignore ├── .gitignore ├── .eslintignore ├── README.md ├── .travis.yml ├── babel.config.js ├── CODE_OF_CONDUCT.md ├── package.json ├── CHANGELOG.md └── LICENSE /app/utils/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["bliss"] 3 | } 4 | -------------------------------------------------------------------------------- /internals/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /app/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/app/app.icns -------------------------------------------------------------------------------- /app/main.prod.js.LICENSE: -------------------------------------------------------------------------------- 1 | /*! http://mths.be/fromcodepoint v0.1.0 by @mathias */ 2 | -------------------------------------------------------------------------------- /internals/flow/WebpackAsset.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | declare export default string 3 | -------------------------------------------------------------------------------- /app/components/DeviceSelect/style.scss: -------------------------------------------------------------------------------- 1 | .select-device { 2 | min-width: 420px; 3 | } 4 | -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/resources/icon.png -------------------------------------------------------------------------------- /app/.testcafe-electron-rc: -------------------------------------------------------------------------------- 1 | { 2 | "mainWindowUrl": "./app.html", 3 | "appPath": "." 4 | } 5 | -------------------------------------------------------------------------------- /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 } -------------------------------------------------------------------------------- /internals/img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/js.png -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/resources/icon.icns -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard", "stylelint-config-prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /internals/img/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/flow.png -------------------------------------------------------------------------------- /internals/img/jest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/jest.png -------------------------------------------------------------------------------- /internals/img/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/npm.png -------------------------------------------------------------------------------- /internals/img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/react.png -------------------------------------------------------------------------------- /internals/img/redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/redux.png -------------------------------------------------------------------------------- /internals/img/yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/yarn.png -------------------------------------------------------------------------------- /internals/img/eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/eslint.png -------------------------------------------------------------------------------- /internals/img/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/webpack.png -------------------------------------------------------------------------------- /resources/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/resources/icons/16x16.png -------------------------------------------------------------------------------- /resources/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/resources/icons/32x32.png -------------------------------------------------------------------------------- /resources/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/resources/icons/48x48.png -------------------------------------------------------------------------------- /resources/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/resources/icons/64x64.png -------------------------------------------------------------------------------- /screenshots/recording.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/screenshots/recording.png -------------------------------------------------------------------------------- /screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/screenshots/settings.png -------------------------------------------------------------------------------- /internals/img/erb-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/erb-banner.png -------------------------------------------------------------------------------- /internals/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/erb-logo.png -------------------------------------------------------------------------------- /internals/img/js-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/js-padded.png -------------------------------------------------------------------------------- /resources/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/resources/icons/128x128.png -------------------------------------------------------------------------------- /resources/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/resources/icons/256x256.png -------------------------------------------------------------------------------- /internals/img/flow-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/flow-padded.png -------------------------------------------------------------------------------- /internals/img/jest-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/jest-padded.png -------------------------------------------------------------------------------- /internals/img/react-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/react-padded.png -------------------------------------------------------------------------------- /internals/img/react-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/react-router.png -------------------------------------------------------------------------------- /internals/img/redux-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/redux-padded.png -------------------------------------------------------------------------------- /internals/img/yarn-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/yarn-padded.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.png binary 3 | *.jpg binary 4 | *.jpeg binary 5 | *.ico binary 6 | *.icns binary 7 | -------------------------------------------------------------------------------- /internals/img/eslint-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/eslint-padded.png -------------------------------------------------------------------------------- /internals/img/flow-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/flow-padded-90.png -------------------------------------------------------------------------------- /internals/img/jest-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/jest-padded-90.png -------------------------------------------------------------------------------- /internals/img/react-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/react-padded-90.png -------------------------------------------------------------------------------- /internals/img/redux-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/redux-padded-90.png -------------------------------------------------------------------------------- /internals/img/webpack-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/webpack-padded.png -------------------------------------------------------------------------------- /internals/img/yarn-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/yarn-padded-90.png -------------------------------------------------------------------------------- /internals/img/eslint-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/eslint-padded-90.png -------------------------------------------------------------------------------- /internals/img/webpack-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/webpack-padded-90.png -------------------------------------------------------------------------------- /internals/img/react-router-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/HEAD/internals/img/react-router-padded.png -------------------------------------------------------------------------------- /internals/img/react-router-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peilinok/EasyRecorder/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 | -------------------------------------------------------------------------------- /internals/scripts/BabelRegister.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | require('@babel/register')({ 4 | cwd: path.join(__dirname, '..', '..') 5 | }); 6 | -------------------------------------------------------------------------------- /app/containers/Content/Recording/style.scss: -------------------------------------------------------------------------------- 1 | .recording-layout { 2 | padding: 16px; 3 | display: flex; 4 | flex-direction: column; 5 | min-height: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /app/containers/Header/style.scss: -------------------------------------------------------------------------------- 1 | .header-layout { 2 | padding: 0 16px; 3 | span { 4 | color: #666; 5 | font-size: 22px; 6 | font-weight: 600; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/e2e/helpers.js: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | import { ClientFunction } from 'testcafe'; 3 | 4 | export const getPageUrl = ClientFunction(() => window.location.href); 5 | -------------------------------------------------------------------------------- /app/containers/Content/Settings/style.scss: -------------------------------------------------------------------------------- 1 | .setting-layout { 2 | padding: 16px; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: flex-start; 7 | } 8 | -------------------------------------------------------------------------------- /configs/webpack.config.eslint.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | require('@babel/register'); 3 | 4 | module.exports = require('./webpack.config.renderer.dev.babel').default; 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "dzannotti.vscode-babel-coloring", 5 | "EditorConfig.EditorConfig", 6 | "flowtype.flow-for-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /app/event.js: -------------------------------------------------------------------------------- 1 | import { dialog, ipcMain } from 'electron'; 2 | 3 | ipcMain.on('custom-open-folder-req', (event, arg) => { 4 | dialog.showOpenDialog({ 5 | ...arg, 6 | properties: ['openDirectory'] 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb', 3 | settings: { 4 | 'import/resolver': { 5 | webpack: { 6 | config: require.resolve('./configs/webpack.config.eslint.js') 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /internals/scripts/CheckYarn.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | if (!/yarn\.js$/.test(process.env.npm_execpath || '')) { 4 | console.warn( 5 | "\u001b[33mYou don't seem to be using yarn. This could produce unexpected results.\u001b[39m" 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": [".prettierrc", ".babelrc", ".eslintrc", ".stylelintrc"], 5 | "options": { 6 | "parser": "json" 7 | } 8 | } 9 | ], 10 | "singleQuote": true 11 | } 12 | -------------------------------------------------------------------------------- /app/Routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | import App from './containers'; 4 | 5 | export default () => ( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /.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 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /app/containers/Footer/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | type Props = {}; 5 | 6 | export default class FooterLayout extends Component { 7 | props: Props; 8 | 9 | render() { 10 | return <>; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plugin:testcafe/recommended", 3 | "env": { 4 | "jest/globals": true 5 | }, 6 | "plugins": ["jest", "testcafe"], 7 | "rules": { 8 | "jest/no-disabled-tests": "warn", 9 | "jest/no-focused-tests": "error", 10 | "jest/no-identical-title": "error" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/components/CustomButton/style.scss: -------------------------------------------------------------------------------- 1 | .icon-button { 2 | -webkit-app-region: no-drag; 3 | color: rgba(37, 29, 29, 0.603) !important; 4 | cursor: pointer; 5 | font-size: 20px; 6 | } 7 | 8 | .icon-button:hover { 9 | color: rgb(177, 157, 157) !important; 10 | } 11 | 12 | .normal-button { 13 | margin: 0 3px; 14 | } 15 | -------------------------------------------------------------------------------- /app/components/RecordingBar/style.scss: -------------------------------------------------------------------------------- 1 | .recording-bar { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: flex-start; 5 | align-items: center; 6 | 7 | .recording-bar-duration { 8 | margin-left: 14px; 9 | color: black; 10 | font-size: 24px; 11 | } 12 | 13 | button { 14 | margin-right: 10px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/actions/__snapshots__/counter.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`actions should decrement should create decrement action 1`] = ` 4 | Object { 5 | "type": "DECREMENT_COUNTER", 6 | } 7 | `; 8 | 9 | exports[`actions should increment should create increment action 1`] = ` 10 | Object { 11 | "type": "INCREMENT_COUNTER", 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /test/reducers/__snapshots__/counter.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`reducers counter should handle DECREMENT_COUNTER 1`] = `0`; 4 | 5 | exports[`reducers counter should handle INCREMENT_COUNTER 1`] = `2`; 6 | 7 | exports[`reducers counter should handle initial state 1`] = `0`; 8 | 9 | exports[`reducers counter should handle unknown action type 1`] = `1`; 10 | -------------------------------------------------------------------------------- /app/reducers/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { combineReducers } from 'redux'; 3 | import { connectRouter } from 'connected-react-router'; 4 | import type { HashHistory } from 'history'; 5 | import recorder from './recorder'; 6 | 7 | export default function createRootReducer(history: HashHistory) { 8 | return combineReducers<{}, *>({ 9 | router: connectRouter(history), 10 | recorder 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /app/utils/types.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export type ContentComponent = { 4 | path: string, 5 | title: string, 6 | icon: string, 7 | component: React.ComponentType, 8 | subItems: Array 9 | }; 10 | 11 | export type DeviceItem = { 12 | id: sring, 13 | name: string, 14 | isDefault: number 15 | }; 16 | 17 | export type EncoderItem = { 18 | id: number, 19 | name: string 20 | }; 21 | -------------------------------------------------------------------------------- /app/store/configureStore.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import configureStoreDev from './configureStore.dev'; 3 | import configureStoreProd from './configureStore.prod'; 4 | 5 | const selectedConfigureStore = 6 | process.env.NODE_ENV === 'production' 7 | ? configureStoreProd 8 | : configureStoreDev; 9 | 10 | const { configureStore, history } = selectedConfigureStore; 11 | 12 | const configuredStore = configureStore(); 13 | 14 | export { configuredStore, history as configuredHistory }; 15 | -------------------------------------------------------------------------------- /app/containers/Header/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | import './style.scss'; 5 | 6 | type Props = { 7 | pathname: string 8 | }; 9 | 10 | export default class HeaderLayout extends Component { 11 | props: Props; 12 | 13 | render() { 14 | const { pathname } = this.props; 15 | 16 | return ( 17 |
18 | {pathname.toUpperCase()} 19 |
20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/containers/contents.js: -------------------------------------------------------------------------------- 1 | import Recording from './Content/Recording'; 2 | import Settings from './Content/Settings'; 3 | 4 | const contents = [ 5 | { 6 | path: '/recording', 7 | title: 'Recording', 8 | icon: 'camera', 9 | component: Recording, 10 | subItems: [] 11 | }, 12 | { 13 | path: '/setting', 14 | title: 'Setting', 15 | icon: 'setting', 16 | component: Settings, 17 | subItems: [] 18 | } 19 | ]; 20 | 21 | export default contents; 22 | -------------------------------------------------------------------------------- /app/components/RecordingPreview/style.scss: -------------------------------------------------------------------------------- 1 | .recording-preview { 2 | flex-grow: 1; 3 | background-color: #ababab; 4 | margin: 16px 0 0 0; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | overflow: hidden; 10 | 11 | h1 { 12 | font-size: 48px; 13 | } 14 | 15 | h2 { 16 | color: rgba(0, 0, 0, 0.85); 17 | font-size: 24px; 18 | } 19 | 20 | canvas { 21 | width: 100%; 22 | height: 100%; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /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( 11 | chalk.whiteBright.bgRed.bold( 12 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 13 | ) 14 | ); 15 | process.exit(2); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/reducers/types.js: -------------------------------------------------------------------------------- 1 | import type { Dispatch as ReduxDispatch, Store as ReduxStore } from 'redux'; 2 | 3 | export type RecorderStateType = { 4 | +isPaused: boolean, 5 | +isRecording: boolean, 6 | +curFile: string 7 | }; 8 | 9 | export type StateType = { 10 | +recorder: RecorderStateType 11 | }; 12 | 13 | export type Action = { 14 | +type: string, 15 | // eslint-disable-next-line flowtype/no-weak-types 16 | +payload?: any 17 | }; 18 | 19 | export type GetState = () => StateType; 20 | 21 | export type Dispatch = ReduxDispatch; 22 | 23 | export type Store = ReduxStore; 24 | -------------------------------------------------------------------------------- /internals/scripts/CheckPortInUse.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import chalk from 'chalk'; 3 | import detectPort from 'detect-port'; 4 | 5 | (function CheckPortInUse() { 6 | const port: string = process.env.PORT || '1212'; 7 | 8 | detectPort(port, (err: ?Error, availablePort: number) => { 9 | if (port !== String(availablePort)) { 10 | throw new Error( 11 | chalk.whiteBright.bgRed.bold( 12 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn dev` 13 | ) 14 | ); 15 | } else { 16 | process.exit(0); 17 | } 18 | }); 19 | })(); 20 | -------------------------------------------------------------------------------- /app/actions/recorder.js: -------------------------------------------------------------------------------- 1 | export const RECORDER_START = 'RECORDER_START'; 2 | export const RECORDER_STOP = 'RECORDER_STOP'; 3 | export const RECORDER_PAUSE = 'RECORDER_PAUSE'; 4 | export const RECORDER_RESUME = 'RECORDER_RESUME'; 5 | 6 | export function recorderStart(filePath: string) { 7 | return { 8 | type: RECORDER_START, 9 | payload: { filePath } 10 | }; 11 | } 12 | 13 | export function recorderStop() { 14 | return { type: RECORDER_STOP }; 15 | } 16 | 17 | export function recorderPause() { 18 | return { type: RECORDER_PAUSE }; 19 | } 20 | 21 | export function recorderResume() { 22 | return { type: RECORDER_RESUME }; 23 | } 24 | -------------------------------------------------------------------------------- /app/containers/style.scss: -------------------------------------------------------------------------------- 1 | .app-layout { 2 | min-height: 100vh; 3 | height: 100%; 4 | background-color: #f0f2f5; 5 | 6 | .app-sider { 7 | .ant-menu-item { 8 | margin-top: 0 !important; 9 | margin-bottom: 0 !important; 10 | height: 48px; 11 | line-height: 48px; 12 | } 13 | } 14 | 15 | .app-header { 16 | background-color: #f0f2f5; 17 | box-shadow: 0 2px 8px #cec8c8; 18 | z-index: 1; 19 | height: 48px; 20 | line-height: 48px; 21 | padding: 0; 22 | } 23 | 24 | .app-content { 25 | background-color: #f0f2f5; 26 | } 27 | 28 | .app-footer { 29 | background-color: #f0f2f5; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2017 2 | 3 | platform: 4 | - x64 5 | 6 | environment: 7 | matrix: 8 | - nodejs_version: 10 9 | 10 | cache: 11 | - '%LOCALAPPDATA%/Yarn' 12 | - node_modules 13 | - app/node_modules 14 | - flow-typed 15 | - '%USERPROFILE%\.electron' 16 | 17 | matrix: 18 | fast_finish: true 19 | 20 | build: off 21 | 22 | version: '{build}' 23 | 24 | shallow_clone: true 25 | 26 | clone_depth: 1 27 | 28 | install: 29 | - ps: Install-Product node $env:nodejs_version x64 30 | - set CI=true 31 | - yarn 32 | 33 | test_script: 34 | - yarn package-ci 35 | - yarn lint 36 | # - yarn flow 37 | - yarn test 38 | - yarn build-e2e 39 | - yarn test-e2e 40 | -------------------------------------------------------------------------------- /app/components/RecordingPreview/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | import recorder from '../../utils/recorder'; 5 | 6 | import './style.scss'; 7 | 8 | type Props = {}; 9 | 10 | export default class RecordingPreview extends Component { 11 | props: Props; 12 | 13 | componentDidMount() { 14 | const dom = document.getElementById('recording-preview'); 15 | recorder.enablePreview(true); 16 | recorder.bind(dom); 17 | } 18 | 19 | componentWillUnmount() { 20 | recorder.enablePreview(false); 21 | recorder.unBind(); 22 | } 23 | 24 | render() { 25 | return
; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | import { createHashHistory } from 'history'; 5 | import { routerMiddleware } from 'connected-react-router'; 6 | import createRootReducer from '../reducers'; 7 | import type { StateType } from '../reducers/types'; 8 | 9 | const history = createHashHistory(); 10 | const rootReducer = createRootReducer(history); 11 | const router = routerMiddleware(history); 12 | const enhancer = applyMiddleware(thunk, router); 13 | 14 | function configureStore(initialState?: StateType) { 15 | return createStore<*, StateType, *>(rootReducer, initialState, enhancer); 16 | } 17 | 18 | export default { configureStore, history }; 19 | -------------------------------------------------------------------------------- /test/reducers/counter.spec.js: -------------------------------------------------------------------------------- 1 | import counter from '../../app/reducers/counter'; 2 | import { 3 | INCREMENT_COUNTER, 4 | DECREMENT_COUNTER 5 | } from '../../app/actions/counter'; 6 | 7 | describe('reducers', () => { 8 | describe('counter', () => { 9 | it('should handle initial state', () => { 10 | expect(counter(undefined, {})).toMatchSnapshot(); 11 | }); 12 | 13 | it('should handle INCREMENT_COUNTER', () => { 14 | expect(counter(1, { type: INCREMENT_COUNTER })).toMatchSnapshot(); 15 | }); 16 | 17 | it('should handle DECREMENT_COUNTER', () => { 18 | expect(counter(1, { type: DECREMENT_COUNTER })).toMatchSnapshot(); 19 | }); 20 | 21 | it('should handle unknown action type', () => { 22 | expect(counter(1, { type: 'unknown' })).toMatchSnapshot(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /app/actions/counter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GetState, Dispatch } from '../reducers/types'; 3 | 4 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; 5 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'; 6 | 7 | export function increment() { 8 | return { 9 | type: INCREMENT_COUNTER 10 | }; 11 | } 12 | 13 | export function decrement() { 14 | return { 15 | type: DECREMENT_COUNTER 16 | }; 17 | } 18 | 19 | export function incrementIfOdd() { 20 | return (dispatch: Dispatch, getState: GetState) => { 21 | const { counter } = getState(); 22 | 23 | if (counter % 2 === 0) { 24 | return; 25 | } 26 | 27 | dispatch(increment()); 28 | }; 29 | } 30 | 31 | export function incrementAsync(delay: number = 1000) { 32 | return (dispatch: Dispatch) => { 33 | setTimeout(() => { 34 | dispatch(increment()); 35 | }, delay); 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /internals/scripts/ElectronRebuild.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import path from 'path'; 3 | import { execSync } from 'child_process'; 4 | import fs from 'fs'; 5 | import { dependencies } from '../../app/package.json'; 6 | 7 | (() => { 8 | const nodeModulesPath = path.join( 9 | __dirname, 10 | '..', 11 | '..', 12 | 'app', 13 | 'node_modules' 14 | ); 15 | 16 | if ( 17 | Object.keys(dependencies || {}).length > 0 && 18 | fs.existsSync(nodeModulesPath) 19 | ) { 20 | const electronRebuildCmd = 21 | '../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .'; 22 | const cmd = 23 | process.platform === 'win32' 24 | ? electronRebuildCmd.replace(/\//g, '\\') 25 | : electronRebuildCmd; 26 | execSync(cmd, { 27 | cwd: path.join(__dirname, '..', '..', 'app'), 28 | stdio: 'inherit' 29 | }); 30 | } 31 | })(); 32 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-recorder", 3 | "productName": "EasyRecorder", 4 | "version": "0.18.1", 5 | "description": "EasyRecorder based on React, React Router, Webpack, React Hot Loader for rapid application development", 6 | "main": "./main.prod.js", 7 | "author": { 8 | "name": "Sylar", 9 | "email": "peilinok@gmail.com", 10 | "url": "https://github.com/EasyRecorder" 11 | }, 12 | "scripts": { 13 | "electron-rebuild": "node -r ../internals/scripts/BabelRegister.js ../internals/scripts/ElectronRebuild.js", 14 | "postinstall": "yarn electron-rebuild" 15 | }, 16 | "license": "MIT", 17 | "dependencies": { 18 | "ffmpeg-recorder": "1.4.15", 19 | "platform-folders": "^0.4.1" 20 | }, 21 | "ffmpeg_recorder": { 22 | "electron_version": "7.1.4", 23 | "platform": "win32", 24 | "runtime": "electron", 25 | "msvs_version": "2015", 26 | "debug": false, 27 | "silent": false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internals/scripts/CheckBuildsExist.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( 10 | __dirname, 11 | '..', 12 | '..', 13 | 'app', 14 | 'dist', 15 | 'renderer.prod.js' 16 | ); 17 | 18 | if (!fs.existsSync(mainPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The main process is not built yet. Build it by running "yarn build-main"' 22 | ) 23 | ); 24 | } 25 | 26 | if (!fs.existsSync(rendererPath)) { 27 | throw new Error( 28 | chalk.whiteBright.bgRed.bold( 29 | 'The renderer process is not built yet. Build it by running "yarn build-renderer"' 30 | ) 31 | ); 32 | } 33 | } 34 | 35 | CheckBuildsExist(); 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".babelrc": "jsonc", 4 | ".eslintrc": "jsonc", 5 | ".prettierrc": "jsonc", 6 | 7 | ".stylelintrc": "json", 8 | 9 | ".dockerignore": "ignore", 10 | ".eslintignore": "ignore", 11 | ".flowconfig": "ignore" 12 | }, 13 | 14 | "javascript.validate.enable": false, 15 | "javascript.format.enable": false, 16 | "typescript.validate.enable": false, 17 | "typescript.format.enable": false, 18 | 19 | "flow.useNPMPackagedFlow": true, 20 | "search.exclude": { 21 | ".git": true, 22 | ".eslintcache": true, 23 | "app/dist": true, 24 | "app/main.prod.js": true, 25 | "app/main.prod.js.map": true, 26 | "bower_components": true, 27 | "dll": true, 28 | "flow-typed": true, 29 | "release": true, 30 | "node_modules": true, 31 | "npm-debug.log.*": true, 32 | "test/**/__snapshots__": true, 33 | "yarn.lock": true 34 | }, 35 | "editor.codeActionsOnSave": { 36 | "source.fixAll": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/reducers/recorder.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import moment from 'moment'; 3 | 4 | import { 5 | RECORDER_START, 6 | RECORDER_STOP, 7 | RECORDER_PAUSE, 8 | RECORDER_RESUME 9 | } from '../actions/recorder'; 10 | import type { Action, RecorderStateType } from './types'; 11 | 12 | export default function recorder( 13 | state: RecorderStateType = { 14 | isPaused: false, 15 | isRecording: false, 16 | curFilePath: '', 17 | startTime: 0 18 | }, 19 | action: Action 20 | ) { 21 | switch (action.type) { 22 | case RECORDER_START: 23 | return { 24 | ...state, 25 | isRecording: true, 26 | curFile: action.payload.filePath, 27 | startTime: moment() 28 | }; 29 | case RECORDER_STOP: 30 | return { ...state, isRecording: false, curFile: '', startTime: 0 }; 31 | case RECORDER_PAUSE: 32 | return { ...state, isPaused: true }; 33 | case RECORDER_RESUME: 34 | return { ...state, isPaused: false }; 35 | default: 36 | return state; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /app/main.prod.js 3 | /app/main.prod.js.map 4 | /app/dist/.* 5 | /resources/.* 6 | /node_modules/webpack-cli 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 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 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 | 30 | # OSX 31 | .DS_Store 32 | 33 | # flow-typed 34 | flow-typed/npm/* 35 | !flow-typed/npm/module_vx.x.x.js 36 | 37 | # App packaged 38 | release 39 | app/main.prod.js 40 | app/main.prod.js.map 41 | app/renderer.prod.js 42 | app/renderer.prod.js.map 43 | app/style.css 44 | app/style.css.map 45 | dist 46 | dll 47 | main.js 48 | main.js.map 49 | 50 | .idea 51 | npm-debug.log.* 52 | .*.dockerfile -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer as ReactHotAppContainer } from 'react-hot-loader'; 4 | import { Provider } from 'react-redux'; 5 | import { ConnectedRouter } from 'connected-react-router'; 6 | import { hot } from 'react-hot-loader/root'; 7 | import type { Store } from './reducers/types'; 8 | import { configuredStore, configuredHistory } from './store/configureStore'; 9 | import Routes from './routes'; 10 | import './utils'; 11 | 12 | import './app.global.css'; 13 | 14 | const AppContainer = process.env.PLAIN_HMR ? Fragment : ReactHotAppContainer; 15 | 16 | type Props = { 17 | store: Store, 18 | history: {} 19 | }; 20 | 21 | const Root = hot(({ store, history }: Props) => ( 22 | 23 | 24 | 25 | 26 | 27 | )); 28 | 29 | render( 30 | 31 | 32 | , 33 | document.getElementById('root') 34 | ); 35 | -------------------------------------------------------------------------------- /.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 | 30 | # OSX 31 | .DS_Store 32 | 33 | # flow-typed 34 | flow-typed/npm/* 35 | !flow-typed/npm/module_vx.x.x.js 36 | 37 | # App packaged 38 | release 39 | app/main.prod.js 40 | app/main.prod.js.map 41 | app/renderer.prod.js 42 | app/renderer.prod.js.map 43 | app/style.css 44 | app/style.css.map 45 | dist 46 | dll 47 | main.js 48 | main.js.map 49 | 50 | .idea 51 | npm-debug.log.* 52 | /yarn.lock 53 | /app/build/config.gypi 54 | /app/yarn.lock 55 | -------------------------------------------------------------------------------- /.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 | 30 | # OSX 31 | .DS_Store 32 | 33 | # flow-typed 34 | flow-typed/npm/* 35 | !flow-typed/npm/module_vx.x.x.js 36 | 37 | # App packaged 38 | release 39 | app/main.prod.js 40 | app/main.prod.js.map 41 | app/renderer.prod.js 42 | app/renderer.prod.js.map 43 | app/style.css 44 | app/style.css.map 45 | dist 46 | dll 47 | main.js 48 | main.js.map 49 | 50 | .idea 51 | npm-debug.log.* 52 | __snapshots__ 53 | 54 | # Package.json 55 | package.json 56 | .travis.yml 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EasyRecorder 2 | 3 |
4 | 5 |

6 | EasyRecorder is a cross-platform screen recorder which is written by node-js and depended on ffmpeg-recorder. 7 |

8 | 9 |
10 | 11 | ## ScreenShots 12 | 13 | ![](screenshots/recording.png) 14 | 15 | ![](screenshots/settings.png) 16 | 17 | ## Features 18 | 19 |
20 | 21 | - Different ways to record screen 、speaker and microphone 22 | - Encode screen data from rgb to h264 23 | - Encode audio data from pcm to aac 24 | - Mux h264 and aac to a single mp4 file 25 | - Only support in windows for now(at least win7) 26 | - Image preview(yuv420) 27 | - Record only mic or speaker,or only video without any audio 28 | 29 |
30 | 31 | ## Todo 32 | 33 |
34 | 35 | - More muxers such as .mkv 36 | - Screen grabber for mac and linux systems 37 | - Audio grabber for mac and linux systems 38 | 39 |
40 | 41 | ## Usage 42 | 43 |
44 | 45 | ### Install depends 46 | 47 | ```sh 48 | 49 | $ yarn 50 | 51 | ``` 52 | 53 | ### Run in dev 54 | 55 | ```sh 56 | 57 | $ yarn dev 58 | 59 | ``` 60 | 61 | ### Package 62 | 63 | ```sh 64 | 65 | $ yarn package 66 | 67 | ``` 68 | -------------------------------------------------------------------------------- /app/containers/Content/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/state-in-constructor */ 2 | // @flow 3 | import * as React from 'react'; 4 | import { Route, Switch, Redirect } from 'react-router-dom'; 5 | 6 | import { ContentComponent } from '../../utils/types'; 7 | 8 | type Props = { 9 | contents: Array, 10 | defaultContent: ContentComponent 11 | }; 12 | 13 | export default class ContentLayout extends React.Component { 14 | props: Props; 15 | 16 | render() { 17 | const { contents, defaultContent } = this.props; 18 | return ( 19 | 20 | {contents.map(content => { 21 | let route = null; 22 | if (content.subItems !== undefined && content.subItems.length > 0) 23 | route = <>; 24 | else 25 | route = ( 26 | 31 | ); 32 | 33 | return route; 34 | })} 35 | 36 | 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /configs/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 | 9 | export default { 10 | externals: [...Object.keys(externals || {})], 11 | 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.jsx?$/, 16 | exclude: /node_modules/, 17 | use: { 18 | loader: 'babel-loader', 19 | options: { 20 | cacheDirectory: true 21 | } 22 | } 23 | } 24 | ] 25 | }, 26 | 27 | output: { 28 | path: path.join(__dirname, '..', 'app'), 29 | // https://github.com/webpack/webpack/issues/1114 30 | libraryTarget: 'commonjs2' 31 | }, 32 | 33 | /** 34 | * Determine the array of extensions that should be used to resolve modules. 35 | */ 36 | resolve: { 37 | extensions: ['.js', '.jsx', '.json'], 38 | modules: [path.join(__dirname, '..', 'app'), 'node_modules'] 39 | }, 40 | 41 | plugins: [ 42 | new webpack.EnvironmentPlugin({ 43 | NODE_ENV: 'production' 44 | }), 45 | 46 | new webpack.NamedModulesPlugin() 47 | ] 48 | }; 49 | -------------------------------------------------------------------------------- /app/components/CustomButton/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import { Button, Icon } from 'antd'; 4 | 5 | import './style.scss'; 6 | 7 | type Props = { 8 | isIconButton: boolean, 9 | title: string, 10 | visible: boolean, 11 | loading: boolean, 12 | icon: string, 13 | type: string, 14 | style: object, 15 | className: string, 16 | onClick: func 17 | }; 18 | 19 | export default class CustomButton extends Component { 20 | props: Props; 21 | 22 | render() { 23 | const { 24 | isIconButton, 25 | title, 26 | visible, 27 | icon, 28 | type, 29 | style, 30 | className, 31 | onClick, 32 | loading 33 | } = this.props; 34 | 35 | if (visible === false) return <>; 36 | 37 | return isIconButton === true ? ( 38 | 45 | ) : ( 46 | 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/components/DeviceSelect/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | import { Select } from 'antd'; 5 | 6 | import { DeviceItem } from '../../utils/types'; 7 | 8 | import './style.scss'; 9 | 10 | type Props = { 11 | options: Array, 12 | selected?: DeviceItem, 13 | disabled: boolean, 14 | onSelect: DeviceItem => void 15 | }; 16 | 17 | export default class DeviceSelect extends Component { 18 | props: Props; 19 | 20 | static defaultProps = { 21 | selected: {} 22 | }; 23 | 24 | constructor(props) { 25 | super(props); 26 | 27 | this.handleSelect = this.handleSelect.bind(this); 28 | } 29 | 30 | handleSelect(value, options) { 31 | const { onSelect } = this.props; 32 | 33 | if (onSelect) onSelect(options.props.data); 34 | } 35 | 36 | render() { 37 | const { options, selected, disabled } = this.props; 38 | 39 | return ( 40 | 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/utils/notification.js: -------------------------------------------------------------------------------- 1 | import { notification, message } from 'antd'; 2 | 3 | notification.config({ 4 | duration: 3, 5 | placement: 'topLeft', 6 | top: 10 7 | }); 8 | 9 | message.config({ 10 | duration: 3, 11 | maxCount: 3, 12 | top: 10 13 | }); 14 | 15 | export function notifyInfo(content) { 16 | notification.info({ 17 | key: 'easy-recorder-notifiy-info', 18 | description: 'EasyRecorder', 19 | message: content 20 | }); 21 | } 22 | 23 | export function notifyWarn(content) { 24 | notification.warn({ 25 | key: 'easy-recorder-notifiy-warn', 26 | description: 'EasyRecorder', 27 | message: content 28 | }); 29 | } 30 | 31 | export function notifySuccess(content) { 32 | notification.success({ 33 | key: 'easy-recorder-notifiy-success', 34 | description: 'EasyRecorder', 35 | message: content 36 | }); 37 | } 38 | 39 | export function notifyError(content) { 40 | notification.error({ 41 | key: 'easy-recorder-notifiy-error', 42 | description: 'EasyRecorder', 43 | message: content 44 | }); 45 | } 46 | 47 | export function msgInfo(content) { 48 | message.info(content); 49 | } 50 | 51 | export function msgWarn(content) { 52 | message.warn(content); 53 | } 54 | 55 | export function msgSuccess(content) { 56 | message.success(content); 57 | } 58 | 59 | export function msgError(content) { 60 | message.error(content); 61 | } 62 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EasyRecorder 6 | 17 | 18 | 19 |
20 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /test/components/__snapshots__/Counter.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Counter component should match exact snapshot 1`] = ` 4 |
5 |
6 |
10 | 14 | 17 | 18 |
19 |
23 | 1 24 |
25 |
28 | 38 | 48 | 56 | 64 |
65 |
66 |
67 | `; 68 | -------------------------------------------------------------------------------- /test/actions/counter.spec.js: -------------------------------------------------------------------------------- 1 | import { spy } from 'sinon'; 2 | import * as actions from '../../app/actions/counter'; 3 | 4 | describe('actions', () => { 5 | it('should increment should create increment action', () => { 6 | expect(actions.increment()).toMatchSnapshot(); 7 | }); 8 | 9 | it('should decrement should create decrement action', () => { 10 | expect(actions.decrement()).toMatchSnapshot(); 11 | }); 12 | 13 | it('should incrementIfOdd should create increment action', () => { 14 | const fn = actions.incrementIfOdd(); 15 | expect(fn).toBeInstanceOf(Function); 16 | const dispatch = spy(); 17 | const getState = () => ({ counter: 1 }); 18 | fn(dispatch, getState); 19 | expect(dispatch.calledWith({ type: actions.INCREMENT_COUNTER })).toBe(true); 20 | }); 21 | 22 | it('should incrementIfOdd shouldnt create increment action if counter is even', () => { 23 | const fn = actions.incrementIfOdd(); 24 | const dispatch = spy(); 25 | const getState = () => ({ counter: 2 }); 26 | fn(dispatch, getState); 27 | expect(dispatch.called).toBe(false); 28 | }); 29 | 30 | // There's no nice way to test this at the moment... 31 | it('should incrementAsync', done => { 32 | const fn = actions.incrementAsync(1); 33 | expect(fn).toBeInstanceOf(Function); 34 | const dispatch = spy(); 35 | fn(dispatch); 36 | setTimeout(() => { 37 | expect(dispatch.calledWith({ type: actions.INCREMENT_COUNTER })).toBe( 38 | true 39 | ); 40 | done(); 41 | }, 5); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /app/utils/helper.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import moment from 'moment'; 3 | import path from 'path'; 4 | 5 | import { DeviceItem, EncoderItem } from './types'; 6 | 7 | moment.locale('zh-cn'); 8 | 9 | const helper = { 10 | getDefaultSelectedDevice: (devices: Array, id?: string) => { 11 | if (devices === undefined || devices.length === 0) return undefined; 12 | 13 | let device; 14 | for (let index = 0; index < devices.length; index += 1) { 15 | // no stored device id,return default one 16 | if ((id === undefined || id === '') && devices[index].isDefault === 1) { 17 | device = devices[index]; 18 | break; 19 | } 20 | 21 | // find device by specified device id 22 | if (devices[index].id === id) { 23 | device = devices[index]; 24 | break; 25 | } 26 | } 27 | 28 | // return found device or undefined 29 | return device; 30 | }, 31 | getDefaultSelectedEncoder: (encoders: Array, id: number) => { 32 | if (encoders === undefined || encoders.length === 0) return undefined; 33 | 34 | for (let index = 0; index < encoders.length; index += 1) { 35 | if (encoders[index].id === id) return encoders[index]; 36 | } 37 | 38 | if (encoders && encoders.length > 0) return encoders[0]; 39 | 40 | return undefined; 41 | }, 42 | generateVideoFileName(folder: string, ext: string) { 43 | return path.join( 44 | folder, 45 | // eslint-disable-next-line new-cap 46 | `${new moment().format('YYYY-MM-DD-hhmmss')}.${ext}` 47 | ); 48 | } 49 | }; 50 | 51 | export default helper; 52 | -------------------------------------------------------------------------------- /app/app.global.css: -------------------------------------------------------------------------------- 1 | /* 2 | * @NOTE: Prepend a `~` to css file paths that are in your node_modules 3 | * See https://github.com/webpack-contrib/sass-loader#imports 4 | */ 5 | @import '~@fortawesome/fontawesome-free/css/all.css'; 6 | @import '~antd/dist/antd.css'; 7 | 8 | body { 9 | position: relative; 10 | color: white; 11 | height: 100vh; 12 | background-color: #232c39; 13 | background-image: linear-gradient( 14 | 45deg, 15 | rgba(0, 216, 255, 0.5) 10%, 16 | rgba(0, 1, 127, 0.7) 17 | ); 18 | font-family: Arial, Helvetica, Helvetica Neue, serif; 19 | overflow-y: hidden; 20 | } 21 | 22 | #root { 23 | height: 100%; 24 | } 25 | 26 | h2 { 27 | margin: 0; 28 | font-size: 2.25rem; 29 | font-weight: bold; 30 | letter-spacing: -0.025em; 31 | color: #fff; 32 | } 33 | 34 | p { 35 | font-size: 24px; 36 | } 37 | 38 | li { 39 | list-style: none; 40 | } 41 | 42 | a { 43 | color: white; 44 | opacity: 0.75; 45 | text-decoration: none; 46 | } 47 | 48 | a:hover { 49 | opacity: 1; 50 | text-decoration: none; 51 | cursor: pointer; 52 | } 53 | 54 | *::-webkit-scrollbar { 55 | /* 滚动条整体样式 */ 56 | width: 5px; 57 | 58 | /* 高宽分别对应横竖滚动条的尺寸 */ 59 | height: 1px; 60 | } 61 | 62 | *::-webkit-scrollbar-thumb { 63 | /* 滚动条里面小方块 */ 64 | border-radius: 10px; 65 | background-color: rgba(172, 15, 15, 0.795); 66 | background-image: -webkit-linear-gradient( 67 | 45deg, 68 | rgba(255, 255, 255, 0.1), 69 | transparent 25%, 70 | transparent 50%, 71 | rgba(255, 255, 255, 0.1), 72 | rgba(255, 255, 255, 0.1), 73 | transparent 75%, 74 | transparent 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /app/utils/storage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import ElectronStore from 'electron-store'; 3 | 4 | const STORAGE_KEY_MIC = 'STORAGE_KEY_MIC'; 5 | const STORAGE_KEY_SPEAKER = 'STORAGE_KEY_SPEAKER'; 6 | const STORAGE_KEY_FPS = 'STORAGE_KEY_FPS'; 7 | const STORAGE_KEY_QUALITY = 'STORAGE_KEY_QUALITY'; 8 | const STORAGE_KEY_OUTPUT_DIR = 'STORAGE_KEY_OUTPUT_DIR'; 9 | 10 | const STORAGE_KEY_ENABLE_MIC = 'STORAGE_KEY_ENABLE_MIC'; 11 | const STORAGE_KEY_ENABLE_SPEAKER = 'STORAGE_KEY_ENABLE_SPEAKER'; 12 | 13 | const STORAGE_KEY_ENCODER_VIDEO = 'STORAGE_KEY_ENCODER_VIDEO'; 14 | 15 | const store = new ElectronStore(); 16 | 17 | const read = (key, defaultValue) => store.get(key, defaultValue); 18 | const write = (key, value) => store.set(key, value); 19 | 20 | const storage = { 21 | getMic: () => read(STORAGE_KEY_MIC, ''), 22 | setMic: (value: string) => write(STORAGE_KEY_MIC, value), 23 | 24 | getSpeaker: () => read(STORAGE_KEY_SPEAKER, ''), 25 | setSpeaker: (value: string) => write(STORAGE_KEY_SPEAKER, value), 26 | 27 | getFps: () => read(STORAGE_KEY_FPS, 20), 28 | setFps: (value: number) => write(STORAGE_KEY_FPS, value), 29 | 30 | getQuality: () => read(STORAGE_KEY_QUALITY, 60), 31 | setQuality: (value: number) => write(STORAGE_KEY_QUALITY, value), 32 | 33 | getOutputDir: () => read(STORAGE_KEY_OUTPUT_DIR, ''), 34 | setOutputDir: (value: string) => write(STORAGE_KEY_OUTPUT_DIR, value), 35 | 36 | isMicEnabled: () => read(STORAGE_KEY_ENABLE_MIC, true), 37 | enableMic: (value: boolean) => write(STORAGE_KEY_ENABLE_MIC, value), 38 | 39 | isSpeakerEnabled: () => read(STORAGE_KEY_ENABLE_SPEAKER, true), 40 | enableSpeaker: (value: boolean) => write(STORAGE_KEY_ENABLE_SPEAKER, value), 41 | 42 | getVideoEncoder: () => read(STORAGE_KEY_ENCODER_VIDEO, 0), 43 | setVideoEncoder: (value: number) => write(STORAGE_KEY_ENCODER_VIDEO, value) 44 | }; 45 | 46 | // store.openInEditor(); 47 | 48 | export default storage; 49 | -------------------------------------------------------------------------------- /app/containers/Sider/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { Menu, Icon } from 'antd'; 4 | import { NavLink } from 'react-router-dom'; 5 | 6 | import { ContentComponent } from '../../utils/types'; 7 | 8 | type Props = { 9 | contents: Array, 10 | defaultOpen: ContentComponent, 11 | defaultSelect: ContentComponent 12 | }; 13 | 14 | export default class SiderLayout extends Component { 15 | props: Props; 16 | 17 | render() { 18 | const { contents, defaultOpen, defaultSelect } = this.props; 19 | return ( 20 | 28 | {contents.map(content => { 29 | if (content.subItems.length > 0) { 30 | return ( 31 | 35 | 36 | {content.title} 37 | 38 | } 39 | > 40 | {content.subItems.map(subItem => ( 41 | 42 | 43 | 44 | {subItem.title} 45 | 46 | 47 | ))} 48 | 49 | ); 50 | } 51 | return ( 52 | 53 | 54 | 55 | {content.title} 56 | 57 | 58 | ); 59 | })} 60 | 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/containers/Content/Recording/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { shell } from 'electron'; 3 | import { connect } from 'react-redux'; 4 | 5 | import recorder from '../../../utils/recorder'; 6 | import storage from '../../../utils/storage'; 7 | 8 | import RecordingBar from '../../../components/RecordingBar'; 9 | import RecordingPreview from '../../../components/RecordingPreview'; 10 | 11 | import './style.scss'; 12 | 13 | type Props = { 14 | isRecording: boolean, 15 | isPaused: boolean, 16 | startTime: number 17 | }; 18 | 19 | class RecordingLayout extends Component { 20 | props: Props; 21 | 22 | render() { 23 | const { isRecording, isPaused, startTime } = this.props; 24 | return ( 25 |
26 | { 32 | switch (action) { 33 | case 'start': 34 | recorder.start(); 35 | recorder.enablePreview(true); 36 | break; 37 | case 'stop': 38 | recorder.stop(); 39 | break; 40 | case 'pause': 41 | recorder.pause(); 42 | break; 43 | case 'resume': 44 | recorder.resume(); 45 | break; 46 | default: 47 | break; 48 | } 49 | }} 50 | onFolderClick={() => { 51 | shell.showItemInFolder(`${storage.getOutputDir()}\\abc.mp4`); 52 | }} 53 | /> 54 | 55 |
56 | ); 57 | } 58 | } 59 | 60 | function mapStateToProps(state) { 61 | return { 62 | isRecording: state.recorder.isRecording, 63 | isPaused: state.recorder.isPaused, 64 | startTime: state.recorder.startTime 65 | }; 66 | } 67 | 68 | export default connect(mapStateToProps)(RecordingLayout); 69 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.dev.dll.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, import/no-dynamic-require: off */ 2 | 3 | /** 4 | * Builds the DLL for development electron renderer process 5 | */ 6 | 7 | import webpack from 'webpack'; 8 | import path from 'path'; 9 | import merge from 'webpack-merge'; 10 | import baseConfig from './webpack.config.base'; 11 | import { dependencies } from '../package.json'; 12 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 13 | 14 | CheckNodeEnv('development'); 15 | 16 | const dist = path.join(__dirname, '..', 'dll'); 17 | 18 | export default merge.smart(baseConfig, { 19 | context: path.join(__dirname, '..'), 20 | 21 | devtool: 'eval', 22 | 23 | mode: 'development', 24 | 25 | target: 'electron-renderer', 26 | 27 | externals: ['fsevents', 'crypto-browserify'], 28 | 29 | /** 30 | * Use `module` from `webpack.config.renderer.dev.js` 31 | */ 32 | module: require('./webpack.config.renderer.dev.babel').default.module, 33 | 34 | entry: { 35 | renderer: Object.keys(dependencies || {}) 36 | }, 37 | 38 | output: { 39 | library: 'renderer', 40 | path: dist, 41 | filename: '[name].dev.dll.js', 42 | libraryTarget: 'var' 43 | }, 44 | 45 | plugins: [ 46 | new webpack.DllPlugin({ 47 | path: path.join(dist, '[name].json'), 48 | name: '[name]' 49 | }), 50 | 51 | /** 52 | * Create global constants which can be configured at compile time. 53 | * 54 | * Useful for allowing different behaviour between development builds and 55 | * release builds 56 | * 57 | * NODE_ENV should be production so that modules do not perform certain 58 | * development checks 59 | */ 60 | new webpack.EnvironmentPlugin({ 61 | NODE_ENV: 'development' 62 | }), 63 | 64 | new webpack.LoaderOptionsPlugin({ 65 | debug: true, 66 | options: { 67 | context: path.join(__dirname, '..', 'app'), 68 | output: { 69 | path: path.join(__dirname, '..', 'dll') 70 | } 71 | } 72 | }) 73 | ] 74 | }); 75 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - os: osx 4 | language: node_js 5 | node_js: 6 | - node 7 | env: 8 | - ELECTRON_CACHE=$HOME/.cache/electron 9 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder 10 | - os: linux 11 | language: node_js 12 | node_js: 13 | - node 14 | services: 15 | - xvfb 16 | addons: 17 | apt: 18 | sources: 19 | - sourceline: 'ppa:ubuntu-toolchain-r/test' 20 | packages: 21 | - gcc-multilib 22 | - g++-8 23 | - g++-multilib 24 | - icnsutils 25 | - graphicsmagick 26 | - xz-utils 27 | - xorriso 28 | - rpm 29 | 30 | before_cache: 31 | - rm -rf $HOME/.cache/electron-builder/wine 32 | 33 | cache: 34 | yarn: true 35 | directories: 36 | - node_modules 37 | - $(npm config get prefix)/lib/node_modules 38 | - flow-typed 39 | - $HOME/.cache/electron 40 | - $HOME/.cache/electron-builder 41 | 42 | before_install: 43 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX="g++-8"; fi 44 | 45 | install: 46 | - yarn --ignore-engines 47 | # On Linux, initialize "virtual display". See before_script 48 | - | 49 | if [ "$TRAVIS_OS_NAME" == "linux" ]; then 50 | /sbin/start-stop-daemon \ 51 | --start \ 52 | --quiet \ 53 | --pidfile /tmp/custom_xvfb_99.pid \ 54 | --make-pidfile \ 55 | --background \ 56 | --exec /usr/bin/Xvfb \ 57 | -- :99 -ac -screen 0 1280x1024x16 58 | else 59 | : 60 | fi 61 | 62 | before_script: 63 | # On Linux, create a "virtual display". This allows browsers to work properly 64 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export DISPLAY=:99.0; fi 65 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sleep 3; fi 66 | 67 | script: 68 | - yarn package-ci 69 | - yarn lint 70 | - yarn flow 71 | # HACK: Temporarily ignore `yarn test` on linux 72 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then yarn test; fi 73 | - yarn build-e2e 74 | - yarn test-e2e 75 | -------------------------------------------------------------------------------- /app/containers/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/state-in-constructor */ 2 | // @flow 3 | import React, { Component } from 'react'; 4 | import { Layout } from 'antd'; 5 | 6 | import SiderLayout from './Sider'; 7 | import HeaderLayout from './Header'; 8 | import ContentLayout from './Content'; 9 | import FooterLayout from './Footer'; 10 | 11 | import contents from './contents'; 12 | import './style.scss'; 13 | 14 | const { Header, Footer, Sider, Content } = Layout; 15 | 16 | type Props = { 17 | location: { 18 | pathname: string 19 | }, 20 | match: { 21 | path: string 22 | } 23 | }; 24 | 25 | type State = { 26 | collapsed: boolean 27 | }; 28 | 29 | export default class AppLayout extends Component { 30 | props: Props; 31 | 32 | state: State; 33 | 34 | state = { 35 | collapsed: true 36 | }; 37 | 38 | toggleCollapsed = () => { 39 | const { collapsed } = this.state; 40 | 41 | this.setState({ 42 | collapsed: !collapsed 43 | }); 44 | }; 45 | 46 | render() { 47 | const { collapsed } = this.state; 48 | const { match, location } = this.props; 49 | return ( 50 | 51 | 58 | 63 | 64 | 65 |
66 | 69 |
70 | 71 | 72 | 73 |
74 | 75 |
76 |
77 |
78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/containers/CounterPage.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Enzyme, { mount } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import { Provider } from 'react-redux'; 5 | import { createBrowserHistory } from 'history'; 6 | import { ConnectedRouter } from 'connected-react-router'; 7 | import CounterPage from '../../app/containers/CounterPage'; 8 | import { configureStore } from '../../app/store/configureStore'; 9 | 10 | Enzyme.configure({ adapter: new Adapter() }); 11 | 12 | function setup(initialState) { 13 | const store = configureStore(initialState); 14 | const history = createBrowserHistory(); 15 | const provider = ( 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | const app = mount(provider); 23 | return { 24 | app, 25 | buttons: app.find('button'), 26 | p: app.find('.counter') 27 | }; 28 | } 29 | 30 | describe('containers', () => { 31 | describe('App', () => { 32 | it('should display initial count', () => { 33 | const { p } = setup(); 34 | expect(p.text()).toMatch(/^0$/); 35 | }); 36 | 37 | it('should display updated count after increment button click', () => { 38 | const { buttons, p } = setup(); 39 | buttons.at(0).simulate('click'); 40 | expect(p.text()).toMatch(/^1$/); 41 | }); 42 | 43 | it('should display updated count after decrement button click', () => { 44 | const { buttons, p } = setup(); 45 | buttons.at(1).simulate('click'); 46 | expect(p.text()).toMatch(/^-1$/); 47 | }); 48 | 49 | it('shouldnt change if even and if odd button clicked', () => { 50 | const { buttons, p } = setup(); 51 | buttons.at(2).simulate('click'); 52 | expect(p.text()).toMatch(/^0$/); 53 | }); 54 | 55 | it('should change if odd and if odd button clicked', () => { 56 | const { buttons, p } = setup({ counter: 1 }); 57 | buttons.at(2).simulate('click'); 58 | expect(p.text()).toMatch(/^2$/); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /configs/webpack.config.main.prod.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import merge from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 12 | 13 | CheckNodeEnv('production'); 14 | 15 | export default merge.smart(baseConfig, { 16 | devtool: 'source-map', 17 | 18 | mode: 'production', 19 | 20 | target: 'electron-main', 21 | 22 | entry: './app/main.dev', 23 | 24 | output: { 25 | path: path.join(__dirname, '..'), 26 | filename: './app/main.prod.js' 27 | }, 28 | 29 | optimization: { 30 | minimizer: process.env.E2E_BUILD 31 | ? [] 32 | : [ 33 | new TerserPlugin({ 34 | parallel: true, 35 | sourceMap: true, 36 | cache: true 37 | }) 38 | ] 39 | }, 40 | 41 | plugins: [ 42 | new BundleAnalyzerPlugin({ 43 | analyzerMode: 44 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 45 | openAnalyzer: process.env.OPEN_ANALYZER === 'true' 46 | }), 47 | 48 | /** 49 | * Create global constants which can be configured at compile time. 50 | * 51 | * Useful for allowing different behaviour between development builds and 52 | * release builds 53 | * 54 | * NODE_ENV should be production so that modules do not perform certain 55 | * development checks 56 | */ 57 | new webpack.EnvironmentPlugin({ 58 | NODE_ENV: 'production', 59 | DEBUG_PROD: false, 60 | START_MINIMIZED: false 61 | }) 62 | ], 63 | 64 | /** 65 | * Disables webpack processing of __dirname and __filename. 66 | * If you run the bundle in node.js it falls back to these values of node.js. 67 | * https://github.com/webpack/webpack/issues/2010 68 | */ 69 | node: { 70 | __dirname: false, 71 | __filename: false 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /internals/scripts/CheckNativeDep.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fs from 'fs'; 3 | import chalk from 'chalk'; 4 | import { execSync } from 'child_process'; 5 | import { dependencies } from '../../package.json'; 6 | 7 | (() => { 8 | if (!dependencies) return; 9 | const dependenciesKeys = Object.keys(dependencies); 10 | const nativeDeps = fs 11 | .readdirSync('node_modules') 12 | .filter(folder => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 13 | try { 14 | // Find the reason for why the dependency is installed. If it is installed 15 | // because of a devDependency then that is okay. Warn when it is installed 16 | // because of a dependency 17 | const { dependencies: dependenciesObject } = JSON.parse( 18 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString() 19 | ); 20 | const rootDependencies = Object.keys(dependenciesObject); 21 | const filteredRootDependencies = rootDependencies.filter(rootDependency => 22 | dependenciesKeys.includes(rootDependency) 23 | ); 24 | if (filteredRootDependencies.length > 0) { 25 | const plural = filteredRootDependencies.length > 1; 26 | console.log(` 27 | ${chalk.whiteBright.bgYellow.bold( 28 | 'Webpack does not work with native dependencies.' 29 | )} 30 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 31 | plural ? 'are native dependencies' : 'is a native dependency' 32 | } and should be installed inside of the "./app" folder. 33 | First uninstall the packages from "./package.json": 34 | ${chalk.whiteBright.bgGreen.bold('yarn remove your-package')} 35 | ${chalk.bold( 36 | 'Then, instead of installing the package to the root "./package.json":' 37 | )} 38 | ${chalk.whiteBright.bgRed.bold('yarn add your-package')} 39 | ${chalk.bold('Install the package to "./app/package.json"')} 40 | ${chalk.whiteBright.bgGreen.bold('cd ./app && yarn add your-package')} 41 | Read more about native dependencies at: 42 | ${chalk.bold( 43 | 'https://github.com/electron-react-boilerplate/electron-react-boilerplate/wiki/Module-Structure----Two-package.json-Structure' 44 | )} 45 | `); 46 | process.exit(1); 47 | } 48 | } catch (e) { 49 | console.log('Native dependencies could not be checked'); 50 | } 51 | })(); 52 | -------------------------------------------------------------------------------- /app/utils/index.js: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import { getDesktopFolder } from 'platform-folders'; 3 | import moment from 'moment'; 4 | 5 | import storage from './storage'; 6 | import recorder from './recorder'; 7 | import helper from './helper'; 8 | 9 | moment.locale('zh-CN'); 10 | 11 | const initLog = () => { 12 | log.transports.console = message => { 13 | const date = new Date(); 14 | switch (message.level) { 15 | case 'debug': 16 | console.debug(date.toLocaleTimeString(), ...message.data); 17 | break; 18 | case 'info': 19 | console.info(date.toLocaleTimeString(), ...message.data); 20 | break; 21 | case 'error': 22 | console.error(date.toLocaleTimeString(), ...message.data); 23 | break; 24 | case 'warn': 25 | console.warn(date.toLocaleTimeString(), ...message.data); 26 | break; 27 | default: 28 | console.log(date.toLocaleTimeString(), ...message.data); 29 | break; 30 | } 31 | }; 32 | }; 33 | 34 | const initSettings = () => { 35 | const { mics, speakers } = recorder.getDevices(); 36 | const vEncoders = recorder.getVideoEncoders(); 37 | 38 | const vEncoderId = storage.getVideoEncoder(); 39 | const mic = storage.getMic(); 40 | const speaker = storage.getSpeaker(); 41 | const fps = storage.getFps(); 42 | const quality = storage.getQuality(); 43 | const output = storage.getOutputDir(); 44 | 45 | const selectedMic = helper.getDefaultSelectedDevice(mics, mic); 46 | const selectedSpeaker = helper.getDefaultSelectedDevice(speakers, speaker); 47 | const selectedVEncoder = helper.getDefaultSelectedEncoder( 48 | vEncoders, 49 | vEncoderId 50 | ); 51 | 52 | // no mic or speaker stored,store default one 53 | if (mic === undefined || mic === '') { 54 | storage.setMic(selectedMic.id); 55 | } 56 | 57 | if (speaker === undefined || speaker === '') { 58 | storage.setSpeaker(selectedSpeaker.id); 59 | } 60 | 61 | storage.setVideoEncoder(selectedVEncoder.id); 62 | storage.setFps(fps); 63 | storage.setQuality(quality); 64 | storage.setOutputDir(output === '' ? getDesktopFolder() : output); 65 | }; 66 | 67 | const init = () => { 68 | initLog(); 69 | initSettings(); 70 | }; 71 | 72 | init(); 73 | -------------------------------------------------------------------------------- /test/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-props-no-spreading: off */ 2 | import { spy } from 'sinon'; 3 | import React from 'react'; 4 | import Enzyme, { shallow } from 'enzyme'; 5 | import Adapter from 'enzyme-adapter-react-16'; 6 | import { BrowserRouter as Router } from 'react-router-dom'; 7 | import renderer from 'react-test-renderer'; 8 | import Counter from '../../app/components/Counter'; 9 | 10 | Enzyme.configure({ adapter: new Adapter() }); 11 | 12 | function setup() { 13 | const actions = { 14 | increment: spy(), 15 | incrementIfOdd: spy(), 16 | incrementAsync: spy(), 17 | decrement: spy() 18 | }; 19 | const component = shallow(); 20 | return { 21 | component, 22 | actions, 23 | buttons: component.find('button'), 24 | p: component.find('.counter') 25 | }; 26 | } 27 | 28 | describe('Counter component', () => { 29 | it('should should display count', () => { 30 | const { p } = setup(); 31 | expect(p.text()).toMatch(/^1$/); 32 | }); 33 | 34 | it('should first button should call increment', () => { 35 | const { buttons, actions } = setup(); 36 | buttons.at(0).simulate('click'); 37 | expect(actions.increment.called).toBe(true); 38 | }); 39 | 40 | it('should match exact snapshot', () => { 41 | const { actions } = setup(); 42 | const counter = ( 43 |
44 | 45 | 46 | 47 |
48 | ); 49 | const tree = renderer.create(counter).toJSON(); 50 | 51 | expect(tree).toMatchSnapshot(); 52 | }); 53 | 54 | it('should second button should call decrement', () => { 55 | const { buttons, actions } = setup(); 56 | buttons.at(1).simulate('click'); 57 | expect(actions.decrement.called).toBe(true); 58 | }); 59 | 60 | it('should third button should call incrementIfOdd', () => { 61 | const { buttons, actions } = setup(); 62 | buttons.at(2).simulate('click'); 63 | expect(actions.incrementIfOdd.called).toBe(true); 64 | }); 65 | 66 | it('should fourth button should call incrementAsync', () => { 67 | const { buttons, actions } = setup(); 68 | buttons.at(3).simulate('click'); 69 | expect(actions.incrementAsync.called).toBe(true); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /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 'connected-react-router'; 5 | import { createLogger } from 'redux-logger'; 6 | import createRootReducer from '../reducers'; 7 | import * as counterActions from '../actions/counter'; 8 | import type { StateType } from '../reducers/types'; 9 | 10 | const history = createHashHistory(); 11 | 12 | const rootReducer = createRootReducer(history); 13 | 14 | const configureStore = (initialState?: StateType) => { 15 | // Redux Configuration 16 | const middleware = []; 17 | const enhancers = []; 18 | 19 | // Thunk Middleware 20 | middleware.push(thunk); 21 | 22 | // Logging Middleware 23 | const logger = createLogger({ 24 | level: 'info', 25 | collapsed: true 26 | }); 27 | 28 | // Skip redux logs in console during the tests 29 | if (process.env.NODE_ENV !== 'test') { 30 | middleware.push(logger); 31 | } 32 | 33 | // Router Middleware 34 | const router = routerMiddleware(history); 35 | middleware.push(router); 36 | 37 | // Redux DevTools Configuration 38 | const actionCreators = { 39 | ...counterActions, 40 | ...routerActions 41 | }; 42 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose 43 | /* eslint-disable no-underscore-dangle */ 44 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 45 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 46 | // Options: http://extension.remotedev.io/docs/API/Arguments.html 47 | actionCreators 48 | }) 49 | : compose; 50 | /* eslint-enable no-underscore-dangle */ 51 | 52 | // Apply Middleware & Compose Enhancers 53 | enhancers.push(applyMiddleware(...middleware)); 54 | const enhancer = composeEnhancers(...enhancers); 55 | 56 | // Create Store 57 | const store = createStore(rootReducer, initialState, enhancer); 58 | 59 | if (module.hot) { 60 | module.hot.accept( 61 | '../reducers', 62 | // eslint-disable-next-line global-require 63 | () => store.replaceReducer(require('../reducers').default) 64 | ); 65 | } 66 | 67 | return store; 68 | }; 69 | 70 | export default { configureStore, history }; 71 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off */ 2 | 3 | const developmentEnvironments = ['development', 'test']; 4 | 5 | const developmentPlugins = [ 6 | require('react-hot-loader/babel'), 7 | [ 8 | 'import', 9 | { 10 | libraryName: 'antd', 11 | libraryDirectory: 'es', 12 | style: 'css' 13 | } 14 | ] 15 | ]; 16 | 17 | const productionPlugins = [ 18 | require('babel-plugin-dev-expression'), 19 | 20 | // babel-preset-react-optimize 21 | require('@babel/plugin-transform-react-constant-elements'), 22 | require('@babel/plugin-transform-react-inline-elements'), 23 | require('babel-plugin-transform-react-remove-prop-types') 24 | ]; 25 | 26 | module.exports = api => { 27 | // see docs about api at https://babeljs.io/docs/en/config-files#apicache 28 | 29 | const development = api.env(developmentEnvironments); 30 | 31 | return { 32 | presets: [ 33 | [ 34 | require('@babel/preset-env'), 35 | { 36 | targets: { electron: require('electron/package.json').version } 37 | } 38 | ], 39 | require('@babel/preset-flow'), 40 | [require('@babel/preset-react'), { development }] 41 | ], 42 | plugins: [ 43 | // Stage 0 44 | require('@babel/plugin-proposal-function-bind'), 45 | 46 | // Stage 1 47 | require('@babel/plugin-proposal-export-default-from'), 48 | require('@babel/plugin-proposal-logical-assignment-operators'), 49 | [require('@babel/plugin-proposal-optional-chaining'), { loose: false }], 50 | [ 51 | require('@babel/plugin-proposal-pipeline-operator'), 52 | { proposal: 'minimal' } 53 | ], 54 | [ 55 | require('@babel/plugin-proposal-nullish-coalescing-operator'), 56 | { loose: false } 57 | ], 58 | require('@babel/plugin-proposal-do-expressions'), 59 | 60 | // Stage 2 61 | [require('@babel/plugin-proposal-decorators'), { legacy: true }], 62 | require('@babel/plugin-proposal-function-sent'), 63 | require('@babel/plugin-proposal-export-namespace-from'), 64 | require('@babel/plugin-proposal-numeric-separator'), 65 | require('@babel/plugin-proposal-throw-expressions'), 66 | 67 | // Stage 3 68 | require('@babel/plugin-syntax-dynamic-import'), 69 | require('@babel/plugin-syntax-import-meta'), 70 | [require('@babel/plugin-proposal-class-properties'), { loose: true }], 71 | require('@babel/plugin-proposal-json-strings'), 72 | 73 | ...(development ? developmentPlugins : productionPlugins) 74 | ] 75 | }; 76 | }; 77 | -------------------------------------------------------------------------------- /test/e2e/HomePage.e2e.js: -------------------------------------------------------------------------------- 1 | import { ClientFunction, Selector } from 'testcafe'; 2 | import { getPageUrl } from './helpers'; 3 | 4 | const getPageTitle = ClientFunction(() => document.title); 5 | const counterSelector = Selector('[data-tid="counter"]'); 6 | const buttonsSelector = Selector('[data-tclass="btn"]'); 7 | const clickToCounterLink = t => 8 | t.click(Selector('a').withExactText('to Counter')); 9 | const incrementButton = buttonsSelector.nth(0); 10 | const decrementButton = buttonsSelector.nth(1); 11 | const oddButton = buttonsSelector.nth(2); 12 | const asyncButton = buttonsSelector.nth(3); 13 | const getCounterText = () => counterSelector().innerText; 14 | const assertNoConsoleErrors = async t => { 15 | const { error } = await t.getBrowserConsoleMessages(); 16 | await t.expect(error).eql([]); 17 | }; 18 | 19 | fixture`Home Page`.page('../../app/app.html').afterEach(assertNoConsoleErrors); 20 | 21 | test('e2e', async t => { 22 | await t.expect(getPageTitle()).eql('Hello Electron React!'); 23 | }); 24 | 25 | test('should open window and contain expected page title', async t => { 26 | await t.expect(getPageTitle()).eql('Hello Electron React!'); 27 | }); 28 | 29 | test( 30 | 'should not have any logs in console of main window', 31 | assertNoConsoleErrors 32 | ); 33 | 34 | test('should navigate to Counter with click on the "to Counter" link', async t => { 35 | await t 36 | .click('[data-tid=container] > a') 37 | .expect(getCounterText()) 38 | .eql('0'); 39 | }); 40 | 41 | test('should navigate to /counter', async t => { 42 | await t 43 | .click('a') 44 | .expect(getPageUrl()) 45 | .contains('/counter'); 46 | }); 47 | 48 | fixture`Counter Tests` 49 | .page('../../app/app.html') 50 | .beforeEach(clickToCounterLink) 51 | .afterEach(assertNoConsoleErrors); 52 | 53 | test('should display updated count after the increment button click', async t => { 54 | await t 55 | .click(incrementButton) 56 | .expect(getCounterText()) 57 | .eql('1'); 58 | }); 59 | 60 | test('should display updated count after the descrement button click', async t => { 61 | await t 62 | .click(decrementButton) 63 | .expect(getCounterText()) 64 | .eql('-1'); 65 | }); 66 | 67 | test('should not change even counter if odd button clicked', async t => { 68 | await t 69 | .click(oddButton) 70 | .expect(getCounterText()) 71 | .eql('0'); 72 | }); 73 | 74 | test('should change odd counter if odd button clicked', async t => { 75 | await t 76 | .click(incrementButton) 77 | .click(oddButton) 78 | .expect(getCounterText()) 79 | .eql('2'); 80 | }); 81 | 82 | test('should change if async button clicked and a second later', async t => { 83 | await t 84 | .click(asyncButton) 85 | .expect(getCounterText()) 86 | .eql('0') 87 | .expect(getCounterText()) 88 | .eql('1'); 89 | }); 90 | 91 | test('should back to home if back button clicked', async t => { 92 | await t 93 | .click('[data-tid="backButton"] > a') 94 | .expect(Selector('[data-tid="container"]').visible) 95 | .ok(); 96 | }); 97 | -------------------------------------------------------------------------------- /app/utils/recorder.js: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import EasyRecorder from 'ffmpeg-recorder'; 3 | 4 | import storage from './storage'; 5 | import helper from './helper'; 6 | 7 | import { 8 | recorderStart, 9 | recorderStop, 10 | recorderPause, 11 | recorderResume 12 | } from '../actions/recorder'; 13 | 14 | import { configuredStore as store } from '../store/configureStore'; 15 | import { msgSuccess, msgError } from './notification'; 16 | 17 | const ffRecorder = new EasyRecorder(); 18 | 19 | window.ffRecorder = ffRecorder; 20 | 21 | const Recorder = { 22 | getDevices() { 23 | return { 24 | mics: ffRecorder.GetMics(), 25 | speakers: ffRecorder.GetSpeakers() 26 | }; 27 | }, 28 | getVideoEncoders() { 29 | return ffRecorder.GetVideoEncoders(); 30 | }, 31 | start() { 32 | const { isRecording } = store.getState().recorder; 33 | if (isRecording === true) { 34 | log.error('already recording,can not start again'); 35 | return false; 36 | } 37 | 38 | const outputFileName = helper.generateVideoFileName( 39 | storage.getOutputDir(), 40 | 'mkv' 41 | ); 42 | 43 | log.info('start to record to file : ', outputFileName); 44 | 45 | const mic = storage.isMicEnabled() ? storage.getMic() : ''; 46 | const speaker = storage.isSpeakerEnabled() ? storage.getSpeaker() : ''; 47 | const vencoderId = storage.getVideoEncoder(); 48 | 49 | const ret = ffRecorder.Init( 50 | storage.getQuality(), 51 | storage.getFps(), 52 | outputFileName, 53 | speaker, 54 | speaker, 55 | mic, 56 | mic, 57 | vencoderId 58 | ); 59 | 60 | if (ret === 0) { 61 | ffRecorder.EnablePreview(true); 62 | ffRecorder.Start(); 63 | store.dispatch(recorderStart(outputFileName)); 64 | log.info('start to record succed'); 65 | msgSuccess('start to record succed'); 66 | } else { 67 | log.error('start to record failed:', ret); 68 | msgError('start to record failed'); 69 | } 70 | 71 | return ret === 0; 72 | }, 73 | stop() { 74 | const { isRecording } = store.getState().recorder; 75 | 76 | if (isRecording === true) { 77 | ffRecorder.Stop(); 78 | ffRecorder.Release(); 79 | store.dispatch(recorderStop()); 80 | log.info('stop to record succed.'); 81 | msgSuccess('stop succed'); 82 | } else { 83 | log.error('already stopped,can not stop again'); 84 | msgError('stop failed'); 85 | } 86 | 87 | return true; 88 | }, 89 | pause() { 90 | store.dispatch(recorderPause()); 91 | ffRecorder.Pause(); 92 | }, 93 | resume() { 94 | store.dispatch(recorderResume()); 95 | ffRecorder.Resume(); 96 | }, 97 | 98 | /** 99 | * 100 | * @param {(image:{size:number,width:number,height:number,type:number,data:Buffer})=>void} callback 101 | */ 102 | onYuv(callback) { 103 | ffRecorder.SetPreviewYuvCallBack(callback); 104 | }, 105 | 106 | /** 107 | * 108 | * @param {boolean} enable 109 | */ 110 | enablePreview(enable) { 111 | ffRecorder.EnablePreview(enable); 112 | }, 113 | 114 | /** 115 | * 116 | * @param {Element} element 117 | */ 118 | bind(element) { 119 | ffRecorder.SetPreviewElement(element); 120 | }, 121 | 122 | /** 123 | * 124 | */ 125 | unBind() { 126 | ffRecorder.UnSetPreviewElement(); 127 | } 128 | }; 129 | 130 | export default Recorder; 131 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at electronreactboilerplate@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /app/components/RecordingBar/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import moment from 'moment'; 4 | 5 | import CustomButton from '../CustomButton'; 6 | import './style.scss'; 7 | 8 | type OP = 'start' | 'stop' | 'pause' | 'resume'; 9 | 10 | type Props = { 11 | isLoading: boolean, 12 | isRecording: boolean, 13 | isPaused: boolean, 14 | startTime: number, 15 | onRecordClick: OP => void, 16 | onFolderClick: () => void 17 | }; 18 | 19 | const padZero = str => { 20 | return new RegExp(/^\d$/g).test(str) ? `0${str}` : str; 21 | }; 22 | 23 | export default class RecordingBar extends Component { 24 | props: Props; 25 | 26 | constructor(props) { 27 | super(props); 28 | this.state = { 29 | timer: -1, 30 | duration: '00:00:00' 31 | }; 32 | } 33 | 34 | componentDidMount() { 35 | const timer = setInterval(() => { 36 | const { isRecording, isPaused } = this.props; 37 | if (isRecording === false || isPaused === true) return; 38 | 39 | this.setState({ 40 | duration: this.getFormatedTimeSpan() 41 | }); 42 | }, 1000); 43 | 44 | this.setState({ timer, duration: this.getFormatedTimeSpan() }); 45 | } 46 | 47 | componentWillUnmount() { 48 | const { timer } = this.state; 49 | if (timer !== -1) clearInterval(timer); 50 | } 51 | 52 | getFormatedTimeSpan() { 53 | const { startTime } = this.props; 54 | if (startTime === 0) return '00:00:00'; 55 | 56 | const duration = moment.duration(moment() - moment(startTime), 'ms'); 57 | 58 | const hours = duration.get('hours'); 59 | const minutes = duration.get('minutes'); 60 | const seconds = duration.get('seconds'); 61 | 62 | const ret = `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}`; 63 | 64 | return ret; 65 | } 66 | 67 | render() { 68 | const { 69 | isLoading, 70 | isRecording, 71 | isPaused, 72 | onRecordClick, 73 | onFolderClick 74 | } = this.props; 75 | 76 | const { duration } = this.state; 77 | 78 | return ( 79 |
80 | onRecordClick('start')} 87 | /> 88 | 89 | onRecordClick('stop')} 96 | /> 97 | 98 | onRecordClick('resume')} 105 | /> 106 | 107 | onRecordClick('pause')} 114 | /> 115 | 116 | {isRecording ? ( 117 | {duration} 118 | ) : ( 119 | <> 120 | )} 121 | 122 |
123 | 124 | 131 |
132 | ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /app/main.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off */ 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 `yarn build` or `yarn 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, dialog, ipcMain, globalShortcut } from 'electron'; 14 | import { autoUpdater } from 'electron-updater'; 15 | import log from 'electron-log'; 16 | 17 | // import './event'; 18 | 19 | export default class AppUpdater { 20 | constructor() { 21 | log.transports.file.level = 'info'; 22 | autoUpdater.logger = log; 23 | autoUpdater.checkForUpdatesAndNotify(); 24 | } 25 | } 26 | 27 | let mainWindow = null; 28 | 29 | if (process.env.NODE_ENV === 'production') { 30 | const sourceMapSupport = require('source-map-support'); 31 | sourceMapSupport.install(); 32 | } 33 | 34 | if ( 35 | process.env.NODE_ENV === 'development' || 36 | process.env.DEBUG_PROD === 'true' 37 | ) { 38 | require('electron-debug')(); 39 | } 40 | 41 | const installExtensions = async () => { 42 | const installer = require('electron-devtools-installer'); 43 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS; 44 | const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']; 45 | 46 | return Promise.all( 47 | extensions.map(name => installer.default(installer[name], forceDownload)) 48 | ).catch(console.log); 49 | }; 50 | 51 | const createWindow = async () => { 52 | if ( 53 | process.env.NODE_ENV === 'development' || 54 | process.env.DEBUG_PROD === 'true' 55 | ) { 56 | await installExtensions(); 57 | } 58 | 59 | mainWindow = new BrowserWindow({ 60 | show: false, 61 | minWidth: 1024, 62 | minHeight: 768, 63 | width: 1024, 64 | height: 768, 65 | autoHideMenuBar: true, 66 | webPreferences: { 67 | nodeIntegration: true 68 | } 69 | }); 70 | 71 | mainWindow.loadURL(`file://${__dirname}/app.html`); 72 | 73 | // @TODO: Use 'ready-to-show' event 74 | // https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event 75 | mainWindow.webContents.on('did-finish-load', () => { 76 | if (!mainWindow) { 77 | throw new Error('"mainWindow" is not defined'); 78 | } 79 | if (process.env.START_MINIMIZED) { 80 | mainWindow.minimize(); 81 | } else { 82 | mainWindow.show(); 83 | mainWindow.focus(); 84 | } 85 | }); 86 | 87 | mainWindow.on('closed', () => { 88 | mainWindow = null; 89 | }); 90 | 91 | ipcMain.on('custom-open-folder-req', (event, arg) => { 92 | dialog 93 | .showOpenDialog(mainWindow, { 94 | ...arg, 95 | properties: ['openDirectory'] 96 | }) 97 | .then(value => { 98 | log.info('custom open folder req succed,', value); 99 | if (value.canceled === false && value.filePaths.length > 0) { 100 | event.sender.send('custom-open-folder-res', value.filePaths); 101 | } 102 | return value; 103 | }) 104 | .catch(err => { 105 | log.debug('custom open folder req failed,', err); 106 | }); 107 | }); 108 | 109 | globalShortcut.register('Ctrl+Alt+Shift+I', () => { 110 | if (mainWindow) mainWindow.openDevTools(); 111 | }); 112 | 113 | /* 114 | const menuBuilder = new MenuBuilder(mainWindow); 115 | menuBuilder.buildMenu(); 116 | */ 117 | 118 | // Remove this if your app does not use auto updates 119 | // eslint-disable-next-line 120 | new AppUpdater(); 121 | }; 122 | 123 | /** 124 | * Add event listeners... 125 | */ 126 | 127 | app.on('window-all-closed', () => { 128 | // Respect the OSX convention of having the application in memory even 129 | // after all windows have been closed 130 | if (process.platform !== 'darwin') { 131 | app.quit(); 132 | } 133 | }); 134 | 135 | app.on('ready', createWindow); 136 | 137 | app.on('activate', () => { 138 | // On macOS it's common to re-create a window in the app when the 139 | // dock icon is clicked and there are no other windows open. 140 | if (mainWindow === null) createWindow(); 141 | }); 142 | -------------------------------------------------------------------------------- /app/containers/Content/Settings/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import log from 'electron-log'; 3 | import { connect } from 'react-redux'; 4 | 5 | import { DeviceItem, EncoderItem } from '../../../utils/types'; 6 | import recorder from '../../../utils/recorder'; 7 | import helper from '../../../utils/helper'; 8 | import storage from '../../../utils/storage'; 9 | 10 | import SettingForm from './setting-form'; 11 | 12 | import './style.scss'; 13 | 14 | type Props = { 15 | isRecording: boolean 16 | }; 17 | 18 | type State = { 19 | mics: Array, 20 | speakers: Array, 21 | vEncoders: Array, 22 | currentMic: DeviceItem, 23 | currentSpeaker: DeviceItem, 24 | currentVEncoder: EncoderItem, 25 | fps: number, 26 | quality: number, 27 | output: string, 28 | isMicEnabled: boolean, 29 | isSpeakerEnabled: boolean 30 | }; 31 | 32 | class SettingLayout extends Component { 33 | props: Props; 34 | 35 | constructor(props: Props) { 36 | super(props); 37 | 38 | this.state = { 39 | mics: [], 40 | speakers: [], 41 | vEncoders: [], 42 | currentMic: undefined, 43 | currentSpeaker: undefined, 44 | currentVEncoder: undefined, 45 | fps: storage.getFps(), 46 | quality: storage.getQuality(), 47 | output: storage.getOutputDir(), 48 | isMicEnabled: storage.isMicEnabled(), 49 | isSpeakerEnabled: storage.isSpeakerEnabled() 50 | }; 51 | 52 | this.refreshSetting = this.refreshSetting.bind(this); 53 | this.onSubmit = this.onSubmit.bind(this); 54 | } 55 | 56 | componentDidMount() { 57 | const { isRecording } = this.props; 58 | if (isRecording === true) return; 59 | 60 | this.refreshSetting(); 61 | } 62 | 63 | refreshSetting() { 64 | const { mics, speakers } = recorder.getDevices(); 65 | // will crashed when recording with hardware encoder 66 | const vEncoders = recorder.getVideoEncoders(); 67 | const mic = storage.getMic(); 68 | const speaker = storage.getSpeaker(); 69 | const vencoderId = storage.getVideoEncoder(); 70 | 71 | this.setState({ 72 | mics, 73 | speakers, 74 | vEncoders, 75 | currentMic: helper.getDefaultSelectedDevice(mics, mic), 76 | currentSpeaker: helper.getDefaultSelectedDevice(speakers, speaker), 77 | currentVEncoder: helper.getDefaultSelectedEncoder(vEncoders, vencoderId), 78 | fps: storage.getFps(), 79 | quality: storage.getQuality(), 80 | output: storage.getOutputDir(), 81 | isMicEnabled: storage.isMicEnabled(), 82 | isSpeakerEnabled: storage.isSpeakerEnabled() 83 | }); 84 | } 85 | 86 | onSubmit(values) { 87 | log.info('on setting form submit:', values); 88 | 89 | const { 90 | mic, 91 | speaker, 92 | vencoder, 93 | fps, 94 | quality, 95 | output, 96 | isMicEnabled, 97 | isSpeakerEnabled 98 | } = values; 99 | this.setState( 100 | { 101 | currentMic: mic, 102 | currentSpeaker: speaker, 103 | currentVEncoder: vencoder, 104 | fps, 105 | quality, 106 | isMicEnabled, 107 | isSpeakerEnabled, 108 | output 109 | }, 110 | () => { 111 | storage.setMic(mic.id); 112 | storage.setSpeaker(speaker.id); 113 | storage.setFps(fps); 114 | storage.setQuality(quality); 115 | storage.setOutputDir(output); 116 | storage.enableMic(isMicEnabled); 117 | storage.enableSpeaker(isSpeakerEnabled); 118 | storage.setVideoEncoder(vencoder.id); 119 | } 120 | ); 121 | } 122 | 123 | render() { 124 | const { 125 | mics, 126 | speakers, 127 | vEncoders, 128 | currentMic, 129 | currentSpeaker, 130 | currentVEncoder, 131 | fps, 132 | quality, 133 | output, 134 | isMicEnabled, 135 | isSpeakerEnabled 136 | } = this.state; 137 | 138 | const { isRecording } = this.props; 139 | 140 | return ( 141 |
142 | 157 |
158 | ); 159 | } 160 | } 161 | 162 | function mapStateToProps(state) { 163 | return { 164 | isRecording: state.recorder.isRecording 165 | }; 166 | } 167 | 168 | export default connect(mapStateToProps)(SettingLayout); 169 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.prod.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron renderer process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 8 | import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import merge from 'webpack-merge'; 11 | import TerserPlugin from 'terser-webpack-plugin'; 12 | import baseConfig from './webpack.config.base'; 13 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 14 | 15 | CheckNodeEnv('production'); 16 | 17 | export default merge.smart(baseConfig, { 18 | devtool: 'source-map', 19 | 20 | mode: 'production', 21 | 22 | target: 'electron-renderer', 23 | 24 | entry: path.join(__dirname, '..', 'app/index'), 25 | 26 | output: { 27 | path: path.join(__dirname, '..', 'app/dist'), 28 | publicPath: './dist/', 29 | filename: 'renderer.prod.js' 30 | }, 31 | 32 | module: { 33 | rules: [ 34 | // Extract all .global.css to style.css as is 35 | { 36 | test: /\.global\.css$/, 37 | use: [ 38 | { 39 | loader: MiniCssExtractPlugin.loader, 40 | options: { 41 | publicPath: './' 42 | } 43 | }, 44 | { 45 | loader: 'css-loader', 46 | options: { 47 | sourceMap: true 48 | } 49 | } 50 | ] 51 | }, 52 | // Pipe other styles through css modules and append to style.css 53 | { 54 | test: /^((?!\.global).)*\.css$/, 55 | use: [ 56 | { 57 | loader: MiniCssExtractPlugin.loader 58 | }, 59 | { 60 | loader: 'css-loader', 61 | options: { 62 | modules: { 63 | localIdentName: '[name]__[local]__[hash:base64:5]' 64 | }, 65 | sourceMap: true 66 | } 67 | } 68 | ] 69 | }, 70 | // Add SASS support - compile all .global.scss files and pipe it to style.css 71 | { 72 | test: /\.global\.(scss|sass)$/, 73 | use: [ 74 | { 75 | loader: MiniCssExtractPlugin.loader 76 | }, 77 | { 78 | loader: 'css-loader', 79 | options: { 80 | sourceMap: true, 81 | importLoaders: 1 82 | } 83 | }, 84 | { 85 | loader: 'sass-loader', 86 | options: { 87 | sourceMap: true 88 | } 89 | } 90 | ] 91 | }, 92 | // Add SASS support - compile all other .scss files and pipe it to style.css 93 | { 94 | test: /^((?!\.global).)*\.(scss|sass)$/, 95 | use: [ 96 | { 97 | loader: MiniCssExtractPlugin.loader 98 | }, 99 | { 100 | loader: 'css-loader', 101 | options: { 102 | /* 103 | modules: { 104 | localIdentName: '[name]__[local]__[hash:base64:5]' 105 | }, 106 | importLoaders: 1, 107 | */ 108 | sourceMap: true 109 | } 110 | }, 111 | { 112 | loader: 'sass-loader', 113 | options: { 114 | sourceMap: true 115 | } 116 | } 117 | ] 118 | }, 119 | // WOFF Font 120 | { 121 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 122 | use: { 123 | loader: 'url-loader', 124 | options: { 125 | limit: 10000, 126 | mimetype: 'application/font-woff' 127 | } 128 | } 129 | }, 130 | // WOFF2 Font 131 | { 132 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 133 | use: { 134 | loader: 'url-loader', 135 | options: { 136 | limit: 10000, 137 | mimetype: 'application/font-woff' 138 | } 139 | } 140 | }, 141 | // TTF Font 142 | { 143 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 144 | use: { 145 | loader: 'url-loader', 146 | options: { 147 | limit: 10000, 148 | mimetype: 'application/octet-stream' 149 | } 150 | } 151 | }, 152 | // EOT Font 153 | { 154 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 155 | use: 'file-loader' 156 | }, 157 | // SVG Font 158 | { 159 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 160 | use: { 161 | loader: 'url-loader', 162 | options: { 163 | limit: 10000, 164 | mimetype: 'image/svg+xml' 165 | } 166 | } 167 | }, 168 | // Common Image Formats 169 | { 170 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 171 | use: 'url-loader' 172 | } 173 | ] 174 | }, 175 | 176 | optimization: { 177 | minimizer: process.env.E2E_BUILD 178 | ? [] 179 | : [ 180 | new TerserPlugin({ 181 | parallel: true, 182 | sourceMap: true, 183 | cache: true 184 | }), 185 | new OptimizeCSSAssetsPlugin({ 186 | cssProcessorOptions: { 187 | map: { 188 | inline: false, 189 | annotation: true 190 | } 191 | } 192 | }) 193 | ] 194 | }, 195 | 196 | plugins: [ 197 | /** 198 | * Create global constants which can be configured at compile time. 199 | * 200 | * Useful for allowing different behaviour between development builds and 201 | * release builds 202 | * 203 | * NODE_ENV should be production so that modules do not perform certain 204 | * development checks 205 | */ 206 | new webpack.EnvironmentPlugin({ 207 | NODE_ENV: 'production' 208 | }), 209 | 210 | new MiniCssExtractPlugin({ 211 | filename: 'style.css' 212 | }), 213 | 214 | new BundleAnalyzerPlugin({ 215 | analyzerMode: 216 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 217 | openAnalyzer: process.env.OPEN_ANALYZER === 'true' 218 | }) 219 | ] 220 | }); 221 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.dev.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, import/no-dynamic-require: off */ 2 | 3 | /** 4 | * Build config for development electron renderer process that uses 5 | * Hot-Module-Replacement 6 | * 7 | * https://webpack.js.org/concepts/hot-module-replacement/ 8 | */ 9 | 10 | import path from 'path'; 11 | import fs from 'fs'; 12 | import webpack from 'webpack'; 13 | import chalk from 'chalk'; 14 | import merge from 'webpack-merge'; 15 | import { spawn, execSync } from 'child_process'; 16 | import baseConfig from './webpack.config.base'; 17 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 18 | 19 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 20 | // at the dev webpack config is not accidentally run in a production environment 21 | if (process.env.NODE_ENV === 'production') { 22 | CheckNodeEnv('development'); 23 | } 24 | 25 | const port = process.env.PORT || 1212; 26 | const publicPath = `http://localhost:${port}/dist`; 27 | const dll = path.join(__dirname, '..', 'dll'); 28 | const manifest = path.resolve(dll, 'renderer.json'); 29 | const requiredByDLLConfig = module.parent.filename.includes( 30 | 'webpack.config.renderer.dev.dll' 31 | ); 32 | 33 | /** 34 | * Warn if the DLL is not built 35 | */ 36 | if (!requiredByDLLConfig && !(fs.existsSync(dll) && fs.existsSync(manifest))) { 37 | console.log( 38 | chalk.black.bgYellow.bold( 39 | 'The DLL files are missing. Sit back while we build them for you with "yarn build-dll"' 40 | ) 41 | ); 42 | execSync('yarn build-dll'); 43 | } 44 | 45 | export default merge.smart(baseConfig, { 46 | devtool: 'inline-source-map', 47 | 48 | mode: 'development', 49 | 50 | target: 'electron-renderer', 51 | 52 | entry: [ 53 | ...(process.env.PLAIN_HMR ? [] : ['react-hot-loader/patch']), 54 | `webpack-dev-server/client?http://localhost:${port}/`, 55 | 'webpack/hot/only-dev-server', 56 | require.resolve('../app/index') 57 | ], 58 | 59 | output: { 60 | publicPath: `http://localhost:${port}/dist/`, 61 | filename: 'renderer.dev.js' 62 | }, 63 | 64 | module: { 65 | rules: [ 66 | { 67 | test: /\.jsx?$/, 68 | exclude: /node_modules/, 69 | use: { 70 | loader: 'babel-loader', 71 | options: { 72 | cacheDirectory: true 73 | } 74 | } 75 | }, 76 | { 77 | test: /\.global\.css$/, 78 | use: [ 79 | { 80 | loader: 'style-loader' 81 | }, 82 | { 83 | loader: 'css-loader', 84 | options: { 85 | sourceMap: true 86 | } 87 | } 88 | ] 89 | }, 90 | { 91 | test: /^((?!\.global).)*\.css$/, 92 | use: [ 93 | { 94 | loader: 'style-loader' 95 | }, 96 | { 97 | loader: 'css-loader', 98 | options: { 99 | modules: { 100 | localIdentName: '[name]__[local]__[hash:base64:5]' 101 | }, 102 | sourceMap: true, 103 | importLoaders: 1 104 | } 105 | } 106 | ] 107 | }, 108 | // SASS support - compile all .global.scss files and pipe it to style.css 109 | { 110 | test: /\.global\.(scss|sass)$/, 111 | use: [ 112 | { 113 | loader: 'style-loader' 114 | }, 115 | { 116 | loader: 'css-loader', 117 | options: { 118 | sourceMap: true 119 | } 120 | }, 121 | { 122 | loader: 'sass-loader' 123 | } 124 | ] 125 | }, 126 | // SASS support - compile all other .scss files and pipe it to style.css 127 | { 128 | test: /^((?!\.global).)*\.(scss|sass)$/, 129 | use: [ 130 | { 131 | loader: 'style-loader' 132 | }, 133 | { 134 | loader: 'css-loader', 135 | options: { 136 | /* 137 | modules: { 138 | localIdentName: '[name]__[local]__[hash:base64:5]' 139 | }, 140 | importLoaders: 1, 141 | */ 142 | sourceMap: true 143 | } 144 | }, 145 | { 146 | loader: 'sass-loader' 147 | } 148 | ] 149 | }, 150 | // WOFF Font 151 | { 152 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 153 | use: { 154 | loader: 'url-loader', 155 | options: { 156 | limit: 10000, 157 | mimetype: 'application/font-woff' 158 | } 159 | } 160 | }, 161 | // WOFF2 Font 162 | { 163 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 164 | use: { 165 | loader: 'url-loader', 166 | options: { 167 | limit: 10000, 168 | mimetype: 'application/font-woff' 169 | } 170 | } 171 | }, 172 | // TTF Font 173 | { 174 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 175 | use: { 176 | loader: 'url-loader', 177 | options: { 178 | limit: 10000, 179 | mimetype: 'application/octet-stream' 180 | } 181 | } 182 | }, 183 | // EOT Font 184 | { 185 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 186 | use: 'file-loader' 187 | }, 188 | // SVG Font 189 | { 190 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 191 | use: { 192 | loader: 'url-loader', 193 | options: { 194 | limit: 10000, 195 | mimetype: 'image/svg+xml' 196 | } 197 | } 198 | }, 199 | // Common Image Formats 200 | { 201 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 202 | use: 'url-loader' 203 | } 204 | ] 205 | }, 206 | resolve: { 207 | alias: { 208 | 'react-dom': '@hot-loader/react-dom' 209 | } 210 | }, 211 | plugins: [ 212 | requiredByDLLConfig 213 | ? null 214 | : new webpack.DllReferencePlugin({ 215 | context: path.join(__dirname, '..', 'dll'), 216 | manifest: require(manifest), 217 | sourceType: 'var' 218 | }), 219 | 220 | new webpack.HotModuleReplacementPlugin({ 221 | multiStep: true 222 | }), 223 | 224 | new webpack.NoEmitOnErrorsPlugin(), 225 | 226 | /** 227 | * Create global constants which can be configured at compile time. 228 | * 229 | * Useful for allowing different behaviour between development builds and 230 | * release builds 231 | * 232 | * NODE_ENV should be production so that modules do not perform certain 233 | * development checks 234 | * 235 | * By default, use 'development' as NODE_ENV. This can be overriden with 236 | * 'staging', for example, by changing the ENV variables in the npm scripts 237 | */ 238 | new webpack.EnvironmentPlugin({ 239 | NODE_ENV: 'development' 240 | }), 241 | 242 | new webpack.LoaderOptionsPlugin({ 243 | debug: true 244 | }) 245 | ], 246 | 247 | node: { 248 | __dirname: false, 249 | __filename: false 250 | }, 251 | 252 | devServer: { 253 | port, 254 | publicPath, 255 | compress: true, 256 | noInfo: true, 257 | stats: 'errors-only', 258 | inline: true, 259 | lazy: false, 260 | hot: true, 261 | headers: { 'Access-Control-Allow-Origin': '*' }, 262 | contentBase: path.join(__dirname, 'dist'), 263 | watchOptions: { 264 | aggregateTimeout: 300, 265 | ignored: /node_modules/, 266 | poll: 100 267 | }, 268 | historyApiFallback: { 269 | verbose: true, 270 | disableDotRule: false 271 | }, 272 | before() { 273 | if (process.env.START_HOT) { 274 | console.log('Starting Main Process...'); 275 | spawn('npm', ['run', 'start-main-dev'], { 276 | shell: true, 277 | env: process.env, 278 | stdio: 'inherit' 279 | }) 280 | .on('close', code => process.exit(code)) 281 | .on('error', spawnError => console.error(spawnError)); 282 | } 283 | } 284 | } 285 | }); 286 | -------------------------------------------------------------------------------- /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 ( 13 | process.env.NODE_ENV === 'development' || 14 | process.env.DEBUG_PROD === 'true' 15 | ) { 16 | this.setupDevelopmentEnvironment(); 17 | } 18 | 19 | const template = 20 | process.platform === 'darwin' 21 | ? this.buildDarwinTemplate() 22 | : this.buildDefaultTemplate(); 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.buildFromTemplate([ 36 | { 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 | { 51 | label: 'About ElectronReact', 52 | selector: 'orderFrontStandardAboutPanel:' 53 | }, 54 | { type: 'separator' }, 55 | { label: 'Services', submenu: [] }, 56 | { type: 'separator' }, 57 | { 58 | label: 'Hide ElectronReact', 59 | accelerator: 'Command+H', 60 | selector: 'hide:' 61 | }, 62 | { 63 | label: 'Hide Others', 64 | accelerator: 'Command+Shift+H', 65 | selector: 'hideOtherApplications:' 66 | }, 67 | { label: 'Show All', selector: 'unhideAllApplications:' }, 68 | { type: 'separator' }, 69 | { 70 | label: 'Quit', 71 | accelerator: 'Command+Q', 72 | click: () => { 73 | app.quit(); 74 | } 75 | } 76 | ] 77 | }; 78 | const subMenuEdit = { 79 | label: 'Edit', 80 | submenu: [ 81 | { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' }, 82 | { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' }, 83 | { type: 'separator' }, 84 | { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' }, 85 | { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' }, 86 | { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' }, 87 | { 88 | label: 'Select All', 89 | accelerator: 'Command+A', 90 | selector: 'selectAll:' 91 | } 92 | ] 93 | }; 94 | const subMenuViewDev = { 95 | label: 'View', 96 | submenu: [ 97 | { 98 | label: 'Reload', 99 | accelerator: 'Command+R', 100 | click: () => { 101 | this.mainWindow.webContents.reload(); 102 | } 103 | }, 104 | { 105 | label: 'Toggle Full Screen', 106 | accelerator: 'Ctrl+Command+F', 107 | click: () => { 108 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 109 | } 110 | }, 111 | { 112 | label: 'Toggle Developer Tools', 113 | accelerator: 'Alt+Command+I', 114 | click: () => { 115 | this.mainWindow.toggleDevTools(); 116 | } 117 | } 118 | ] 119 | }; 120 | const subMenuViewProd = { 121 | label: 'View', 122 | submenu: [ 123 | { 124 | label: 'Toggle Full Screen', 125 | accelerator: 'Ctrl+Command+F', 126 | click: () => { 127 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 128 | } 129 | } 130 | ] 131 | }; 132 | const subMenuWindow = { 133 | label: 'Window', 134 | submenu: [ 135 | { 136 | label: 'Minimize', 137 | accelerator: 'Command+M', 138 | selector: 'performMiniaturize:' 139 | }, 140 | { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }, 141 | { type: 'separator' }, 142 | { label: 'Bring All to Front', selector: 'arrangeInFront:' } 143 | ] 144 | }; 145 | const subMenuHelp = { 146 | label: 'Help', 147 | submenu: [ 148 | { 149 | label: 'Learn More', 150 | click() { 151 | shell.openExternal('http://electron.atom.io'); 152 | } 153 | }, 154 | { 155 | label: 'Documentation', 156 | click() { 157 | shell.openExternal( 158 | 'https://github.com/atom/electron/tree/master/docs#readme' 159 | ); 160 | } 161 | }, 162 | { 163 | label: 'Community Discussions', 164 | click() { 165 | shell.openExternal('https://discuss.atom.io/c/electron'); 166 | } 167 | }, 168 | { 169 | label: 'Search Issues', 170 | click() { 171 | shell.openExternal('https://github.com/atom/electron/issues'); 172 | } 173 | } 174 | ] 175 | }; 176 | 177 | const subMenuView = 178 | process.env.NODE_ENV === 'development' ? subMenuViewDev : subMenuViewProd; 179 | 180 | return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp]; 181 | } 182 | 183 | buildDefaultTemplate() { 184 | const templateDefault = [ 185 | { 186 | label: '&File', 187 | submenu: [ 188 | { 189 | label: '&Open', 190 | accelerator: 'Ctrl+O' 191 | }, 192 | { 193 | label: '&Close', 194 | accelerator: 'Ctrl+W', 195 | click: () => { 196 | this.mainWindow.close(); 197 | } 198 | } 199 | ] 200 | }, 201 | { 202 | label: '&View', 203 | submenu: 204 | process.env.NODE_ENV === 'development' 205 | ? [ 206 | { 207 | label: '&Reload', 208 | accelerator: 'Ctrl+R', 209 | click: () => { 210 | this.mainWindow.webContents.reload(); 211 | } 212 | }, 213 | { 214 | label: 'Toggle &Full Screen', 215 | accelerator: 'F11', 216 | click: () => { 217 | this.mainWindow.setFullScreen( 218 | !this.mainWindow.isFullScreen() 219 | ); 220 | } 221 | }, 222 | { 223 | label: 'Toggle &Developer Tools', 224 | accelerator: 'Alt+Ctrl+I', 225 | click: () => { 226 | this.mainWindow.toggleDevTools(); 227 | } 228 | } 229 | ] 230 | : [ 231 | { 232 | label: 'Toggle &Full Screen', 233 | accelerator: 'F11', 234 | click: () => { 235 | this.mainWindow.setFullScreen( 236 | !this.mainWindow.isFullScreen() 237 | ); 238 | } 239 | } 240 | ] 241 | }, 242 | { 243 | label: 'Help', 244 | submenu: [ 245 | { 246 | label: 'Learn More', 247 | click() { 248 | shell.openExternal('http://electron.atom.io'); 249 | } 250 | }, 251 | { 252 | label: 'Documentation', 253 | click() { 254 | shell.openExternal( 255 | 'https://github.com/atom/electron/tree/master/docs#readme' 256 | ); 257 | } 258 | }, 259 | { 260 | label: 'Community Discussions', 261 | click() { 262 | shell.openExternal('https://discuss.atom.io/c/electron'); 263 | } 264 | }, 265 | { 266 | label: 'Search Issues', 267 | click() { 268 | shell.openExternal('https://github.com/atom/electron/issues'); 269 | } 270 | } 271 | ] 272 | } 273 | ]; 274 | 275 | return templateDefault; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /app/containers/Content/Settings/setting-form.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unused-prop-types */ 2 | import React, { Component } from 'react'; 3 | import { ipcRenderer } from 'electron'; 4 | import { Form, Button, Input, Icon, Slider, Checkbox } from 'antd'; 5 | 6 | import { DeviceItem, EncoderItem } from '../../../utils/types'; 7 | 8 | import DeviceSelect from '../../../components/DeviceSelect'; 9 | 10 | type State = { 11 | isMicEnabled: boolean, 12 | isSpeakerEnabled: boolean 13 | }; 14 | 15 | type SettingFormData = { 16 | mic: DeviceItem, 17 | speaker: DeviceItem, 18 | vencoder: EncoderItem, 19 | fps: number, 20 | quality: number, 21 | isMicEnabled: boolean, 22 | isSpeakerEnabled: boolean, 23 | output: string 24 | }; 25 | 26 | type Props = { 27 | disabled: boolean, 28 | mics: Array, 29 | speakers: Array, 30 | vEncoders: Array, 31 | currentMic: DeviceItem, 32 | currentSpeaker: DeviceItem, 33 | currentVEncoder: EncoderItem, 34 | fps: number, 35 | quality: number, 36 | output: string, 37 | isMicEnabled: boolean, 38 | isSpeakerEnabled: boolean, 39 | onSubmit: SettingFormData => void 40 | }; 41 | 42 | class SettingForm extends Component { 43 | props: Props; 44 | 45 | constructor(props) { 46 | super(props); 47 | 48 | this.state = { 49 | isMicEnabled: props.isMicEnabled, 50 | isSpeakerEnabled: props.isSpeakerEnabled 51 | }; 52 | 53 | this.onSubmit = this.onSubmit.bind(this); 54 | } 55 | 56 | componentDidMount() { 57 | // eslint-disable-next-line react/prop-types 58 | const { form } = this.props; 59 | 60 | ipcRenderer.on('custom-open-folder-res', (event, arg) => { 61 | // eslint-disable-next-line react/prop-types 62 | form.setFieldsValue({ output: arg[0] }); 63 | }); 64 | } 65 | 66 | componentWillUnmount() { 67 | ipcRenderer.removeAllListeners('custom-open-folder-res'); 68 | } 69 | 70 | onSubmit(e) { 71 | // eslint-disable-next-line react/prop-types 72 | const { onSubmit, form } = this.props; 73 | const { isMicEnabled, isSpeakerEnabled } = this.state; 74 | e.preventDefault(); 75 | 76 | // eslint-disable-next-line react/prop-types 77 | form.validateFields((err, values) => { 78 | if (!err) { 79 | if (onSubmit) onSubmit({ ...values, isMicEnabled, isSpeakerEnabled }); 80 | } 81 | }); 82 | } 83 | 84 | render() { 85 | const { 86 | disabled, 87 | mics, 88 | speakers, 89 | vEncoders, 90 | // eslint-disable-next-line react/prop-types 91 | form 92 | } = this.props; 93 | 94 | const { isMicEnabled, isSpeakerEnabled } = this.state; 95 | 96 | // eslint-disable-next-line react/prop-types 97 | const { getFieldDecorator, setFieldsValue, getFieldValue } = form; 98 | 99 | return ( 100 |
101 | { 106 | this.setState({ isMicEnabled: !isMicEnabled }); 107 | }} 108 | disabled={disabled} 109 | > 110 | Microphones 111 | 112 | } 113 | > 114 | {getFieldDecorator('mic', { 115 | rules: [ 116 | { 117 | required: true && isMicEnabled, 118 | message: 'Must select a microphone' 119 | } 120 | ] 121 | })( 122 | setFieldsValue({ mic })} 127 | /> 128 | )} 129 | 130 | 131 | 136 | this.setState({ isSpeakerEnabled: !isSpeakerEnabled }) 137 | } 138 | disabled={disabled} 139 | > 140 | Speakers 141 | 142 | } 143 | > 144 | {getFieldDecorator('speaker', { 145 | rules: [ 146 | { 147 | required: true && isSpeakerEnabled, 148 | message: 'Must select a speaker' 149 | } 150 | ] 151 | })( 152 | setFieldsValue({ speaker })} 157 | /> 158 | )} 159 | 160 | 161 | 162 | {getFieldDecorator('vencoder', { 163 | rules: [ 164 | { 165 | required: true && isSpeakerEnabled, 166 | message: 'Must select a vencoder' 167 | } 168 | ] 169 | })( 170 | setFieldsValue({ vencoder })} 175 | /> 176 | )} 177 | 178 | 179 | 180 | {getFieldDecorator('fps', { 181 | rules: [ 182 | { 183 | required: true 184 | } 185 | ] 186 | })( 187 | 198 | )} 199 | 200 | 201 | 202 | {getFieldDecorator('quality', { 203 | rules: [ 204 | { 205 | required: true 206 | } 207 | ] 208 | })( 209 | 219 | )} 220 | 221 | 222 | 223 | {getFieldDecorator('output', { 224 | rules: [ 225 | { 226 | required: true, 227 | message: 'Must set a output directory' 228 | } 229 | ] 230 | })( 231 | { 237 | if (disabled === true) return; 238 | ipcRenderer.send('custom-open-folder-req'); 239 | }} 240 | /> 241 | } 242 | readOnly 243 | disabled={disabled} 244 | /> 245 | )} 246 | 247 | 248 | 249 | 252 | 253 |
254 | ); 255 | } 256 | } 257 | 258 | export default Form.create({ 259 | name: 'setting_form', 260 | mapPropsToFields(props) { 261 | return { 262 | mic: Form.createFormField({ 263 | ...props.currentMic, 264 | value: props.currentMic ? props.currentMic : null 265 | }), 266 | speaker: Form.createFormField({ 267 | ...props.currentSpeaker, 268 | value: props.currentSpeaker ? props.currentSpeaker : null 269 | }), 270 | vencoder: Form.createFormField({ 271 | ...props.currentVEncoder, 272 | value: props.currentVEncoder ? props.currentVEncoder : null 273 | }), 274 | fps: Form.createFormField({ 275 | ...props.fps, 276 | value: props.fps ? props.fps : 20 277 | }), 278 | quality: Form.createFormField({ 279 | ...props.quality, 280 | value: props.quality ? props.quality : 60 281 | }), 282 | output: Form.createFormField({ 283 | ...props.output, 284 | value: props.output ? props.output : '' 285 | }) 286 | }; 287 | } 288 | })(SettingForm); 289 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-recorder", 3 | "productName": "EasyRecorder", 4 | "version": "1.0.0", 5 | "description": "EasyRecorder depends on react and screen-recorder", 6 | "scripts": { 7 | "build": "concurrently \"yarn build-main\" \"yarn build-renderer\"", 8 | "build-dll": "cross-env NODE_ENV=development webpack --config ./configs/webpack.config.renderer.dev.dll.babel.js --colors", 9 | "build-e2e": "cross-env E2E_BUILD=true yarn build", 10 | "build-main": "cross-env NODE_ENV=production webpack --config ./configs/webpack.config.main.prod.babel.js --colors", 11 | "build-renderer": "cross-env NODE_ENV=production webpack --config ./configs/webpack.config.renderer.prod.babel.js --colors", 12 | "dev": "cross-env START_HOT=1 node -r @babel/register ./internals/scripts/CheckPortInUse.js && cross-env START_HOT=1 yarn start-renderer-dev", 13 | "electron-rebuild": "electron-rebuild --parallel --force --types prod,dev,optional --module-dir app", 14 | "flow": "flow", 15 | "flow-typed": "rimraf flow-typed/npm && flow-typed install --overwrite || true", 16 | "lint": "cross-env NODE_ENV=development eslint --cache --format=pretty .", 17 | "lint-fix": "yarn --silent lint --fix; exit 0", 18 | "lint-styles": "stylelint --ignore-path .eslintignore '**/*.*(css|scss)' --syntax scss", 19 | "lint-styles-fix": "yarn --silent lint-styles --fix; exit 0", 20 | "package": "yarn build && electron-builder build --publish never", 21 | "package-all": "yarn build && electron-builder build -mwl", 22 | "package-ci": "yarn postinstall && yarn build && electron-builder --publish always", 23 | "package-linux": "yarn build && electron-builder build --linux", 24 | "package-win": "yarn build && electron-builder build --win --x64", 25 | "postinstall": "node -r @babel/register internals/scripts/CheckNativeDep.js && yarn flow-typed && electron-builder install-app-deps && yarn build-dll && opencollective-postinstall", 26 | "postlint-fix": "prettier --ignore-path .eslintignore --single-quote --write '**/*.{js,jsx,json,html,css,less,scss,yml}'", 27 | "postlint-styles-fix": "prettier --ignore-path .eslintignore --single-quote --write '**/*.{css,scss}'", 28 | "preinstall": "node ./internals/scripts/CheckYarn.js", 29 | "prestart": "yarn build", 30 | "start": "cross-env NODE_ENV=production electron ./app/main.prod.js", 31 | "start-main-dev": "cross-env START_HOT=1 NODE_ENV=development electron -r @babel/register ./app/main.dev.js", 32 | "start-renderer-dev": "cross-env NODE_ENV=development webpack-dev-server --config configs/webpack.config.renderer.dev.babel.js", 33 | "test": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 jest", 34 | "test-all": "yarn lint && yarn flow && yarn build && yarn test && yarn build-e2e && yarn test-e2e", 35 | "test-e2e": "node -r @babel/register ./internals/scripts/CheckBuildsExist.js && cross-env NODE_ENV=test testcafe electron:./app ./test/e2e/HomePage.e2e.js", 36 | "test-e2e-live": "node -r @babel/register ./internals/scripts/CheckBuildsExist.js && cross-env NODE_ENV=test testcafe --live electron:./app ./test/e2e/HomePage.e2e.js", 37 | "test-watch": "yarn test --watch" 38 | }, 39 | "lint-staged": { 40 | "*.{js,jsx}": [ 41 | "cross-env NODE_ENV=development eslint --cache --format=pretty", 42 | "git add" 43 | ], 44 | "{*.json,.{babelrc,eslintrc,prettierrc,stylelintrc}}": [ 45 | "prettier --ignore-path .eslintignore --parser json --write", 46 | "git add" 47 | ], 48 | "*.{css,scss}": [ 49 | "stylelint --ignore-path .eslintignore --syntax scss --fix", 50 | "prettier --ignore-path .eslintignore --single-quote --write", 51 | "git add" 52 | ], 53 | "*.{html,md,yml}": [ 54 | "prettier --ignore-path .eslintignore --single-quote --write", 55 | "git add" 56 | ] 57 | }, 58 | "build": { 59 | "productName": "EasyRecorder", 60 | "appId": "org.sylar.EasyRecorder", 61 | "files": [ 62 | "dist/", 63 | "node_modules/", 64 | "app.html", 65 | "main.prod.js", 66 | "main.prod.js.map", 67 | "package.json" 68 | ], 69 | "dmg": { 70 | "contents": [ 71 | { 72 | "x": 130, 73 | "y": 220 74 | }, 75 | { 76 | "x": 410, 77 | "y": 220, 78 | "type": "link", 79 | "path": "/Applications" 80 | } 81 | ] 82 | }, 83 | "win": { 84 | "target": [ 85 | "nsis", 86 | "msi" 87 | ] 88 | }, 89 | "linux": { 90 | "target": [ 91 | "deb", 92 | "rpm", 93 | "AppImage" 94 | ], 95 | "category": "Development" 96 | }, 97 | "directories": { 98 | "buildResources": "resources", 99 | "output": "release" 100 | }, 101 | "publish": { 102 | "provider": "github", 103 | "owner": "sylar", 104 | "repo": "EasyRecorder", 105 | "private": false 106 | } 107 | }, 108 | "repository": { 109 | "type": "git", 110 | "url": "git+git@github.com:peilinok/EasyRecorder.git" 111 | }, 112 | "author": { 113 | "name": "Sylar", 114 | "email": "peilinok@gmail.com", 115 | "url": "https://github.com/peilinok/EasyRecorder" 116 | }, 117 | "license": "MIT", 118 | "bugs": { 119 | "url": "https://github.com/peilinok/EasyRecorder/issues" 120 | }, 121 | "keywords": [ 122 | "electron", 123 | "boilerplate", 124 | "react", 125 | "redux", 126 | "flow", 127 | "sass", 128 | "webpack", 129 | "hot", 130 | "reload" 131 | ], 132 | "homepage": "https://github.com/peilinok/EasyRecorder", 133 | "jest": { 134 | "testURL": "http://localhost/", 135 | "moduleNameMapper": { 136 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/internals/mocks/fileMock.js", 137 | "\\.(css|less|sass|scss)$": "identity-obj-proxy" 138 | }, 139 | "moduleFileExtensions": [ 140 | "js", 141 | "jsx", 142 | "json" 143 | ], 144 | "moduleDirectories": [ 145 | "node_modules", 146 | "app/node_modules" 147 | ], 148 | "transform": { 149 | "^.+\\.jsx?$": "babel-jest" 150 | }, 151 | "setupFiles": [ 152 | "./internals/scripts/CheckBuildsExist.js" 153 | ] 154 | }, 155 | "devDependencies": { 156 | "@babel/core": "^7.7.5", 157 | "@babel/plugin-proposal-class-properties": "^7.7.4", 158 | "@babel/plugin-proposal-decorators": "^7.7.4", 159 | "@babel/plugin-proposal-do-expressions": "^7.7.4", 160 | "@babel/plugin-proposal-export-default-from": "^7.7.4", 161 | "@babel/plugin-proposal-export-namespace-from": "^7.7.4", 162 | "@babel/plugin-proposal-function-bind": "^7.7.4", 163 | "@babel/plugin-proposal-function-sent": "^7.7.4", 164 | "@babel/plugin-proposal-json-strings": "^7.7.4", 165 | "@babel/plugin-proposal-logical-assignment-operators": "^7.7.4", 166 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.7.4", 167 | "@babel/plugin-proposal-numeric-separator": "^7.7.4", 168 | "@babel/plugin-proposal-optional-chaining": "^7.7.5", 169 | "@babel/plugin-proposal-pipeline-operator": "^7.7.4", 170 | "@babel/plugin-proposal-throw-expressions": "^7.7.4", 171 | "@babel/plugin-syntax-dynamic-import": "^7.7.4", 172 | "@babel/plugin-syntax-import-meta": "^7.7.4", 173 | "@babel/plugin-transform-react-constant-elements": "^7.7.4", 174 | "@babel/plugin-transform-react-inline-elements": "^7.7.4", 175 | "@babel/preset-env": "^7.7.6", 176 | "@babel/preset-flow": "^7.7.4", 177 | "@babel/preset-react": "^7.7.4", 178 | "@babel/register": "^7.7.4", 179 | "babel-core": "7.0.0-bridge.0", 180 | "babel-eslint": "^10.0.3", 181 | "babel-jest": "^24.9.0", 182 | "babel-loader": "^8.0.6", 183 | "babel-plugin-dev-expression": "^0.2.2", 184 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 185 | "chalk": "^3.0.0", 186 | "concurrently": "^5.0.1", 187 | "cross-env": "^6.0.3", 188 | "cross-spawn": "^7.0.1", 189 | "css-loader": "^3.3.2", 190 | "detect-port": "^1.3.0", 191 | "electron": "7.1.4", 192 | "electron-builder": "^22.2.0", 193 | "electron-devtools-installer": "^2.2.4", 194 | "electron-rebuild": "^1.8.8", 195 | "enzyme": "^3.7.0", 196 | "enzyme-adapter-react-16": "^1.7.0", 197 | "enzyme-to-json": "^3.3.4", 198 | "eslint": "^6.7.2", 199 | "eslint-config-airbnb": "^18.0.1", 200 | "eslint-config-erb": "^0.1.1", 201 | "eslint-config-prettier": "^6.6.0", 202 | "eslint-formatter-pretty": "^3.0.0", 203 | "eslint-import-resolver-webpack": "^0.12.0", 204 | "eslint-plugin-compat": "^3.3.0", 205 | "eslint-plugin-flowtype": "^4.5.2", 206 | "eslint-plugin-import": "^2.19.1", 207 | "eslint-plugin-jest": "^23.1.1", 208 | "eslint-plugin-jsx-a11y": "6.2.3", 209 | "eslint-plugin-prettier": "^3.1.1", 210 | "eslint-plugin-promise": "^4.2.1", 211 | "eslint-plugin-react": "^7.17.0", 212 | "eslint-plugin-testcafe": "^0.2.1", 213 | "fbjs-scripts": "^1.2.0", 214 | "file-loader": "^5.0.2", 215 | "flow-bin": "^0.113.0", 216 | "flow-runtime": "^0.17.0", 217 | "flow-typed": "^2.6.2", 218 | "husky": "^3.1.0", 219 | "identity-obj-proxy": "^3.0.0", 220 | "jest": "^24.9.0", 221 | "lint-staged": "^9.5.0", 222 | "mini-css-extract-plugin": "^0.8.0", 223 | "node-sass": "^4.13.0", 224 | "opencollective-postinstall": "^2.0.2", 225 | "optimize-css-assets-webpack-plugin": "^5.0.3", 226 | "prettier": "^1.19.1", 227 | "react-test-renderer": "^16.12.0", 228 | "redux-logger": "^3.0.6", 229 | "rimraf": "^3.0.0", 230 | "sass-loader": "^8.0.0", 231 | "sinon": "^7.5.0", 232 | "spectron": "^9.0.0", 233 | "style-loader": "^1.0.1", 234 | "stylelint": "^12.0.0", 235 | "stylelint-config-prettier": "^8.0.0", 236 | "stylelint-config-standard": "^19.0.0", 237 | "terser-webpack-plugin": "^2.3.0", 238 | "testcafe": "^1.6.1", 239 | "testcafe-browser-provider-electron": "^0.0.13", 240 | "testcafe-react-selectors": "^3.3.0", 241 | "url-loader": "^3.0.0", 242 | "webpack": "^4.41.2", 243 | "webpack-bundle-analyzer": "^3.6.0", 244 | "webpack-cli": "^3.3.10", 245 | "webpack-dev-server": "^3.9.0", 246 | "webpack-merge": "^4.2.2", 247 | "yarn": "^1.21.1" 248 | }, 249 | "dependencies": { 250 | "@fortawesome/fontawesome-free": "^5.12.0", 251 | "@hot-loader/react-dom": "^16.11.0", 252 | "antd": "^3.26.4", 253 | "babel-plugin-import": "^1.13.0", 254 | "classnames": "^2.2.6", 255 | "connected-react-router": "^6.6.1", 256 | "core-js": "^3.5.0", 257 | "devtron": "^1.4.0", 258 | "electron-debug": "^3.0.1", 259 | "electron-log": "^4.0.0", 260 | "electron-store": "^5.1.0", 261 | "electron-updater": "^4.2.0", 262 | "history": "^4.10.1", 263 | "moment": "^2.24.0", 264 | "react": "^16.12.0", 265 | "react-dom": "^16.12.0", 266 | "react-hot-loader": "^4.12.18", 267 | "react-redux": "^7.1.3", 268 | "react-router": "^5.1.2", 269 | "react-router-dom": "^5.1.2", 270 | "redux": "^4.0.4", 271 | "redux-thunk": "^2.3.0", 272 | "source-map-support": "^0.5.16" 273 | }, 274 | "devEngines": { 275 | "node": ">=7.x", 276 | "npm": ">=4.x", 277 | "yarn": ">=0.21.3" 278 | }, 279 | "browserslist": "electron 1.6", 280 | "husky": { 281 | "hooks": { 282 | "pre-commit": "lint-staged" 283 | } 284 | }, 285 | "ffmpeg_recorder": { 286 | "electron_version": "7.1.4", 287 | "platform": "win32", 288 | "runtime": "electron", 289 | "msvs_version": "2015", 290 | "debug": false, 291 | "silent": false 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.18.1 (2019.12.12) 2 | 3 | - Fix HMR env bug ([#2343](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2343)) 4 | - Bump all deps to latest semver 5 | - Bump to `electron@7` 6 | 7 | # 0.18.0 (2019.11.19) 8 | 9 | - Bump electron to `electron@6` (`electron@7` introduces breaking changes to testcafe end to end tests) 10 | - Revert back to [two `package.json` structure](https://www.electron.build/tutorials/two-package-structure) 11 | - Bump all deps to latest semver 12 | 13 | # 0.17.1 (2018.11.20) 14 | 15 | - Fix `yarn test-e2e` and testcafe for single package.json structure 16 | - Fixes incorrect path in `yarn start` script 17 | - Bumped deps 18 | - Bump g++ in travis 19 | - Change clone arguments to clone only master 20 | - Change babel config to target current electron version 21 | 22 | For full change list, see https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2021 23 | 24 | # 0.17.0 (2018.10.30) 25 | 26 | - upgraded to `babel@7` (thanks to @vikr01 🎉🎉🎉) 27 | - migrated from [two `package.json` structure](https://www.electron.build/tutorials/two-package-structure) (thanks to @HyperSprite!) 28 | - initial auto update support (experimental) 29 | - migrate from greenkeeper to [renovate](https://renovatebot.com) 30 | - added issue template 31 | - use `babel-preset-env` to target current electron version 32 | - add [opencollective](https://opencollective.com/electron-react-boilerplate-594) banner message display in postinstall script (help support ERB 🙏) 33 | - fix failing ci issues 34 | 35 | # 0.16.0 (2018.10.3) 36 | 37 | - removed unused dependencies 38 | - migrate from `react-redux-router` to `connect-react-router` 39 | - move webpack configs to `./webpack` dir 40 | - use `g++` on travis when testing linux 41 | - migrate from `spectron` to `testcafe` for e2e tests 42 | - add linting support for config styles 43 | - changed stylelint config 44 | - temporarily disabled flow in appveyor to make ci pass 45 | - added necessary infra to publish releases from ci 46 | 47 | # 0.15.0 (2018.8.25) 48 | 49 | - Performance: cache webpack uglify results 50 | - Feature: add start minimized feature 51 | - Feature: lint and fix styles with prettier and stylelint 52 | - Feature: add greenkeeper support 53 | 54 | # 0.14.0 (2018.5.24) 55 | 56 | - Improved CI timings 57 | - Migrated README commands to yarn from npm 58 | - Improved vscode config 59 | - Updated all dependencies to latest semver 60 | - Fix `electron-rebuild` script bug 61 | - Migrated to `mini-css-extract-plugin` from `extract-text-plugin` 62 | - Added `optimize-css-assets-webpack-plugin` 63 | - Run `prettier` on json, css, scss, and more filetypes 64 | 65 | # 0.13.3 (2018.5.24) 66 | 67 | - Add git precommit hook, when git commit will use `prettier` to format git add code 68 | - Add format code function in `lint-fix` npm script which can use `prettier` to format project js code 69 | 70 | # 0.13.2 (2018.1.31) 71 | 72 | - Hot Module Reload (HMR) fixes 73 | - Bumped all dependencies to latest semver 74 | - Prevent error propagation of `CheckNativeDeps` script 75 | 76 | # 0.13.1 (2018.1.13) 77 | 78 | - Hot Module Reload (HMR) fixes 79 | - Bumped all dependencies to latest semver 80 | - Fixed electron-rebuild script 81 | - Fixed tests scripts to run on all platforms 82 | - Skip redux logs in console in test ENV 83 | 84 | # 0.13.0 (2018.1.6) 85 | 86 | #### Additions 87 | 88 | - Add native dependencies check on postinstall 89 | - Updated all dependencies to latest semver 90 | 91 | # 0.12.0 (2017.7.8) 92 | 93 | #### Misc 94 | 95 | - Removed `babel-polyfill` 96 | - Renamed and alphabetized npm scripts 97 | 98 | #### Breaking 99 | 100 | - Changed node dev `__dirname` and `__filename` to node built in fn's (https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/1035) 101 | - Renamed `app/bundle.js` to `app/renderer.prod.js` for consistency 102 | - Renamed `dll/vendor.js` to `dll/renderer.dev.dll.js` for consistency 103 | 104 | #### Additions 105 | 106 | - Enable node_modules cache on CI 107 | 108 | # 0.11.2 (2017.5.1) 109 | 110 | Yay! Another patch release. This release mostly includes refactorings and router bug fixes. Huge thanks to @anthonyraymond! 111 | 112 | ⚠️ Windows electron builds are failing because of [this issue](https://github.com/electron/electron/issues/9321). This is not an issue with the boilerplate ⚠️ 113 | 114 | #### Breaking 115 | 116 | - **Renamed `./app/main.development.js` => `./app/main.{dev,prod}.js`:** [#963](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/963) 117 | 118 | #### Fixes 119 | 120 | - **Fixed reloading when not on `/` path:** [#958](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/958) [#949](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/949) 121 | 122 | #### Additions 123 | 124 | - **Added support for stylefmt:** [#960](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/960) 125 | 126 | # 0.11.1 (2017.4.23) 127 | 128 | You can now debug the production build with devtools like so: 129 | 130 | ``` 131 | DEBUG_PROD=true npm run package 132 | ``` 133 | 134 | 🎉🎉🎉 135 | 136 | #### Additions 137 | 138 | - **Added support for debugging production build:** [#fab245a](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/941/commits/fab245a077d02a09630f74270806c0c534a4ff95) 139 | 140 | #### Bug Fixes 141 | 142 | - **Fixed bug related to importing native dependencies:** [#933](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/933) 143 | 144 | #### Improvements 145 | 146 | - **Updated all deps to latest semver** 147 | 148 | # 0.11.0 (2017.4.19) 149 | 150 | Here's the most notable changes since `v0.10.0`. Its been about a year since a release has been pushed. Expect a new release to be published every 3-4 weeks. 151 | 152 | #### Breaking Changes 153 | 154 | - **Dropped support for node < 6** 155 | - **Refactored webpack config files** 156 | - **Migrate to two-package.json project structure** 157 | - **Updated all devDeps to latest semver** 158 | - **Migrated to Jest:** [#768](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/768) 159 | - **Migrated to `react-router@4`** 160 | - **Migrated to `electron-builder@4`** 161 | - **Migrated to `webpack@2`** 162 | - **Migrated to `react-hot-loader@3`** 163 | - **Changed default live reload server PORT to `1212` from `3000`** 164 | 165 | #### Additions 166 | 167 | - **Added support for Yarn:** [#451](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/451) 168 | - **Added support for Flow:** [#425](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/425) 169 | - **Added support for stylelint:** [#911](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/911) 170 | - **Added support for electron-builder:** [#876](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/876) 171 | - **Added optional support for SASS:** [#880](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/880) 172 | - **Added support for eslint-plugin-flowtype:** [#911](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/911) 173 | - **Added support for appveyor:** [#280](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/280) 174 | - **Added support for webpack dlls:** [#860](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/860) 175 | - **Route based code splitting:** [#884](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/884) 176 | - **Added support for Webpack Bundle Analyzer:** [#922](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/922) 177 | 178 | #### Improvements 179 | 180 | - **Parallelize renderer and main build processes when running `npm run build`** 181 | - **Dynamically generate electron app menu** 182 | - **Improved vscode integration:** [#856](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/856) 183 | 184 | #### Bug Fixes 185 | 186 | - **Fixed hot module replacement race condition bug:** [#917](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/917) [#920](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/920) 187 | 188 | # 0.10.0 (2016.4.18) 189 | 190 | #### Improvements 191 | 192 | - **Use Babel in main process with Webpack build:** [#201](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/201) 193 | - **Change targets to built-in support by webpack:** [#197](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/197) 194 | - **use es2015 syntax for webpack configs:** [#195](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/195) 195 | - **Open application when webcontent is loaded:** [#192](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/192) 196 | - **Upgraded dependencies** 197 | 198 | #### Bug fixed 199 | 200 | - **Fix `npm list electron-prebuilt` in package.js:** [#188](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/188) 201 | 202 | # 0.9.0 (2016.3.23) 203 | 204 | #### Improvements 205 | 206 | - **Added [redux-logger](https://github.com/fcomb/redux-logger)** 207 | - **Upgraded [react-router-redux](https://github.com/reactjs/react-router-redux) to v4** 208 | - **Upgraded dependencies** 209 | - **Added `npm run dev` command:** [#162](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/162) 210 | - **electron to v0.37.2** 211 | 212 | #### Breaking Changes 213 | 214 | - **css module as default:** [#154](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/154). 215 | - **set default NODE_ENV to production:** [#140](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/140) 216 | 217 | # 0.8.0 (2016.2.17) 218 | 219 | #### Bug fixed 220 | 221 | - **Fix lint errors** 222 | - **Fix Webpack publicPath for production builds**: [#119](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/119). 223 | - **package script now chooses correct OS icon extension** 224 | 225 | #### Improvements 226 | 227 | - **babel 6** 228 | - **Upgrade Dependencies** 229 | - **Enable CSS source maps** 230 | - **Add json-loader**: [#128](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/128). 231 | - **react-router 2.0 and react-router-redux 3.0** 232 | 233 | # 0.7.1 (2015.12.27) 234 | 235 | #### Bug fixed 236 | 237 | - **Fixed npm script on windows 10:** [#103](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/103). 238 | - **history and react-router version bump**: [#109](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/109), [#110](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/110). 239 | 240 | #### Improvements 241 | 242 | - **electron 0.36** 243 | 244 | # 0.7.0 (2015.12.16) 245 | 246 | #### Bug fixed 247 | 248 | - **Fixed process.env.NODE_ENV variable in webpack:** [#74](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/74). 249 | - **add missing object-assign**: [#76](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/76). 250 | - **packaging in npm@3:** [#77](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/77). 251 | - **compatibility in windows:** [#100](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/100). 252 | - **disable chrome debugger in production env:** [#102](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/102). 253 | 254 | #### Improvements 255 | 256 | - **redux** 257 | - **css-modules** 258 | - **upgrade to react-router 1.x** 259 | - **unit tests** 260 | - **e2e tests** 261 | - **travis-ci** 262 | - **upgrade to electron 0.35.x** 263 | - **use es2015** 264 | - **check dev engine for node and npm** 265 | 266 | # 0.6.5 (2015.11.7) 267 | 268 | #### Improvements 269 | 270 | - **Bump style-loader to 0.13** 271 | - **Bump css-loader to 0.22** 272 | 273 | # 0.6.4 (2015.10.27) 274 | 275 | #### Improvements 276 | 277 | - **Bump electron-debug to 0.3** 278 | 279 | # 0.6.3 (2015.10.26) 280 | 281 | #### Improvements 282 | 283 | - **Initialize ExtractTextPlugin once:** [#64](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/64). 284 | 285 | # 0.6.2 (2015.10.18) 286 | 287 | #### Bug fixed 288 | 289 | - **Babel plugins production env not be set properly:** [#57](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/57). 290 | 291 | # 0.6.1 (2015.10.17) 292 | 293 | #### Improvements 294 | 295 | - **Bump electron to v0.34.0** 296 | 297 | # 0.6.0 (2015.10.16) 298 | 299 | #### Breaking Changes 300 | 301 | - **From react-hot-loader to react-transform** 302 | 303 | # 0.5.2 (2015.10.15) 304 | 305 | #### Improvements 306 | 307 | - **Run tests with babel-register:** [#29](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/29). 308 | 309 | # 0.5.1 (2015.10.12) 310 | 311 | #### Bug fixed 312 | 313 | - **Fix #51:** use `path.join(__dirname` instead of `./`. 314 | 315 | # 0.5.0 (2015.10.11) 316 | 317 | #### Improvements 318 | 319 | - **Simplify webpack config** see [#50](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/50). 320 | 321 | #### Breaking Changes 322 | 323 | - **webpack configs** 324 | - **port changed:** changed default port from 2992 to 3000. 325 | - **npm scripts:** remove `start-dev` and `dev-server`. rename `hot-dev-server` to `hot-server`. 326 | 327 | # 0.4.3 (2015.9.22) 328 | 329 | #### Bug fixed 330 | 331 | - **Fix #45 zeromq crash:** bump version of `electron-prebuilt`. 332 | 333 | # 0.4.2 (2015.9.15) 334 | 335 | #### Bug fixed 336 | 337 | - **run start-hot breaks chrome refresh(CTRL+R) (#42)**: bump `electron-debug` to `0.2.1` 338 | 339 | # 0.4.1 (2015.9.11) 340 | 341 | #### Improvements 342 | 343 | - **use electron-prebuilt version for packaging (#33)** 344 | 345 | # 0.4.0 (2015.9.5) 346 | 347 | #### Improvements 348 | 349 | - **update dependencies** 350 | 351 | # 0.3.0 (2015.8.31) 352 | 353 | #### Improvements 354 | 355 | - **eslint-config-airbnb** 356 | 357 | # 0.2.10 (2015.8.27) 358 | 359 | #### Features 360 | 361 | - **custom placeholder icon** 362 | 363 | #### Improvements 364 | 365 | - **electron-renderer as target:** via [webpack-target-electron-renderer](https://github.com/chentsulin/webpack-target-electron-renderer) 366 | 367 | # 0.2.9 (2015.8.18) 368 | 369 | #### Bug fixed 370 | 371 | - **Fix hot-reload** 372 | 373 | # 0.2.8 (2015.8.13) 374 | 375 | #### Improvements 376 | 377 | - **bump electron-debug** 378 | - **babelrc** 379 | - **organize webpack scripts** 380 | 381 | # 0.2.7 (2015.7.9) 382 | 383 | #### Bug fixed 384 | 385 | - **defaultProps:** fix typos. 386 | 387 | # 0.2.6 (2015.7.3) 388 | 389 | #### Features 390 | 391 | - **menu** 392 | 393 | #### Bug fixed 394 | 395 | - **package.js:** include webpack build. 396 | 397 | # 0.2.5 (2015.7.1) 398 | 399 | #### Features 400 | 401 | - **NPM Script:** support multi-platform 402 | - **package:** `--all` option 403 | 404 | # 0.2.4 (2015.6.9) 405 | 406 | #### Bug fixed 407 | 408 | - **Eslint:** typo, [#17](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/17) and improve `.eslintrc` 409 | 410 | # 0.2.3 (2015.6.3) 411 | 412 | #### Features 413 | 414 | - **Package Version:** use latest release electron version as default 415 | - **Ignore Large peerDependencies** 416 | 417 | #### Bug fixed 418 | 419 | - **Npm Script:** typo, [#6](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/6) 420 | - **Missing css:** [#7](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/7) 421 | 422 | # 0.2.2 (2015.6.2) 423 | 424 | #### Features 425 | 426 | - **electron-debug** 427 | 428 | #### Bug fixed 429 | 430 | - **Webpack:** add `.json` and `.node` to extensions for imitating node require. 431 | - **Webpack:** set `node_modules` to externals for native module support. 432 | 433 | # 0.2.1 (2015.5.30) 434 | 435 | #### Bug fixed 436 | 437 | - **Webpack:** #1, change build target to `atom`. 438 | 439 | # 0.2.0 (2015.5.30) 440 | 441 | #### Features 442 | 443 | - **Ignore:** `test`, `tools`, `release` folder and devDependencies in `package.json`. 444 | - **Support asar** 445 | - **Support icon** 446 | 447 | # 0.1.0 (2015.5.27) 448 | 449 | #### Features 450 | 451 | - **Webpack:** babel, react-hot, ... 452 | - **Flux:** actions, api, components, containers, stores.. 453 | - **Package:** darwin (osx), linux and win32 (windows) platform. 454 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ### GNU GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | ### Preamble 12 | 13 | The GNU General Public License is a free, copyleft license for 14 | software and other kinds of works. 15 | 16 | The licenses for most software and other practical works are designed 17 | to take away your freedom to share and change the works. By contrast, 18 | the GNU General Public License is intended to guarantee your freedom 19 | to share and change all versions of a program--to make sure it remains 20 | free software for all its users. We, the Free Software Foundation, use 21 | the GNU General Public License for most of our software; it applies 22 | also to any other work released this way by its authors. You can apply 23 | it to your programs, too. 24 | 25 | When we speak of free software, we are referring to freedom, not 26 | price. Our General Public Licenses are designed to make sure that you 27 | have the freedom to distribute copies of free software (and charge for 28 | them if you wish), that you receive source code or can get it if you 29 | want it, that you can change the software or use pieces of it in new 30 | free programs, and that you know you can do these things. 31 | 32 | To protect your rights, we need to prevent others from denying you 33 | these rights or asking you to surrender the rights. Therefore, you 34 | have certain responsibilities if you distribute copies of the 35 | software, or if you modify it: responsibilities to respect the freedom 36 | of others. 37 | 38 | For example, if you distribute copies of such a program, whether 39 | gratis or for a fee, you must pass on to the recipients the same 40 | freedoms that you received. You must make sure that they, too, receive 41 | or can get the source code. And you must show them these terms so they 42 | know their rights. 43 | 44 | Developers that use the GNU GPL protect your rights with two steps: 45 | (1) assert copyright on the software, and (2) offer you this License 46 | giving you legal permission to copy, distribute and/or modify it. 47 | 48 | For the developers' and authors' protection, the GPL clearly explains 49 | that there is no warranty for this free software. For both users' and 50 | authors' sake, the GPL requires that modified versions be marked as 51 | changed, so that their problems will not be attributed erroneously to 52 | authors of previous versions. 53 | 54 | Some devices are designed to deny users access to install or run 55 | modified versions of the software inside them, although the 56 | manufacturer can do so. This is fundamentally incompatible with the 57 | aim of protecting users' freedom to change the software. The 58 | systematic pattern of such abuse occurs in the area of products for 59 | individuals to use, which is precisely where it is most unacceptable. 60 | Therefore, we have designed this version of the GPL to prohibit the 61 | practice for those products. If such problems arise substantially in 62 | other domains, we stand ready to extend this provision to those 63 | domains in future versions of the GPL, as needed to protect the 64 | freedom of users. 65 | 66 | Finally, every program is threatened constantly by software patents. 67 | States should not allow patents to restrict development and use of 68 | software on general-purpose computers, but in those that do, we wish 69 | to avoid the special danger that patents applied to a free program 70 | could make it effectively proprietary. To prevent this, the GPL 71 | assures that patents cannot be used to render the program non-free. 72 | 73 | The precise terms and conditions for copying, distribution and 74 | modification follow. 75 | 76 | ### TERMS AND CONDITIONS 77 | 78 | #### 0. Definitions. 79 | 80 | "This License" refers to version 3 of the GNU General Public License. 81 | 82 | "Copyright" also means copyright-like laws that apply to other kinds 83 | of works, such as semiconductor masks. 84 | 85 | "The Program" refers to any copyrightable work licensed under this 86 | License. Each licensee is addressed as "you". "Licensees" and 87 | "recipients" may be individuals or organizations. 88 | 89 | To "modify" a work means to copy from or adapt all or part of the work 90 | in a fashion requiring copyright permission, other than the making of 91 | an exact copy. The resulting work is called a "modified version" of 92 | the earlier work or a work "based on" the earlier work. 93 | 94 | A "covered work" means either the unmodified Program or a work based 95 | on the Program. 96 | 97 | To "propagate" a work means to do anything with it that, without 98 | permission, would make you directly or secondarily liable for 99 | infringement under applicable copyright law, except executing it on a 100 | computer or modifying a private copy. Propagation includes copying, 101 | distribution (with or without modification), making available to the 102 | public, and in some countries other activities as well. 103 | 104 | To "convey" a work means any kind of propagation that enables other 105 | parties to make or receive copies. Mere interaction with a user 106 | through a computer network, with no transfer of a copy, is not 107 | conveying. 108 | 109 | An interactive user interface displays "Appropriate Legal Notices" to 110 | the extent that it includes a convenient and prominently visible 111 | feature that (1) displays an appropriate copyright notice, and (2) 112 | tells the user that there is no warranty for the work (except to the 113 | extent that warranties are provided), that licensees may convey the 114 | work under this License, and how to view a copy of this License. If 115 | the interface presents a list of user commands or options, such as a 116 | menu, a prominent item in the list meets this criterion. 117 | 118 | #### 1. Source Code. 119 | 120 | The "source code" for a work means the preferred form of the work for 121 | making modifications to it. "Object code" means any non-source form of 122 | a work. 123 | 124 | A "Standard Interface" means an interface that either is an official 125 | standard defined by a recognized standards body, or, in the case of 126 | interfaces specified for a particular programming language, one that 127 | is widely used among developers working in that language. 128 | 129 | The "System Libraries" of an executable work include anything, other 130 | than the work as a whole, that (a) is included in the normal form of 131 | packaging a Major Component, but which is not part of that Major 132 | Component, and (b) serves only to enable use of the work with that 133 | Major Component, or to implement a Standard Interface for which an 134 | implementation is available to the public in source code form. A 135 | "Major Component", in this context, means a major essential component 136 | (kernel, window system, and so on) of the specific operating system 137 | (if any) on which the executable work runs, or a compiler used to 138 | produce the work, or an object code interpreter used to run it. 139 | 140 | The "Corresponding Source" for a work in object code form means all 141 | the source code needed to generate, install, and (for an executable 142 | work) run the object code and to modify the work, including scripts to 143 | control those activities. However, it does not include the work's 144 | System Libraries, or general-purpose tools or generally available free 145 | programs which are used unmodified in performing those activities but 146 | which are not part of the work. For example, Corresponding Source 147 | includes interface definition files associated with source files for 148 | the work, and the source code for shared libraries and dynamically 149 | linked subprograms that the work is specifically designed to require, 150 | such as by intimate data communication or control flow between those 151 | subprograms and other parts of the work. 152 | 153 | The Corresponding Source need not include anything that users can 154 | regenerate automatically from other parts of the Corresponding Source. 155 | 156 | The Corresponding Source for a work in source code form is that same 157 | work. 158 | 159 | #### 2. Basic Permissions. 160 | 161 | All rights granted under this License are granted for the term of 162 | copyright on the Program, and are irrevocable provided the stated 163 | conditions are met. This License explicitly affirms your unlimited 164 | permission to run the unmodified Program. The output from running a 165 | covered work is covered by this License only if the output, given its 166 | content, constitutes a covered work. This License acknowledges your 167 | rights of fair use or other equivalent, as provided by copyright law. 168 | 169 | You may make, run and propagate covered works that you do not convey, 170 | without conditions so long as your license otherwise remains in force. 171 | You may convey covered works to others for the sole purpose of having 172 | them make modifications exclusively for you, or provide you with 173 | facilities for running those works, provided that you comply with the 174 | terms of this License in conveying all material for which you do not 175 | control copyright. Those thus making or running the covered works for 176 | you must do so exclusively on your behalf, under your direction and 177 | control, on terms that prohibit them from making any copies of your 178 | copyrighted material outside their relationship with you. 179 | 180 | Conveying under any other circumstances is permitted solely under the 181 | conditions stated below. Sublicensing is not allowed; section 10 makes 182 | it unnecessary. 183 | 184 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 185 | 186 | No covered work shall be deemed part of an effective technological 187 | measure under any applicable law fulfilling obligations under article 188 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 189 | similar laws prohibiting or restricting circumvention of such 190 | measures. 191 | 192 | When you convey a covered work, you waive any legal power to forbid 193 | circumvention of technological measures to the extent such 194 | circumvention is effected by exercising rights under this License with 195 | respect to the covered work, and you disclaim any intention to limit 196 | operation or modification of the work as a means of enforcing, against 197 | the work's users, your or third parties' legal rights to forbid 198 | circumvention of technological measures. 199 | 200 | #### 4. Conveying Verbatim Copies. 201 | 202 | You may convey verbatim copies of the Program's source code as you 203 | receive it, in any medium, provided that you conspicuously and 204 | appropriately publish on each copy an appropriate copyright notice; 205 | keep intact all notices stating that this License and any 206 | non-permissive terms added in accord with section 7 apply to the code; 207 | keep intact all notices of the absence of any warranty; and give all 208 | recipients a copy of this License along with the Program. 209 | 210 | You may charge any price or no price for each copy that you convey, 211 | and you may offer support or warranty protection for a fee. 212 | 213 | #### 5. Conveying Modified Source Versions. 214 | 215 | You may convey a work based on the Program, or the modifications to 216 | produce it from the Program, in the form of source code under the 217 | terms of section 4, provided that you also meet all of these 218 | conditions: 219 | 220 | - a) The work must carry prominent notices stating that you modified 221 | it, and giving a relevant date. 222 | - b) The work must carry prominent notices stating that it is 223 | released under this License and any conditions added under 224 | section 7. This requirement modifies the requirement in section 4 225 | to "keep intact all notices". 226 | - c) You must license the entire work, as a whole, under this 227 | License to anyone who comes into possession of a copy. This 228 | License will therefore apply, along with any applicable section 7 229 | additional terms, to the whole of the work, and all its parts, 230 | regardless of how they are packaged. This License gives no 231 | permission to license the work in any other way, but it does not 232 | invalidate such permission if you have separately received it. 233 | - d) If the work has interactive user interfaces, each must display 234 | Appropriate Legal Notices; however, if the Program has interactive 235 | interfaces that do not display Appropriate Legal Notices, your 236 | work need not make them do so. 237 | 238 | A compilation of a covered work with other separate and independent 239 | works, which are not by their nature extensions of the covered work, 240 | and which are not combined with it such as to form a larger program, 241 | in or on a volume of a storage or distribution medium, is called an 242 | "aggregate" if the compilation and its resulting copyright are not 243 | used to limit the access or legal rights of the compilation's users 244 | beyond what the individual works permit. Inclusion of a covered work 245 | in an aggregate does not cause this License to apply to the other 246 | parts of the aggregate. 247 | 248 | #### 6. Conveying Non-Source Forms. 249 | 250 | You may convey a covered work in object code form under the terms of 251 | sections 4 and 5, provided that you also convey the machine-readable 252 | Corresponding Source under the terms of this License, in one of these 253 | ways: 254 | 255 | - a) Convey the object code in, or embodied in, a physical product 256 | (including a physical distribution medium), accompanied by the 257 | Corresponding Source fixed on a durable physical medium 258 | customarily used for software interchange. 259 | - b) Convey the object code in, or embodied in, a physical product 260 | (including a physical distribution medium), accompanied by a 261 | written offer, valid for at least three years and valid for as 262 | long as you offer spare parts or customer support for that product 263 | model, to give anyone who possesses the object code either (1) a 264 | copy of the Corresponding Source for all the software in the 265 | product that is covered by this License, on a durable physical 266 | medium customarily used for software interchange, for a price no 267 | more than your reasonable cost of physically performing this 268 | conveying of source, or (2) access to copy the Corresponding 269 | Source from a network server at no charge. 270 | - c) Convey individual copies of the object code with a copy of the 271 | written offer to provide the Corresponding Source. This 272 | alternative is allowed only occasionally and noncommercially, and 273 | only if you received the object code with such an offer, in accord 274 | with subsection 6b. 275 | - d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | - e) Convey the object code using peer-to-peer transmission, 288 | provided you inform other peers where the object code and 289 | Corresponding Source of the work are being offered to the general 290 | public at no charge under subsection 6d. 291 | 292 | A separable portion of the object code, whose source code is excluded 293 | from the Corresponding Source as a System Library, need not be 294 | included in conveying the object code work. 295 | 296 | A "User Product" is either (1) a "consumer product", which means any 297 | tangible personal property which is normally used for personal, 298 | family, or household purposes, or (2) anything designed or sold for 299 | incorporation into a dwelling. In determining whether a product is a 300 | consumer product, doubtful cases shall be resolved in favor of 301 | coverage. For a particular product received by a particular user, 302 | "normally used" refers to a typical or common use of that class of 303 | product, regardless of the status of the particular user or of the way 304 | in which the particular user actually uses, or expects or is expected 305 | to use, the product. A product is a consumer product regardless of 306 | whether the product has substantial commercial, industrial or 307 | non-consumer uses, unless such uses represent the only significant 308 | mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to 312 | install and execute modified versions of a covered work in that User 313 | Product from a modified version of its Corresponding Source. The 314 | information must suffice to ensure that the continued functioning of 315 | the modified object code is in no case prevented or interfered with 316 | solely because modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or 331 | updates for a work that has been modified or installed by the 332 | recipient, or for the User Product in which it has been modified or 333 | installed. Access to a network may be denied when the modification 334 | itself materially and adversely affects the operation of the network 335 | or violates the rules and protocols for communication across the 336 | network. 337 | 338 | Corresponding Source conveyed, and Installation Information provided, 339 | in accord with this section must be in a format that is publicly 340 | documented (and with an implementation available to the public in 341 | source code form), and must require no special password or key for 342 | unpacking, reading or copying. 343 | 344 | #### 7. Additional Terms. 345 | 346 | "Additional permissions" are terms that supplement the terms of this 347 | License by making exceptions from one or more of its conditions. 348 | Additional permissions that are applicable to the entire Program shall 349 | be treated as though they were included in this License, to the extent 350 | that they are valid under applicable law. If additional permissions 351 | apply only to part of the Program, that part may be used separately 352 | under those permissions, but the entire Program remains governed by 353 | this License without regard to the additional permissions. 354 | 355 | When you convey a copy of a covered work, you may at your option 356 | remove any additional permissions from that copy, or from any part of 357 | it. (Additional permissions may be written to require their own 358 | removal in certain cases when you modify the work.) You may place 359 | additional permissions on material, added by you to a covered work, 360 | for which you have or can give appropriate copyright permission. 361 | 362 | Notwithstanding any other provision of this License, for material you 363 | add to a covered work, you may (if authorized by the copyright holders 364 | of that material) supplement the terms of this License with terms: 365 | 366 | - a) Disclaiming warranty or limiting liability differently from the 367 | terms of sections 15 and 16 of this License; or 368 | - b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | - c) Prohibiting misrepresentation of the origin of that material, 372 | or requiring that modified versions of such material be marked in 373 | reasonable ways as different from the original version; or 374 | - d) Limiting the use for publicity purposes of names of licensors 375 | or authors of the material; or 376 | - e) Declining to grant rights under trademark law for use of some 377 | trade names, trademarks, or service marks; or 378 | - f) Requiring indemnification of licensors and authors of that 379 | material by anyone who conveys the material (or modified versions 380 | of it) with contractual assumptions of liability to the recipient, 381 | for any liability that these contractual assumptions directly 382 | impose on those licensors and authors. 383 | 384 | All other non-permissive additional terms are considered "further 385 | restrictions" within the meaning of section 10. If the Program as you 386 | received it, or any part of it, contains a notice stating that it is 387 | governed by this License along with a term that is a further 388 | restriction, you may remove that term. If a license document contains 389 | a further restriction but permits relicensing or conveying under this 390 | License, you may add to a covered work material governed by the terms 391 | of that license document, provided that the further restriction does 392 | not survive such relicensing or conveying. 393 | 394 | If you add terms to a covered work in accord with this section, you 395 | must place, in the relevant source files, a statement of the 396 | additional terms that apply to those files, or a notice indicating 397 | where to find the applicable terms. 398 | 399 | Additional terms, permissive or non-permissive, may be stated in the 400 | form of a separately written license, or stated as exceptions; the 401 | above requirements apply either way. 402 | 403 | #### 8. Termination. 404 | 405 | You may not propagate or modify a covered work except as expressly 406 | provided under this License. Any attempt otherwise to propagate or 407 | modify it is void, and will automatically terminate your rights under 408 | this License (including any patent licenses granted under the third 409 | paragraph of section 11). 410 | 411 | However, if you cease all violation of this License, then your license 412 | from a particular copyright holder is reinstated (a) provisionally, 413 | unless and until the copyright holder explicitly and finally 414 | terminates your license, and (b) permanently, if the copyright holder 415 | fails to notify you of the violation by some reasonable means prior to 416 | 60 days after the cessation. 417 | 418 | Moreover, your license from a particular copyright holder is 419 | reinstated permanently if the copyright holder notifies you of the 420 | violation by some reasonable means, this is the first time you have 421 | received notice of violation of this License (for any work) from that 422 | copyright holder, and you cure the violation prior to 30 days after 423 | your receipt of the notice. 424 | 425 | Termination of your rights under this section does not terminate the 426 | licenses of parties who have received copies or rights from you under 427 | this License. If your rights have been terminated and not permanently 428 | reinstated, you do not qualify to receive new licenses for the same 429 | material under section 10. 430 | 431 | #### 9. Acceptance Not Required for Having Copies. 432 | 433 | You are not required to accept this License in order to receive or run 434 | a copy of the Program. Ancillary propagation of a covered work 435 | occurring solely as a consequence of using peer-to-peer transmission 436 | to receive a copy likewise does not require acceptance. However, 437 | nothing other than this License grants you permission to propagate or 438 | modify any covered work. These actions infringe copyright if you do 439 | not accept this License. Therefore, by modifying or propagating a 440 | covered work, you indicate your acceptance of this License to do so. 441 | 442 | #### 10. Automatic Licensing of Downstream Recipients. 443 | 444 | Each time you convey a covered work, the recipient automatically 445 | receives a license from the original licensors, to run, modify and 446 | propagate that work, subject to this License. You are not responsible 447 | for enforcing compliance by third parties with this License. 448 | 449 | An "entity transaction" is a transaction transferring control of an 450 | organization, or substantially all assets of one, or subdividing an 451 | organization, or merging organizations. If propagation of a covered 452 | work results from an entity transaction, each party to that 453 | transaction who receives a copy of the work also receives whatever 454 | licenses to the work the party's predecessor in interest had or could 455 | give under the previous paragraph, plus a right to possession of the 456 | Corresponding Source of the work from the predecessor in interest, if 457 | the predecessor has it or can get it with reasonable efforts. 458 | 459 | You may not impose any further restrictions on the exercise of the 460 | rights granted or affirmed under this License. For example, you may 461 | not impose a license fee, royalty, or other charge for exercise of 462 | rights granted under this License, and you may not initiate litigation 463 | (including a cross-claim or counterclaim in a lawsuit) alleging that 464 | any patent claim is infringed by making, using, selling, offering for 465 | sale, or importing the Program or any portion of it. 466 | 467 | #### 11. Patents. 468 | 469 | A "contributor" is a copyright holder who authorizes use under this 470 | License of the Program or a work on which the Program is based. The 471 | work thus licensed is called the contributor's "contributor version". 472 | 473 | A contributor's "essential patent claims" are all patent claims owned 474 | or controlled by the contributor, whether already acquired or 475 | hereafter acquired, that would be infringed by some manner, permitted 476 | by this License, of making, using, or selling its contributor version, 477 | but do not include claims that would be infringed only as a 478 | consequence of further modification of the contributor version. For 479 | purposes of this definition, "control" includes the right to grant 480 | patent sublicenses in a manner consistent with the requirements of 481 | this License. 482 | 483 | Each contributor grants you a non-exclusive, worldwide, royalty-free 484 | patent license under the contributor's essential patent claims, to 485 | make, use, sell, offer for sale, import and otherwise run, modify and 486 | propagate the contents of its contributor version. 487 | 488 | In the following three paragraphs, a "patent license" is any express 489 | agreement or commitment, however denominated, not to enforce a patent 490 | (such as an express permission to practice a patent or covenant not to 491 | sue for patent infringement). To "grant" such a patent license to a 492 | party means to make such an agreement or commitment not to enforce a 493 | patent against the party. 494 | 495 | If you convey a covered work, knowingly relying on a patent license, 496 | and the Corresponding Source of the work is not available for anyone 497 | to copy, free of charge and under the terms of this License, through a 498 | publicly available network server or other readily accessible means, 499 | then you must either (1) cause the Corresponding Source to be so 500 | available, or (2) arrange to deprive yourself of the benefit of the 501 | patent license for this particular work, or (3) arrange, in a manner 502 | consistent with the requirements of this License, to extend the patent 503 | license to downstream recipients. "Knowingly relying" means you have 504 | actual knowledge that, but for the patent license, your conveying the 505 | covered work in a country, or your recipient's use of the covered work 506 | in a country, would infringe one or more identifiable patents in that 507 | country that you have reason to believe are valid. 508 | 509 | If, pursuant to or in connection with a single transaction or 510 | arrangement, you convey, or propagate by procuring conveyance of, a 511 | covered work, and grant a patent license to some of the parties 512 | receiving the covered work authorizing them to use, propagate, modify 513 | or convey a specific copy of the covered work, then the patent license 514 | you grant is automatically extended to all recipients of the covered 515 | work and works based on it. 516 | 517 | A patent license is "discriminatory" if it does not include within the 518 | scope of its coverage, prohibits the exercise of, or is conditioned on 519 | the non-exercise of one or more of the rights that are specifically 520 | granted under this License. You may not convey a covered work if you 521 | are a party to an arrangement with a third party that is in the 522 | business of distributing software, under which you make payment to the 523 | third party based on the extent of your activity of conveying the 524 | work, and under which the third party grants, to any of the parties 525 | who would receive the covered work from you, a discriminatory patent 526 | license (a) in connection with copies of the covered work conveyed by 527 | you (or copies made from those copies), or (b) primarily for and in 528 | connection with specific products or compilations that contain the 529 | covered work, unless you entered into that arrangement, or that patent 530 | license was granted, prior to 28 March 2007. 531 | 532 | Nothing in this License shall be construed as excluding or limiting 533 | any implied license or other defenses to infringement that may 534 | otherwise be available to you under applicable patent law. 535 | 536 | #### 12. No Surrender of Others' Freedom. 537 | 538 | If conditions are imposed on you (whether by court order, agreement or 539 | otherwise) that contradict the conditions of this License, they do not 540 | excuse you from the conditions of this License. If you cannot convey a 541 | covered work so as to satisfy simultaneously your obligations under 542 | this License and any other pertinent obligations, then as a 543 | consequence you may not convey it at all. For example, if you agree to 544 | terms that obligate you to collect a royalty for further conveying 545 | from those to whom you convey the Program, the only way you could 546 | satisfy both those terms and this License would be to refrain entirely 547 | from conveying the Program. 548 | 549 | #### 13. Use with the GNU Affero General Public License. 550 | 551 | Notwithstanding any other provision of this License, you have 552 | permission to link or combine any covered work with a work licensed 553 | under version 3 of the GNU Affero General Public License into a single 554 | combined work, and to convey the resulting work. The terms of this 555 | License will continue to apply to the part which is the covered work, 556 | but the special requirements of the GNU Affero General Public License, 557 | section 13, concerning interaction through a network will apply to the 558 | combination as such. 559 | 560 | #### 14. Revised Versions of this License. 561 | 562 | The Free Software Foundation may publish revised and/or new versions 563 | of the GNU General Public License from time to time. Such new versions 564 | will be similar in spirit to the present version, but may differ in 565 | detail to address new problems or concerns. 566 | 567 | Each version is given a distinguishing version number. If the Program 568 | specifies that a certain numbered version of the GNU General Public 569 | License "or any later version" applies to it, you have the option of 570 | following the terms and conditions either of that numbered version or 571 | of any later version published by the Free Software Foundation. If the 572 | Program does not specify a version number of the GNU General Public 573 | License, you may choose any version ever published by the Free 574 | Software Foundation. 575 | 576 | If the Program specifies that a proxy can decide which future versions 577 | of the GNU General Public License can be used, that proxy's public 578 | statement of acceptance of a version permanently authorizes you to 579 | choose that version for the Program. 580 | 581 | Later license versions may give you additional or different 582 | permissions. However, no additional obligations are imposed on any 583 | author or copyright holder as a result of your choosing to follow a 584 | later version. 585 | 586 | #### 15. Disclaimer of Warranty. 587 | 588 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 589 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 590 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 591 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 592 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 593 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 594 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 595 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 596 | CORRECTION. 597 | 598 | #### 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 602 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 603 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 604 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 605 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 606 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 607 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 608 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 609 | 610 | #### 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | ### How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these 626 | terms. 627 | 628 | To do so, attach the following notices to the program. It is safest to 629 | attach them to the start of each source file to most effectively state 630 | the exclusion of warranty; and each file should have at least the 631 | "copyright" line and a pointer to where the full notice is found. 632 | 633 | 634 | Copyright (C) 635 | 636 | This program is free software: you can redistribute it and/or modify 637 | it under the terms of the GNU General Public License as published by 638 | the Free Software Foundation, either version 3 of the License, or 639 | (at your option) any later version. 640 | 641 | This program is distributed in the hope that it will be useful, 642 | but WITHOUT ANY WARRANTY; without even the implied warranty of 643 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 644 | GNU General Public License for more details. 645 | 646 | You should have received a copy of the GNU General Public License 647 | along with this program. If not, see . 648 | 649 | Also add information on how to contact you by electronic and paper 650 | mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands \`show w' and \`show c' should show the 661 | appropriate parts of the General Public License. Of course, your 662 | program's commands might be different; for a GUI interface, you would 663 | use an "about box". 664 | 665 | You should also get your employer (if you work as a programmer) or 666 | school, if any, to sign a "copyright disclaimer" for the program, if 667 | necessary. For more information on this, and how to apply and follow 668 | the GNU GPL, see . 669 | 670 | The GNU General Public License does not permit incorporating your 671 | program into proprietary programs. If your program is a subroutine 672 | library, you may consider it more useful to permit linking proprietary 673 | applications with the library. If this is what you want to do, use the 674 | GNU Lesser General Public License instead of this License. But first, 675 | please read . 676 | --------------------------------------------------------------------------------