Repository: phobal/ivideo Branch: master Commit: e9a26b66177a Files: 68 Total size: 76.3 KB Directory structure: gitextract_9m4e08g_/ ├── .babelrc ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .stylelintrc ├── .travis.yml ├── .vscode/ │ └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app/ │ ├── .eslintrc │ ├── actions/ │ │ └── source.js │ ├── app.global.css │ ├── app.html │ ├── app.icns │ ├── components/ │ │ └── .githook │ ├── containers/ │ │ ├── App.js │ │ ├── Channel.js │ │ ├── Frame.js │ │ ├── Root.js │ │ ├── ToolBar.js │ │ └── Video.js │ ├── index.js │ ├── main.dev.js │ ├── menu.js │ ├── package.json │ ├── reducers/ │ │ ├── index.js │ │ └── source.js │ ├── routes.js │ ├── store/ │ │ ├── configureStore.dev.js │ │ ├── configureStore.js │ │ └── configureStore.prod.js │ └── utils/ │ ├── .gitkeep │ └── fetch.js ├── appveyor.yml ├── flow-typed/ │ └── module_vx.x.x.js ├── internals/ │ ├── flow/ │ │ ├── CSSModule.js.flow │ │ └── WebpackAsset.js.flow │ ├── mocks/ │ │ └── fileMock.js │ └── scripts/ │ ├── CheckBuiltsExist.js │ ├── CheckNativeDep.js │ ├── CheckNodeEnv.js │ ├── CheckPortInUse.js │ ├── ElectronRebuild.js │ └── RunTests.js ├── package.json ├── resources/ │ ├── icon.icns │ └── viplist.json ├── test/ │ ├── .eslintrc │ ├── actions/ │ │ ├── __snapshots__/ │ │ │ └── counter.spec.js.snap │ │ └── counter.spec.js │ ├── components/ │ │ ├── Counter.spec.js │ │ └── __snapshots__/ │ │ └── Counter.spec.js.snap │ ├── containers/ │ │ └── CounterPage.spec.js │ ├── e2e/ │ │ └── e2e.spec.js │ ├── example.js │ └── reducers/ │ ├── __snapshots__/ │ │ └── counter.spec.js.snap │ └── counter.spec.js ├── webpack.config.base.js ├── webpack.config.eslint.js ├── webpack.config.main.prod.js ├── webpack.config.renderer.dev.dll.js ├── webpack.config.renderer.dev.js └── webpack.config.renderer.prod.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ ["env", { "targets": { "node": 7 }, "useBuiltIns": true }], "stage-0", "react" ], "plugins": ["add-module-exports", ["import", { "libraryName": "antd", "style": "css" } ] ], "env": { "production": { "presets": ["react-optimize"], "plugins": ["dev-expression"] }, "development": { "plugins": [ "transform-class-properties", "transform-es2015-classes", ["flow-runtime", { "assert": true, "annotate": true }] ] } } } ================================================ FILE: .dockerignore ================================================ # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release .eslintcache # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules app/node_modules # OSX .DS_Store # flow-typed flow-typed/npm/* !flow-typed/npm/module_vx.x.x.js # App packaged release app/main.prod.js app/main.prod.js.map app/renderer.prod.js app/renderer.prod.js.map app/style.css app/style.css.map dist dll main.js main.js.map .idea npm-debug.log.* .*.dockerfile ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .eslintignore ================================================ # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release .eslintcache # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules app/node_modules # OSX .DS_Store # flow-typed flow-typed/npm/* !flow-typed/npm/module_vx.x.x.js # App packaged release app/main.prod.js app/main.prod.js.map app/renderer.prod.js app/renderer.prod.js.map app/style.css app/style.css.map dist dll main.js main.js.map .idea npm-debug.log.* __snapshots__ ================================================ FILE: .eslintrc ================================================ { "parser": "babel-eslint", "parserOptions": { "sourceType": "module", "allowImportExportEverywhere": true }, "extends": "airbnb", "env": { "browser": true, "node": true }, "rules": { "arrow-parens": ["off"], "compat/compat": "error", "consistent-return": "off", "comma-dangle": "off", "generator-star-spacing": "off", "import/no-unresolved": "error", "import/no-extraneous-dependencies": "off", "jsx-a11y/anchor-is-valid": "off", "no-console": "off", "no-use-before-define": "off", "no-multi-assign": "off", "promise/param-names": "error", "promise/always-return": "error", "promise/catch-or-return": "error", "promise/no-native": "off", "react/sort-comp": ["error", { "order": ["type-annotations", "static-methods", "lifecycle", "everything-else", "render"] }], "react/jsx-no-bind": "off", "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }], "react/prefer-stateless-function": "off" }, "plugins": [ "flowtype", "import", "promise", "compat", "react" ], "settings": { "import/resolver": { "webpack": { "config": "webpack.config.eslint.js" } } } } ================================================ FILE: .flowconfig ================================================ [ignore] /node_modules/* /app/main.prod.js /app/main.prod.js.map /app/dist/.* /resources/.* /release/.* /dll/.* /release/.* /git/.* [include] [libs] [options] esproposal.class_static_fields=enable esproposal.class_instance_fields=enable esproposal.export_star_as=enable module.name_mapper.extension='css' -> '/internals/flow/CSSModule.js.flow' module.name_mapper.extension='styl' -> '/internals/flow/CSSModule.js.flow' module.name_mapper.extension='scss' -> '/internals/flow/CSSModule.js.flow' module.name_mapper.extension='png' -> '/internals/flow/WebpackAsset.js.flow' module.name_mapper.extension='jpg' -> '/internals/flow/WebpackAsset.js.flow' suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe suppress_comment=\\(.\\|\n\\)*\\$FlowIssue ================================================ FILE: .gitattributes ================================================ * text eol=lf *.png binary *.jpg binary *.jpeg binary *.ico binary *.icns binary ================================================ FILE: .gitignore ================================================ # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release .eslintcache # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules app/node_modules # OSX .DS_Store # flow-typed flow-typed/npm/* !flow-typed/npm/module_vx.x.x.js # App packaged release app/main.prod.js app/main.prod.js.map app/renderer.prod.js app/renderer.prod.js.map app/style.css app/style.css.map dist dll main.js main.js.map .idea npm-debug.log.* ================================================ FILE: .stylelintrc ================================================ { "extends": "stylelint-config-standard" } ================================================ FILE: .travis.yml ================================================ sudo: true language: node_js node_js: - 8 - 7 cache: yarn: true directories: - node_modules - app/node_modules addons: apt: sources: - ubuntu-toolchain-r-test packages: - g++-4.8 - icnsutils - graphicsmagick - xz-utils - xorriso install: - export CXX="g++-4.8" - yarn - cd app && yarn && cd .. - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16" before_script: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start & - sleep 3 script: - node --version - yarn lint - yarn package - yarn test - yarn test-e2e ================================================ FILE: .vscode/settings.json ================================================ { "javascript.validate.enable": false, "flow.useNPMPackagedFlow": true, "search.exclude": { ".git": true, ".eslintcache": true, "app/dist": true, "app/main.prod.js": true, "app/main.prod.js.map": true, "bower_components": true, "dll": true, "flow-typed": true, "release": true, "node_modules": true, "npm-debug.log.*": true, "test/**/__snapshots__": true, "yarn.lock": true } } ================================================ FILE: CHANGELOG.md ================================================ * 2018.5.5 修改页面样式,当全屏时隐藏掉左侧菜单栏和顶部接口切换栏 * 2018.07.13 修改线路下拉框不能出现滚动条的 bug ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-present C. T. Lin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ### i视频 #### 产品介绍 > 基于 Electron 开发的跨平台客户端版本的视频播放器,该播放器包括国内主流视频平台视频资源,你不用去单独下载各个平台的客户端,只需要使用这一个客户端就能查看所有平台的视频,并且内置了各大视频网站 VIP 资源。 #### 使用方法 1. 下载客户端 * [Mac](https://github.com/phobal/ivideo/releases/download/v1.1.4/ivideo-1.1.4.dmg.zip) * [Windows](https://github.com/phobal/ivideo/releases/download/v1.1.4/ivideo.Setup.1.1.4.exe.zip) * [Linux](https://github.com/phobal/ivideo/releases/download/1.0.0/linux-unpacked.v1.0.0.zip) 2. 选择视频资源 比方说看腾讯视频上的 VIP 才能看的《下一站,别离》 ![](./resources/showcase01.jpg) 点击进去以后提示需要开通VIP才能看 ![](./resources/showcase02.jpg) 3. 选择资源播放接口 ![](./resources/showcase03.jpg) 点击【确定】按钮就可以播放了,如果遇到无法播放的情况,请多换几条线路试试 ![](./resources/showcase04.jpg) ### 技术栈 * Electron * React * Redux ### 如何启动 > node version >= 7.6 1. clone 项目到本地 ``` bash git clone https://github.com/phobal/ivideo.git ``` 2. 进入项目 ` cd ivideo` 3. 安装依赖 `yarn install`(如果没有的话,请全局安装yarn, `npm i yarn -g`) 4. 打开开发环境 `yarn start` ### 如何编译 * 编译全平台 ` yarn package-all` * 编译当前平台 `yarn package` * windows: `yarn package-win` * Linux `yarn package-linux` 编译出来的包都放在 `release` 目录下 该项目是基于 [electron-react-boilerplate](https://github.com/chentsulin/electron-react-boilerplate) 脚手架 进行创建,感谢 @[chentsulin](https://github.com/chentsulin) # 最后请大家低调使用,祝大家看得舒心 ## 本项目仅作为个人学习用途,如有侵权请联系我删除该仓库 ================================================ FILE: app/.eslintrc ================================================ { "rules": { "flowtype/boolean-style": ["error", "boolean"], "flowtype/define-flow-type": "warn", "flowtype/delimiter-dangle": ["error", "never"], "flowtype/generic-spacing": ["error", "never"], "flowtype/no-primitive-constructor-types": "error", "flowtype/no-weak-types": "warn", "flowtype/object-type-delimiter": ["error", "comma"], "flowtype/require-parameter-type": "off", "flowtype/require-return-type": "off", "flowtype/require-valid-file-annotation": "off", "flowtype/semi": ["error", "always"], "flowtype/space-after-type-colon": ["error", "always"], "flowtype/space-before-generic-bracket": ["error", "never"], "flowtype/space-before-type-colon": ["error", "never"], "flowtype/union-intersection-spacing": ["error", "always"], "flowtype/use-flow-type": "error", "flowtype/valid-syntax": "error" } } ================================================ FILE: app/actions/source.js ================================================ import * as api from '../utils/fetch'; export function getAllVideoSource() { return (dispatch) => { api.source.getAllVideoSource().then((res) => dispatch({ type: 'GETALLVIDEOSOURCE', payload: res.data })); }; } ================================================ FILE: app/app.global.css ================================================ /* * @NOTE: Prepend a `~` to css file paths that are in your node_modules * See https://github.com/webpack-contrib/sass-loader#imports */ @import "~font-awesome/css/font-awesome.css"; @import "~rc-menu/assets/index.css"; /* @import "~antd/lib/style/index.css"; */ @import '~rc-select/assets/index.css'; * { margin: 0; padding: 0; } body { position: relative; color:#000; height: 100vh; /* background-color: #232c39; */ /* background-image: linear-gradient(45deg, rgba(0, 216, 255, 0.5) 10%, rgba(0, 1, 127, 0.7)); */ font-family: Arial, Helvetica, Helvetica Neue, serif; overflow-y: hidden; } h2 { margin: 0; font-size: 2.25rem; font-weight: bold; letter-spacing: -0.025em; color: #fff; } p { font-size: 24px; } li { list-style: none; } a { color: white; opacity: 0.75; text-decoration: none; } a:hover { opacity: 1; text-decoration: none; cursor: pointer; } .rc-select-dropdown-menu { max-height: 400px; } ================================================ FILE: app/app.html ================================================ i视频
================================================ FILE: app/components/.githook ================================================ ================================================ FILE: app/containers/App.js ================================================ // @flow import * as React from 'react'; type Props = { children: React.Node }; export default class App extends React.Component { props: Props; render() { return (
{this.props.children}
); } } ================================================ FILE: app/containers/Channel.js ================================================ import React from 'react'; import Menu, { Item as MenuItem } from 'rc-menu'; const Channel = ({ channel, handleSwitchChannel }) => { const item = channel.map((d) => {d.name}); return {item}; }; export default Channel; ================================================ FILE: app/containers/Frame.js ================================================ // @flow import React from 'react' import Channel from './Channel' import ToolBar from './ToolBar' export const Frame = ({ onComeback, onSourceSelected, onSwitchSource, handleSwitchChannel, channel, url, freeUrl, title, isFullScreen, children }) => { const isHiddenStyle = isFullScreen ? { display: 'none' } : { display: 'flex' } return (
{children}
) } export default Frame ================================================ FILE: app/containers/Root.js ================================================ // @flow import React, { Component } from 'react'; import { Provider } from 'react-redux'; import { ConnectedRouter } from 'react-router-redux'; import Routes from '../routes'; type Props = { store: {}, history: {} }; export default class Root extends Component { render() { return ( ); } } ================================================ FILE: app/containers/ToolBar.js ================================================ import React from 'react'; import Select, { Option } from 'rc-select'; // import { Icon, Select, Button } from 'antd'; const ToolBar = ({ onComeback, onSwitchSource, onSourceSelected, freeUrl, title }) => { const options = freeUrl.map(d => { return ( ) }) return (
返回
{title}
确定
) } export default ToolBar; ================================================ FILE: app/containers/Video.js ================================================ // @flow import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { webview, ipcRenderer } from 'electron'; import Channel from './Channel'; import ToolBar from './ToolBar'; import Frame from './Frame'; import * as sourceActions from '../actions/source'; class VideoPlay extends PureComponent { constructor(props) { super(props); this.handleSwitchChannel = this.handleSwitchChannel.bind(this); this.onComeback = this.onComeback.bind(this); this.onSourceSelected = this.onSourceSelected.bind(this); this.onSwitchSource = this.onSwitchSource.bind(this); } state = { channel: [], url: 'https://v.qq.com', freeUrl: [], selectedUrl: 'http://vip.jlsprh.com/index.php?url=', isFullScreen: false } componentDidMount() { this.props.actions.getAllVideoSource(); const webView = this.webview; webView.addEventListener('dom-ready', () => { this.setTitle(); }); webView.addEventListener('new-window', (obj) => { this.setState({ url: `${obj.url}` }); }); webView.addEventListener('will-navigate', (obj) => { this.setState({ url: `${obj.url}` }); }); ipcRenderer.on('enter-full-screen', (e, msg) => { this.setState({ isFullScreen: msg }); }); } componentWillReceiveProps(nextProps) { const { source } = nextProps; if (source) { this.setState({ channel: source.platformlist, freeUrl: source.list }); } } handleSwitchChannel(value) { this.setState({ url: value.key }); } setTitle() { const title = this.webview.getTitle(); this.setState({ title }); } onComeback() { this.webview.goBack(); } onSourceSelected(value) { const selectedUrl = this.state.freeUrl.find((d) => { if (d.name === value) { return d.url; } }); this.setState({ selectedUrl }); } onSwitchSource() { const { selectedUrl } = this.state; const currentVideoUrl = this.webview.getURL(); this.setState({ url: `${selectedUrl.url}${currentVideoUrl}` }); } render() { const { channel, url, freeUrl, title, isFullScreen } = this.state; return ( { this.webview = webview; }} title="腾讯视频" style={{ height: isFullScreen ? '100vh' : 'calc(100vh - 60px)', width: '100%' }} src={url} allowpopups="true" plugins /> ); } } function mapDispatchToProps(dispatch) { return { actions: { ...bindActionCreators(sourceActions, dispatch) } }; } function mapStateToProps(state) { return { source: state.source }; } export default connect( mapStateToProps, mapDispatchToProps )(VideoPlay); ================================================ FILE: app/index.js ================================================ import React from 'react'; import { render } from 'react-dom'; import { AppContainer } from 'react-hot-loader'; import Root from './containers/Root'; import { configureStore, history } from './store/configureStore'; import './app.global.css'; const store = configureStore(); render( , document.getElementById('root') ); if (module.hot) { module.hot.accept('./containers/Root', () => { const NextRoot = require('./containers/Root'); // eslint-disable-line global-require render( , document.getElementById('root') ); }); } ================================================ FILE: app/main.dev.js ================================================ /* eslint global-require: 0, flowtype-errors/show-errors: 0 */ /** * This module executes inside of electron's main process. You can start * electron renderer process from here and communicate with the other processes * through IPC. * * When running `npm run build` or `npm run build-main`, this file is compiled to * `./app/main.prod.js` using webpack. This gives us some performance wins. * * @flow */ import { app, BrowserWindow, ipcMain } from 'electron'; import MenuBuilder from './menu'; let mainWindow = null; if (process.env.NODE_ENV === 'production') { const sourceMapSupport = require('source-map-support'); sourceMapSupport.install(); } if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { require('electron-debug')(); const path = require('path'); const p = path.join(__dirname, '..', 'app', 'node_modules'); require('module').globalPaths.push(p); } const installExtensions = async () => { const installer = require('electron-devtools-installer'); const forceDownload = !!process.env.UPGRADE_EXTENSIONS; const extensions = [ 'REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS' ]; return Promise .all(extensions.map(name => installer.default(installer[name], forceDownload))) .catch(console.log); }; // Try to append Pepper flash. See https://github.com/electron/electron/blob/master/docs/tutorial/using-pepper-flash-plugin.md if (process.platform === 'darwin' && app.getPath("pepperFlashSystemPlugin")) { app.commandLine.appendSwitch( "ppapi-flash-path", app.getPath("pepperFlashSystemPlugin") ); } /** * Add event listeners... */ app.on('window-all-closed', () => { // Respect the OSX convention of having the application in memory even // after all windows have been closed if (process.platform !== 'darwin') { app.quit(); } }); app.on('ready', async () => { if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { await installExtensions(); } mainWindow = new BrowserWindow({ show: false, width: 1024, height: 728, webPreferences: { plugins: true } }); mainWindow.loadURL(`file://${__dirname}/app.html`); // @TODO: Use 'ready-to-show' event // https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event mainWindow.webContents.on('did-finish-load', () => { if (!mainWindow) { throw new Error('"mainWindow" is not defined'); } mainWindow.show(); mainWindow.focus(); }); mainWindow.on('closed', () => { mainWindow = null; }); mainWindow.on('enter-full-screen', () => { mainWindow.webContents.send('enter-full-screen', true); }) mainWindow.on('leave-full-screen', () => { mainWindow.webContents.send('enter-full-screen', false); }) const menuBuilder = new MenuBuilder(mainWindow); menuBuilder.buildMenu(); }); ================================================ FILE: app/menu.js ================================================ // @flow import { app, Menu, shell, BrowserWindow } from 'electron'; export default class MenuBuilder { mainWindow: BrowserWindow; constructor(mainWindow: BrowserWindow) { this.mainWindow = mainWindow; } buildMenu() { if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') { this.setupDevelopmentEnvironment(); } const template = process.platform === 'darwin' ? this.buildDarwinTemplate() : this.buildDefaultTemplate(); const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); return menu; } setupDevelopmentEnvironment() { this.mainWindow.openDevTools(); this.mainWindow.webContents.on('context-menu', (e, props) => { const { x, y } = props; Menu .buildFromTemplate([{ label: 'Inspect element', click: () => { this.mainWindow.inspectElement(x, y); } }]) .popup(this.mainWindow); }); } buildDarwinTemplate() { const subMenuAbout = { label: 'Electron', submenu: [ { label: 'About ElectronReact', selector: 'orderFrontStandardAboutPanel:' }, { type: 'separator' }, { label: 'Services', submenu: [] }, { type: 'separator' }, { label: 'Hide ElectronReact', accelerator: 'Command+H', selector: 'hide:' }, { label: 'Hide Others', accelerator: 'Command+Shift+H', selector: 'hideOtherApplications:' }, { label: 'Show All', selector: 'unhideAllApplications:' }, { type: 'separator' }, { label: 'Quit', accelerator: 'Command+Q', click: () => { app.quit(); } } ] }; const subMenuEdit = { label: 'Edit', submenu: [ { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' }, { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' }, { type: 'separator' }, { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' }, { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' }, { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' }, { label: 'Select All', accelerator: 'Command+A', selector: 'selectAll:' } ] }; const subMenuViewDev = { label: 'View', submenu: [ { label: 'Reload', accelerator: 'Command+R', click: () => { this.mainWindow.webContents.reload(); } }, { label: 'Toggle Full Screen', accelerator: 'Ctrl+Command+F', click: () => { this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); } }, { label: 'Toggle Developer Tools', accelerator: 'Alt+Command+I', click: () => { this.mainWindow.toggleDevTools(); } } ] }; const subMenuViewProd = { label: 'View', submenu: [ { label: 'Toggle Full Screen', accelerator: 'Ctrl+Command+F', click: () => { this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); } } ] }; const subMenuWindow = { label: 'Window', submenu: [ { label: 'Minimize', accelerator: 'Command+M', selector: 'performMiniaturize:' }, { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }, { type: 'separator' }, { label: 'Bring All to Front', selector: 'arrangeInFront:' } ] }; const subMenuHelp = { label: 'Help', submenu: [ { label: 'Learn More', click() { shell.openExternal('http://electron.atom.io'); } }, { label: 'Documentation', click() { shell.openExternal('https://github.com/atom/electron/tree/master/docs#readme'); } }, { label: 'Community Discussions', click() { shell.openExternal('https://discuss.atom.io/c/electron'); } }, { label: 'Search Issues', click() { shell.openExternal('https://github.com/atom/electron/issues'); } } ] }; const subMenuView = process.env.NODE_ENV === 'development' ? subMenuViewDev : subMenuViewProd; return [ subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp ]; } buildDefaultTemplate() { const templateDefault = [{ label: '&File', submenu: [{ label: '&Open', accelerator: 'Ctrl+O' }, { label: '&Close', accelerator: 'Ctrl+W', click: () => { this.mainWindow.close(); } }] }, { label: '&View', submenu: (process.env.NODE_ENV === 'development') ? [{ label: '&Reload', accelerator: 'Ctrl+R', click: () => { this.mainWindow.webContents.reload(); } }, { label: 'Toggle &Full Screen', accelerator: 'F11', click: () => { this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); } }, { label: 'Toggle &Developer Tools', accelerator: 'Alt+Ctrl+I', click: () => { this.mainWindow.toggleDevTools(); } }] : [{ label: 'Toggle &Full Screen', accelerator: 'F11', click: () => { this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); } }] }, { label: 'Help', submenu: [{ label: 'Learn More', click() { shell.openExternal('http://electron.atom.io'); } }, { label: 'Documentation', click() { shell.openExternal('https://github.com/atom/electron/tree/master/docs#readme'); } }, { label: 'Community Discussions', click() { shell.openExternal('https://discuss.atom.io/c/electron'); } }, { label: 'Search Issues', click() { shell.openExternal('https://github.com/atom/electron/issues'); } }] }]; return templateDefault; } } ================================================ FILE: app/package.json ================================================ { "name": "electron-react-boilerplate", "productName": "electron-react-boilerplate", "version": "1.1.4", "description": "Electron application boilerplate based on React, React Router, Webpack, React Hot Loader for rapid application development", "main": "./main.prod.js", "author": { "name": "C. T. Lin", "email": "chentsulin@gmail.com", "url": "https://github.com/chentsulin" }, "scripts": { "electron-rebuild": "node -r babel-register ../internals/scripts/ElectronRebuild.js", "postinstall": "npm run electron-rebuild" }, "license": "MIT", "dependencies": {} } ================================================ FILE: app/reducers/index.js ================================================ // @flow import { combineReducers } from 'redux'; import { routerReducer as router } from 'react-router-redux'; import source from './source'; const rootReducer = combineReducers({ router, source, }); export default rootReducer; ================================================ FILE: app/reducers/source.js ================================================ export default function source(state = null, action) { switch(action.type) { case 'GETALLVIDEOSOURCE': return action.payload; default: return state; } } ================================================ FILE: app/routes.js ================================================ /* eslint flowtype-errors/show-errors: 0 */ import React from 'react'; import { Switch, Route } from 'react-router'; import App from './containers/App'; import Video from './containers/Video'; export default () => ( ); ================================================ FILE: app/store/configureStore.dev.js ================================================ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; import { createHashHistory } from 'history'; import { routerMiddleware, routerActions } from 'react-router-redux'; import { createLogger } from 'redux-logger'; import rootReducer from '../reducers'; // import type { counterStateType } from '../reducers/counter'; const history = createHashHistory(); const configureStore = (initialState) => { // Redux Configuration const middleware = []; const enhancers = []; // Thunk Middleware middleware.push(thunk); // Logging Middleware const logger = createLogger({ level: 'info', collapsed: true }); // Skip redux logs in console during the tests if (process.env.NODE_ENV !== 'test') { middleware.push(logger); } // Router Middleware const router = routerMiddleware(history); middleware.push(router); // Redux DevTools Configuration const actionCreators = { ...routerActions, }; // If Redux DevTools Extension is installed use it, otherwise use Redux compose /* eslint-disable no-underscore-dangle */ const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ // Options: http://zalmoxisus.github.io/redux-devtools-extension/API/Arguments.html actionCreators, }) : compose; /* eslint-enable no-underscore-dangle */ // Apply Middleware & Compose Enhancers enhancers.push(applyMiddleware(...middleware)); const enhancer = composeEnhancers(...enhancers); // Create Store const store = createStore(rootReducer, initialState, enhancer); if (module.hot) { module.hot.accept('../reducers', () => store.replaceReducer(require('../reducers'))); // eslint-disable-line global-require } return store; }; export default { configureStore, history }; ================================================ FILE: app/store/configureStore.js ================================================ // @flow if (process.env.NODE_ENV === 'production') { module.exports = require('./configureStore.prod'); // eslint-disable-line global-require } else { module.exports = require('./configureStore.dev'); // eslint-disable-line global-require } ================================================ FILE: app/store/configureStore.prod.js ================================================ // @flow import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import { createBrowserHistory } from 'history'; import { routerMiddleware } from 'react-router-redux'; import rootReducer from '../reducers'; import type { counterStateType } from '../reducers/counter'; const history = createBrowserHistory(); const router = routerMiddleware(history); const enhancer = applyMiddleware(thunk, router); function configureStore(initialState?: counterStateType) { return createStore(rootReducer, initialState, enhancer); } export default { configureStore, history }; ================================================ FILE: app/utils/.gitkeep ================================================ ================================================ FILE: app/utils/fetch.js ================================================ import axios from 'axios'; const BASEURL = 'https://raw.githubusercontent.com/phobal/ivideo/master/resources/viplist.json'; const instance = axios.create({ baseURL: BASEURL, timeout: 10000 }); const createAPI = (url, method, config) => { config = config || {} // eslint-disable-line return instance({ url, method, ...config }); }; const source = { getAllVideoSource: (config) => createAPI('', 'GET', config) }; export { source }; ================================================ FILE: appveyor.yml ================================================ os: unstable environment: matrix: - nodejs_version: 8 - nodejs_version: 7 cache: - "%LOCALAPPDATA%/Yarn" - node_modules -> package.json - app/node_modules -> app/package.json matrix: fast_finish: true build: off version: '{build}' shallow_clone: true clone_depth: 1 install: - ps: Install-Product node $env:nodejs_version - set CI=true - yarn - cd app && yarn test_script: - node --version - yarn lint - yarn package - yarn test - yarn test-e2e ================================================ FILE: flow-typed/module_vx.x.x.js ================================================ declare module 'module' { declare module.exports: any; } ================================================ FILE: internals/flow/CSSModule.js.flow ================================================ // @flow declare export default { [key: string]: string } ================================================ FILE: internals/flow/WebpackAsset.js.flow ================================================ // @flow declare export default string ================================================ FILE: internals/mocks/fileMock.js ================================================ export default 'test-file-stub'; ================================================ FILE: internals/scripts/CheckBuiltsExist.js ================================================ // @flow // Check if the renderer and main bundles are built import path from 'path'; import chalk from 'chalk'; import fs from 'fs'; function CheckBuildsExist() { const mainPath = path.join(__dirname, '..', '..', 'app', 'main.prod.js'); const rendererPath = path.join(__dirname, '..', '..', 'app', 'dist', 'renderer.prod.js'); if (!fs.existsSync(mainPath)) { throw new Error(chalk.whiteBright.bgRed.bold('The main process is not built yet. Build it by running "npm run build-main"')); } if (!fs.existsSync(rendererPath)) { throw new Error(chalk.whiteBright.bgRed.bold('The renderer process is not built yet. Build it by running "npm run build-renderer"')); } } CheckBuildsExist(); ================================================ FILE: internals/scripts/CheckNativeDep.js ================================================ // @flow import fs from 'fs'; import chalk from 'chalk'; import { execSync } from 'child_process'; import { dependencies } from '../../package.json'; (() => { if (!dependencies) return; const dependenciesKeys = Object.keys(dependencies); const nativeDeps = fs.readdirSync('node_modules') .filter(folder => fs.existsSync(`node_modules/${folder}/binding.gyp`)); try { // Find the reason for why the dependency is installed. If it is installed // because of a devDependency then that is okay. Warn when it is installed // because of a dependency const dependenciesObject = JSON.parse(execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString()); const rootDependencies = Object.keys(dependenciesObject.dependencies); const filteredRootDependencies = rootDependencies .filter(rootDependency => dependenciesKeys.includes(rootDependency)); if (filteredRootDependencies.length > 0) { const plural = filteredRootDependencies.length > 1; console.log(` ${chalk.whiteBright.bgYellow.bold('Webpack does not work with native dependencies.')} ${chalk.bold(filteredRootDependencies.join(', '))} ${plural ? 'are native dependencies' : 'is a native dependency'} and should be installed inside of the "./app" folder. First uninstall the packages from "./package.json": ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} ${chalk.bold('Then, instead of installing the package to the root "./package.json":')} ${chalk.whiteBright.bgRed.bold('npm install your-package --save')} ${chalk.bold('Install the package to "./app/package.json"')} ${chalk.whiteBright.bgGreen.bold('cd ./app && npm install your-package --save')} Read more about native dependencies at: ${chalk.bold('https://github.com/chentsulin/electron-react-boilerplate/wiki/Module-Structure----Two-package.json-Structure')} `); process.exit(1); } } catch (e) { console.log('Native dependencies could not be checked'); } })(); ================================================ FILE: internals/scripts/CheckNodeEnv.js ================================================ // @flow import chalk from 'chalk'; export default function CheckNodeEnv(expectedEnv: string) { if (!expectedEnv) { throw new Error('"expectedEnv" not set'); } if (process.env.NODE_ENV !== expectedEnv) { console.log(chalk.whiteBright.bgRed.bold(`"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`)); process.exit(2); } } ================================================ FILE: internals/scripts/CheckPortInUse.js ================================================ // @flow import chalk from 'chalk'; import detectPort from 'detect-port'; (function CheckPortInUse() { const port: string = process.env.PORT || '1212'; detectPort(port, (err: ?Error, availablePort: number) => { if (port !== String(availablePort)) { throw new Error(chalk.whiteBright.bgRed.bold(`Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm run dev`)); } else { process.exit(0); } }); }()); ================================================ FILE: internals/scripts/ElectronRebuild.js ================================================ // @flow import path from 'path'; import { execSync } from 'child_process'; import fs from 'fs'; import dependencies from '../../app/package.json'; const nodeModulesPath = path.join(__dirname, '..', '..', 'app', 'node_modules'); if (Object.keys(dependencies || {}).length > 0 && fs.existsSync(nodeModulesPath)) { const electronRebuildCmd = '../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .'; const cmd = process.platform === 'win32' ? electronRebuildCmd.replace(/\//g, '\\') : electronRebuildCmd; execSync(cmd, { cwd: path.join(__dirname, '..', '..', 'app') }); } ================================================ FILE: internals/scripts/RunTests.js ================================================ import spawn from 'cross-spawn'; import path from 'path'; const pattern = process.argv[2] === 'e2e' ? 'test/e2e/.+\\.spec\\.js' : 'test/(?!e2e/)[^/]+/.+\\.spec\\.js$'; const result = spawn.sync( path.normalize('./node_modules/.bin/jest'), [pattern, ...process.argv.slice(2)], { stdio: 'inherit' } ); process.exit(result.status); ================================================ FILE: package.json ================================================ { "name": "ivideo", "productName": "ivideo", "version": "1.1.4", "description": "一个视频播放器观看国内主流视频网站,不用单独下载各个平台客户端", "scripts": { "build": "concurrently \"npm run build-main\" \"npm run build-renderer\"", "build-dll": "cross-env NODE_ENV=development node --trace-warnings -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.renderer.dev.dll.js --colors", "build-main": "cross-env NODE_ENV=production node --trace-warnings -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.main.prod.js --colors", "build-renderer": "cross-env NODE_ENV=production node --trace-warnings -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.renderer.prod.js --colors", "dev": "cross-env START_HOT=1 node -r babel-register ./internals/scripts/CheckPortInUse.js && cross-env START_HOT=1 npm run start-renderer-dev", "electron-rebuild": "electron-rebuild --parallel --force --types prod,dev,optional --module-dir app", "flow": "flow", "flow-typed": "rimraf flow-typed/npm && flow-typed install --overwrite || true", "lint": "cross-env NODE_ENV=development eslint --cache --format=node_modules/eslint-formatter-pretty .", "lint-fix": "npm run lint -- --fix", "lint-styles": "stylelint app/*.css app/components/*.css --syntax scss", "lint-styles-fix": "stylefmt -r app/*.css app/components/*.css", "package": "npm run build && build --publish never", "package-all": "npm run build && build -mwl", "package-linux": "npm run build && build --linux", "package-win": "npm run build && build --win --x64", "postinstall": "node -r babel-register internals/scripts/CheckNativeDep.js && npm run flow-typed && npm run build-dll && electron-builder install-app-deps && node node_modules/fbjs-scripts/node/check-dev-engines.js package.json", "prestart": "npm run build", "start": "cross-env NODE_ENV=production electron ./app/", "start-main-dev": "cross-env HOT=1 NODE_ENV=development electron -r babel-register ./app/main.dev", "start-renderer-dev": "cross-env NODE_ENV=development node --trace-warnings -r babel-register ./node_modules/webpack-dev-server/bin/webpack-dev-server --config webpack.config.renderer.dev.js", "test": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 node --trace-warnings -r babel-register ./internals/scripts/RunTests.js", "test-all": "npm run lint && npm run flow && npm run build && npm run test && npm run test-e2e", "test-e2e": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 node --trace-warnings -r babel-register ./internals/scripts/RunTests.js e2e", "test-watch": "npm test -- --watch" }, "browserslist": "electron 1.6", "build": { "productName": "ivideo", "appId": "org.phobal.ivideo", "files": [ "dist/", "node_modules/", "app.html", "main.prod.js", "main.prod.js.map", "package.json" ], "dmg": { "contents": [ { "x": 130, "y": 220 }, { "x": 410, "y": 220, "type": "link", "path": "/Applications" } ] }, "win": { "target": [ { "target": "nsis", "arch": [ "x64", "ia32" ] } ] }, "linux": { "target": [ "deb", "AppImage" ], "category": "Development" }, "directories": { "buildResources": "resources", "output": "release" } }, "repository": { "type": "git", "url": "git+https://github.com/phobal/ivideo.git" }, "author": { "name": "phobal", "email": "phobal@126.com", "url": "https://github.com/phobal" }, "license": "MIT", "bugs": { "url": "https://github.com/phobal/ivideo/issues" }, "keywords": [ "electron", "boilerplate", "react", "redux", "flow", "sass", "webpack", "hot", "reload" ], "homepage": "https://github.com/phobal/ivideo#readme", "jest": { "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/internals/mocks/fileMock.js", "\\.(css|less|sass|scss)$": "identity-obj-proxy" }, "moduleFileExtensions": [ "js" ], "moduleDirectories": [ "node_modules", "app/node_modules" ], "transform": { "^.+\\.js$": "babel-jest" }, "setupFiles": [ "./internals/scripts/CheckBuiltsExist.js" ] }, "devDependencies": { "babel-core": "^6.26.0", "babel-eslint": "^8.2.1", "babel-jest": "^22.1.0", "babel-loader": "^7.1.2", "babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-dev-expression": "^0.2.1", "babel-plugin-flow-runtime": "^0.15.0", "babel-plugin-import": "^1.7.0", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-es2015-classes": "^6.24.1", "babel-preset-env": "^1.6.1", "babel-preset-react": "^6.24.1", "babel-preset-react-hmre": "^1.1.1", "babel-preset-react-optimize": "^1.0.1", "babel-preset-stage-0": "^6.24.1", "babel-register": "^6.26.0", "chalk": "^2.3.0", "concurrently": "^3.5.1", "cross-env": "^5.1.3", "cross-spawn": "^6.0.4", "css-loader": "^0.28.9", "detect-port": "^1.2.2", "electron": "^1.7.11", "electron-builder": "^19.55.3", "electron-devtools-installer": "^2.2.3", "electron-rebuild": "^1.7.3", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", "enzyme-to-json": "^3.3.1", "eslint": "^4.16.0", "eslint-config-airbnb": "^16.1.0", "eslint-formatter-pretty": "^1.3.0", "eslint-import-resolver-webpack": "^0.8.4", "eslint-plugin-compat": "^2.2.0", "eslint-plugin-flowtype": "^2.42.0", "eslint-plugin-import": "^2.8.0", "eslint-plugin-jest": "^21.7.0", "eslint-plugin-jsx-a11y": "6.0.3", "eslint-plugin-promise": "^3.6.0", "eslint-plugin-react": "^7.6.1", "express": "^4.16.2", "extract-text-webpack-plugin": "^3.0.2", "fbjs-scripts": "^0.8.1", "file-loader": "^1.1.6", "flow-bin": "^0.64.0", "flow-runtime": "^0.16.0", "flow-typed": "^2.3.0", "identity-obj-proxy": "^3.0.0", "jest": "^22.1.4", "less": "^3.0.1", "less-loader": "^4.1.0", "minimist": "^1.2.0", "node-sass": "^4.7.2", "npm-logical-tree": "^1.2.1", "react-test-renderer": "^16.2.0", "redux-logger": "^3.0.6", "rimraf": "^2.6.2", "sass-loader": "^6.0.6", "sinon": "^4.2.2", "spectron": "^3.8.0", "style-loader": "^0.20.1", "stylefmt": "^6.0.0", "stylelint": "^8.4.0", "stylelint-config-standard": "^18.0.0", "uglifyjs-webpack-plugin": "1.1.8", "url-loader": "^0.6.2", "webpack": "^3.10.0", "webpack-bundle-analyzer": "^2.9.2", "webpack-dev-server": "^2.11.1", "webpack-merge": "^4.1.1" }, "dependencies": { "axios": "^0.18.0", "devtron": "^1.4.0", "electron-debug": "^1.5.0", "font-awesome": "^4.7.0", "history": "^4.7.2", "rc-menu": "^6.2.10", "rc-select": "^7.7.7", "react": "^16.2.0", "react-dom": "^16.2.0", "react-hot-loader": "^4.0.0-beta.13", "react-redux": "^5.0.6", "react-router": "^4.2.0", "react-router-dom": "^4.2.2", "react-router-redux": "^5.0.0-alpha.6", "redux": "^3.7.2", "redux-thunk": "^2.2.0", "source-map-support": "^0.5.3" }, "devEngines": { "node": ">=7.x", "npm": ">=4.x", "yarn": ">=0.21.3" } } ================================================ FILE: resources/viplist.json ================================================ { "platformlist": [ { "name": "爱奇艺", "url": "http://www.iqiyi.com/" }, { "name": "腾讯视频", "url": "https://v.qq.com/" }, { "name": "芒果", "url": "https://www.mgtv.com/" }, { "name": "优酷", "url": "https://www.youku.com/" }, { "name": "搜狐视频", "url": "https://tv.sohu.com/" }, { "name": "乐视视频", "url": "https://www.le.com/" }, { "name": "电影天堂", "url": "http://www.btbtdy.net/" }, { "name": "新视觉影院", "url": "http://www.yy3080.com/vod-type-id-1-pg-1.html/" } ], "list": [ { "name": "5月-21", "url": "http://jiexi.071811.cc/jx2.php?url=" }, { "name": "9月-2", "url": "http://jqaaa.com/jx.php?url=" }, { "name": "5月-4", "url": "http://beaacc.com/api.php?url=" }, { "name": "4.21-4", "url": "http://www.82190555.com/index.php?url=" }, { "name": "4.21-6", "url": "http://www.85105052.com/admin.php?url=" }, { "name": "5月-23", "url": "http://api.baiyug.cn/vip/index.php?url=" }, { "name": "4.21-3-慢", "url": "https://yooomm.com/index.php?url=" }, { "name": "5月-24", "url": "http://www.82190555.com/index/qqvod.php?url=" }, { "name": "1", "url": "http://17kyun.com/api.php?url=" }, { "name": "品优解析-可播但广告", "url": "http://api.pucms.com/xnflv/?url=" }, { "name": "5月-1", "url": "http://www.82190555.com/index/qqvod.php?url=" }, { "name": "腾讯可用,金桥解析", "url": "http://jqaaa.com/jx.php?url=" }, { "name": "速度牛", "url": "http://api.wlzhan.com/sudu/?url=" }, { "name": "万能接口6", "url": "http://wwwhe1.177kdy.cn/4.php?pass=1&url=" }, { "name": "花园影视(可能无效)", "url": "http://j.zz22x.com/jx/?url=" }, { "name": "9月-1", "url": "http://api.ledboke.com/?url=" } ] } ================================================ FILE: test/.eslintrc ================================================ { "env": { "jest/globals": true }, "plugins": [ "jest" ], "rules": { "jest/no-disabled-tests": "warn", "jest/no-focused-tests": "error", "jest/no-identical-title": "error" } } ================================================ FILE: test/actions/__snapshots__/counter.spec.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`actions should decrement should create decrement action 1`] = ` Object { "type": "DECREMENT_COUNTER", } `; exports[`actions should increment should create increment action 1`] = ` Object { "type": "INCREMENT_COUNTER", } `; ================================================ FILE: test/actions/counter.spec.js ================================================ import { spy } from 'sinon'; import * as actions from '../../app/actions/counter'; describe('actions', () => { it('should increment should create increment action', () => { expect(actions.increment()).toMatchSnapshot(); }); it('should decrement should create decrement action', () => { expect(actions.decrement()).toMatchSnapshot(); }); it('should incrementIfOdd should create increment action', () => { const fn = actions.incrementIfOdd(); expect(fn).toBeInstanceOf(Function); const dispatch = spy(); const getState = () => ({ counter: 1 }); fn(dispatch, getState); expect(dispatch.calledWith({ type: actions.INCREMENT_COUNTER })).toBe(true); }); it('should incrementIfOdd shouldnt create increment action if counter is even', () => { const fn = actions.incrementIfOdd(); const dispatch = spy(); const getState = () => ({ counter: 2 }); fn(dispatch, getState); expect(dispatch.called).toBe(false); }); // There's no nice way to test this at the moment... it('should incrementAsync', done => { const fn = actions.incrementAsync(1); expect(fn).toBeInstanceOf(Function); const dispatch = spy(); fn(dispatch); setTimeout(() => { expect(dispatch.calledWith({ type: actions.INCREMENT_COUNTER })).toBe(true); done(); }, 5); }); }); ================================================ FILE: test/components/Counter.spec.js ================================================ import { spy } from 'sinon'; import React from 'react'; import Enzyme, { shallow } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import { BrowserRouter as Router } from 'react-router-dom'; import renderer from 'react-test-renderer'; import Counter from '../../app/components/Counter'; Enzyme.configure({ adapter: new Adapter() }); function setup() { const actions = { increment: spy(), incrementIfOdd: spy(), incrementAsync: spy(), decrement: spy() }; const component = shallow(); return { component, actions, buttons: component.find('button'), p: component.find('.counter') }; } describe('Counter component', () => { it('should should display count', () => { const { p } = setup(); expect(p.text()).toMatch(/^1$/); }); it('should first button should call increment', () => { const { buttons, actions } = setup(); buttons.at(0).simulate('click'); expect(actions.increment.called).toBe(true); }); it('should match exact snapshot', () => { const { actions } = setup(); const counter = (
); const tree = renderer .create(counter) .toJSON(); expect(tree).toMatchSnapshot(); }); it('should second button should call decrement', () => { const { buttons, actions } = setup(); buttons.at(1).simulate('click'); expect(actions.decrement.called).toBe(true); }); it('should third button should call incrementIfOdd', () => { const { buttons, actions } = setup(); buttons.at(2).simulate('click'); expect(actions.incrementIfOdd.called).toBe(true); }); it('should fourth button should call incrementAsync', () => { const { buttons, actions } = setup(); buttons.at(3).simulate('click'); expect(actions.incrementAsync.called).toBe(true); }); }); ================================================ FILE: test/components/__snapshots__/Counter.spec.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Counter component should match exact snapshot 1`] = `
1
`; ================================================ FILE: test/containers/CounterPage.spec.js ================================================ import React from 'react'; import Enzyme, { mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import { Provider } from 'react-redux'; import { createBrowserHistory } from 'history'; import { ConnectedRouter } from 'react-router-redux'; import CounterPage from '../../app/containers/CounterPage'; import { configureStore } from '../../app/store/configureStore'; Enzyme.configure({ adapter: new Adapter() }); function setup(initialState) { const store = configureStore(initialState); const history = createBrowserHistory(); const provider = ( ); const app = mount(provider); return { app, buttons: app.find('button'), p: app.find('.counter') }; } describe('containers', () => { describe('App', () => { it('should display initial count', () => { const { p } = setup(); expect(p.text()).toMatch(/^0$/); }); it('should display updated count after increment button click', () => { const { buttons, p } = setup(); buttons.at(0).simulate('click'); expect(p.text()).toMatch(/^1$/); }); it('should display updated count after descrement button click', () => { const { buttons, p } = setup(); buttons.at(1).simulate('click'); expect(p.text()).toMatch(/^-1$/); }); it('shouldnt change if even and if odd button clicked', () => { const { buttons, p } = setup(); buttons.at(2).simulate('click'); expect(p.text()).toMatch(/^0$/); }); it('should change if odd and if odd button clicked', () => { const { buttons, p } = setup({ counter: 1 }); buttons.at(2).simulate('click'); expect(p.text()).toMatch(/^2$/); }); }); }); ================================================ FILE: test/e2e/e2e.spec.js ================================================ import { Application } from 'spectron'; import electronPath from 'electron'; import path from 'path'; jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; const delay = time => new Promise(resolve => setTimeout(resolve, time)); describe('main window', function spec() { beforeAll(async () => { this.app = new Application({ path: electronPath, args: [path.join(__dirname, '..', '..', 'app')], }); return this.app.start(); }); afterAll(() => { if (this.app && this.app.isRunning()) { return this.app.stop(); } }); const findCounter = () => this.app.client.element('[data-tid="counter"]'); const findButtons = async () => { const { value } = await this.app.client.elements('[data-tclass="btn"]'); return value.map(btn => btn.ELEMENT); }; it('should open window', async () => { const { client, browserWindow } = this.app; await client.waitUntilWindowLoaded(); await delay(500); const title = await browserWindow.getTitle(); expect(title).toBe('Hello Electron React!'); }); it('should haven\'t any logs in console of main window', async () => { const { client } = this.app; const logs = await client.getRenderProcessLogs(); // Print renderer process logs logs.forEach(log => { console.log(log.message); console.log(log.source); console.log(log.level); }); expect(logs).toHaveLength(0); }); it('should to Counter with click "to Counter" link', async () => { const { client } = this.app; await client.click('[data-tid=container] > a'); expect(await findCounter().getText()).toBe('0'); }); it('should display updated count after increment button click', async () => { const { client } = this.app; const buttons = await findButtons(); await client.elementIdClick(buttons[0]); // + expect(await findCounter().getText()).toBe('1'); }); it('should display updated count after descrement button click', async () => { const { client } = this.app; const buttons = await findButtons(); await client.elementIdClick(buttons[1]); // - expect(await findCounter().getText()).toBe('0'); }); it('shouldnt change if even and if odd button clicked', async () => { const { client } = this.app; const buttons = await findButtons(); await client.elementIdClick(buttons[2]); // odd expect(await findCounter().getText()).toBe('0'); }); it('should change if odd and if odd button clicked', async () => { const { client } = this.app; const buttons = await findButtons(); await client.elementIdClick(buttons[0]); // + await client.elementIdClick(buttons[2]); // odd expect(await findCounter().getText()).toBe('2'); }); it('should change if async button clicked and a second later', async () => { const { client } = this.app; const buttons = await findButtons(); await client.elementIdClick(buttons[3]); // async expect(await findCounter().getText()).toBe('2'); await delay(1500); expect(await findCounter().getText()).toBe('3'); }); it('should back to home if back button clicked', async () => { const { client } = this.app; await client.element('[data-tid="backButton"] > a').click(); expect(await client.isExisting('[data-tid="container"]')).toBe(true); }); }); ================================================ FILE: test/example.js ================================================ describe('description', () => { it('should have description', () => { expect(1 + 2).toBe(3); }); }); ================================================ FILE: test/reducers/__snapshots__/counter.spec.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`reducers counter should handle DECREMENT_COUNTER 1`] = `0`; exports[`reducers counter should handle INCREMENT_COUNTER 1`] = `2`; exports[`reducers counter should handle initial state 1`] = `0`; exports[`reducers counter should handle unknown action type 1`] = `1`; ================================================ FILE: test/reducers/counter.spec.js ================================================ import counter from '../../app/reducers/counter'; import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../../app/actions/counter'; describe('reducers', () => { describe('counter', () => { it('should handle initial state', () => { expect(counter(undefined, {})).toMatchSnapshot(); }); it('should handle INCREMENT_COUNTER', () => { expect(counter(1, { type: INCREMENT_COUNTER })).toMatchSnapshot(); }); it('should handle DECREMENT_COUNTER', () => { expect(counter(1, { type: DECREMENT_COUNTER })).toMatchSnapshot(); }); it('should handle unknown action type', () => { expect(counter(1, { type: 'unknown' })).toMatchSnapshot(); }); }); }); ================================================ FILE: webpack.config.base.js ================================================ /** * Base webpack config used across other specific configs */ import path from 'path'; import webpack from 'webpack'; import { dependencies as externals } from './app/package.json'; export default { externals: Object.keys(externals || {}), module: { rules: [{ test: /\.jsx?$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { cacheDirectory: true } } }] }, output: { path: path.join(__dirname, 'app'), // https://github.com/webpack/webpack/issues/1114 libraryTarget: 'commonjs2' }, /** * Determine the array of extensions that should be used to resolve modules. */ resolve: { extensions: ['.js', '.jsx', '.json'], modules: [ path.join(__dirname, 'app'), 'node_modules', ], }, plugins: [ new webpack.EnvironmentPlugin({ NODE_ENV: 'production' }), new webpack.NamedModulesPlugin(), ], }; ================================================ FILE: webpack.config.eslint.js ================================================ require('babel-register'); module.exports = require('./webpack.config.renderer.dev'); ================================================ FILE: webpack.config.main.prod.js ================================================ /** * Webpack config for production electron main process */ import webpack from 'webpack'; import merge from 'webpack-merge'; import UglifyJSPlugin from 'uglifyjs-webpack-plugin'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import baseConfig from './webpack.config.base'; import CheckNodeEnv from './internals/scripts/CheckNodeEnv'; CheckNodeEnv('production'); export default merge.smart(baseConfig, { devtool: 'source-map', target: 'electron-main', entry: './app/main.dev', output: { path: __dirname, filename: './app/main.prod.js' }, plugins: [ new UglifyJSPlugin({ parallel: true, sourceMap: true }), new BundleAnalyzerPlugin({ analyzerMode: process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', openAnalyzer: process.env.OPEN_ANALYZER === 'true' }), /** * Create global constants which can be configured at compile time. * * Useful for allowing different behaviour between development builds and * release builds * * NODE_ENV should be production so that modules do not perform certain * development checks */ new webpack.EnvironmentPlugin({ NODE_ENV: 'production', DEBUG_PROD: 'false' }) ], /** * Disables webpack processing of __dirname and __filename. * If you run the bundle in node.js it falls back to these values of node.js. * https://github.com/webpack/webpack/issues/2010 */ node: { __dirname: false, __filename: false }, }); ================================================ FILE: webpack.config.renderer.dev.dll.js ================================================ /** * Builds the DLL for development electron renderer process */ import webpack from 'webpack'; import path from 'path'; import merge from 'webpack-merge'; import baseConfig from './webpack.config.base'; import { dependencies } from './package.json'; import CheckNodeEnv from './internals/scripts/CheckNodeEnv'; CheckNodeEnv('development'); const dist = path.resolve(process.cwd(), 'dll'); export default merge.smart(baseConfig, { context: process.cwd(), devtool: 'eval', target: 'electron-renderer', externals: ['fsevents', 'crypto-browserify'], /** * Use `module` from `webpack.config.renderer.dev.js` */ module: { rules: [ { test: /\.jsx?$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { cacheDirectory: true, plugins: [ // Here, we include babel plugins that are only required for the // renderer process. The 'transform-*' plugins must be included // before react-hot-loader/babel 'transform-class-properties', 'transform-es2015-classes', 'react-hot-loader/babel' ], } } }, { test: /\.global\.css$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { sourceMap: true, }, } ] }, { test: /^((?!\.global).)*\.css$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { modules: true, sourceMap: true, importLoaders: 1, localIdentName: '[name]__[local]__[hash:base64:5]', } }, ] }, // SASS support - compile all .global.scss files and pipe it to style.css { test: /\.global\.(scss|sass)$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { sourceMap: true, }, }, { loader: 'sass-loader' } ] }, // SASS support - compile all other .scss files and pipe it to style.css { test: /^((?!\.global).)*\.(scss|sass)$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { modules: true, sourceMap: true, importLoaders: 1, localIdentName: '[name]__[local]__[hash:base64:5]', } }, { loader: 'sass-loader' } ] }, // WOFF Font { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, use: { loader: 'url-loader', options: { limit: 10000, mimetype: 'application/font-woff', } }, }, // WOFF2 Font { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, use: { loader: 'url-loader', options: { limit: 10000, mimetype: 'application/font-woff', } } }, // TTF Font { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, use: { loader: 'url-loader', options: { limit: 10000, mimetype: 'application/octet-stream' } } }, // EOT Font { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader', }, // SVG Font { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, use: { loader: 'url-loader', options: { limit: 10000, mimetype: 'image/svg+xml', } } }, // Common Image Formats { test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, use: 'url-loader', } ] }, entry: { renderer: ( Object .keys(dependencies || {}) .filter(dependency => dependency !== 'font-awesome') ) }, output: { library: 'renderer', path: dist, filename: '[name].dev.dll.js', libraryTarget: 'var' }, plugins: [ new webpack.DllPlugin({ path: path.join(dist, '[name].json'), name: '[name]', }), /** * Create global constants which can be configured at compile time. * * Useful for allowing different behaviour between development builds and * release builds * * NODE_ENV should be production so that modules do not perform certain * development checks */ new webpack.EnvironmentPlugin({ NODE_ENV: 'development' }), new webpack.LoaderOptionsPlugin({ debug: true, options: { context: path.resolve(process.cwd(), 'app'), output: { path: path.resolve(process.cwd(), 'dll'), }, }, }) ], }); ================================================ FILE: webpack.config.renderer.dev.js ================================================ /* eslint global-require: 0, import/no-dynamic-require: 0 */ /** * Build config for development electron renderer process that uses * Hot-Module-Replacement * * https://webpack.js.org/concepts/hot-module-replacement/ */ import path from 'path'; import fs from 'fs'; import webpack from 'webpack'; import chalk from 'chalk'; import merge from 'webpack-merge'; import { spawn, execSync } from 'child_process'; import ExtractTextPlugin from 'extract-text-webpack-plugin'; import baseConfig from './webpack.config.base'; import CheckNodeEnv from './internals/scripts/CheckNodeEnv'; CheckNodeEnv('development'); const port = process.env.PORT || 1212; const publicPath = `http://localhost:${port}/dist`; const dll = path.resolve(process.cwd(), 'dll'); const manifest = path.resolve(dll, 'renderer.json'); /** * Warn if the DLL is not built */ if (!(fs.existsSync(dll) && fs.existsSync(manifest))) { console.log(chalk.black.bgYellow.bold('The DLL files are missing. Sit back while we build them for you with "npm run build-dll"')); execSync('npm run build-dll'); } export default merge.smart(baseConfig, { devtool: 'inline-source-map', target: 'electron-renderer', entry: [ 'react-hot-loader/patch', `webpack-dev-server/client?http://localhost:${port}/`, 'webpack/hot/only-dev-server', path.join(__dirname, 'app/index.js'), ], output: { publicPath: `http://localhost:${port}/dist/`, filename: 'renderer.dev.js' }, module: { rules: [ { test: /\.jsx?$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { cacheDirectory: true, plugins: [ // Here, we include babel plugins that are only required for the // renderer process. The 'transform-*' plugins must be included // before react-hot-loader/babel 'transform-class-properties', 'transform-es2015-classes', 'react-hot-loader/babel' ], } } }, { test: /\.less$/, loader: `style!css!less`, include: path.resolve(__dirname, 'node_modules'), }, { test: /\.global\.css$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { sourceMap: true, }, } ] }, { test: /^((?!\.global).)*\.css$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { modules: true, sourceMap: true, importLoaders: 1, localIdentName: '[name]__[local]__[hash:base64:5]', } }, ] }, // SASS support - compile all .global.scss files and pipe it to style.css { test: /\.global\.(scss|sass)$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { sourceMap: true, }, }, { loader: 'sass-loader' } ] }, // SASS support - compile all other .scss files and pipe it to style.css { test: /^((?!\.global).)*\.(scss|sass)$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { modules: true, sourceMap: true, importLoaders: 1, localIdentName: '[name]__[local]__[hash:base64:5]', } }, { loader: 'sass-loader' } ] }, // WOFF Font { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, use: { loader: 'url-loader', options: { limit: 10000, mimetype: 'application/font-woff', } }, }, // WOFF2 Font { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, use: { loader: 'url-loader', options: { limit: 10000, mimetype: 'application/font-woff', } } }, // TTF Font { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, use: { loader: 'url-loader', options: { limit: 10000, mimetype: 'application/octet-stream' } } }, // EOT Font { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader', }, // SVG Font { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, use: { loader: 'url-loader', options: { limit: 10000, mimetype: 'image/svg+xml', } } }, // Common Image Formats { test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, use: 'url-loader', } ] }, plugins: [ new webpack.DllReferencePlugin({ context: process.cwd(), manifest: require(manifest), sourceType: 'var', }), new webpack.HotModuleReplacementPlugin({ multiStep: true }), new webpack.NoEmitOnErrorsPlugin(), /** * Create global constants which can be configured at compile time. * * Useful for allowing different behaviour between development builds and * release builds * * NODE_ENV should be production so that modules do not perform certain * development checks * * By default, use 'development' as NODE_ENV. This can be overriden with * 'staging', for example, by changing the ENV variables in the npm scripts */ new webpack.EnvironmentPlugin({ NODE_ENV: 'development' }), new webpack.LoaderOptionsPlugin({ debug: true }), new ExtractTextPlugin({ filename: '[name].css' }), ], node: { __dirname: false, __filename: false }, devServer: { port, publicPath, compress: true, noInfo: true, stats: 'errors-only', inline: true, lazy: false, hot: true, headers: { 'Access-Control-Allow-Origin': '*' }, contentBase: path.join(__dirname, 'dist'), watchOptions: { aggregateTimeout: 300, ignored: /node_modules/, poll: 100 }, historyApiFallback: { verbose: true, disableDotRule: false, }, before() { if (process.env.START_HOT) { console.log('Starting Main Process...'); spawn( 'npm', ['run', 'start-main-dev'], { shell: true, env: process.env, stdio: 'inherit' } ) .on('close', code => process.exit(code)) .on('error', spawnError => console.error(spawnError)); } } }, }); ================================================ FILE: webpack.config.renderer.prod.js ================================================ /** * Build config for electron renderer process */ import path from 'path'; import webpack from 'webpack'; import ExtractTextPlugin from 'extract-text-webpack-plugin'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import merge from 'webpack-merge'; import UglifyJSPlugin from 'uglifyjs-webpack-plugin'; import baseConfig from './webpack.config.base'; import CheckNodeEnv from './internals/scripts/CheckNodeEnv'; CheckNodeEnv('production'); export default merge.smart(baseConfig, { devtool: 'source-map', target: 'electron-renderer', entry: './app/index', output: { path: path.join(__dirname, 'app/dist'), publicPath: './dist/', filename: 'renderer.prod.js' }, module: { rules: [ // Extract all .global.css to style.css as is { test: /\.global\.css$/, use: ExtractTextPlugin.extract({ publicPath: './', use: { loader: 'css-loader', options: { minimize: true, } }, fallback: 'style-loader', }) }, // Pipe other styles through css modules and append to style.css { test: /^((?!\.global).)*\.css$/, use: ExtractTextPlugin.extract({ use: { loader: 'css-loader', options: { modules: true, minimize: true, importLoaders: 1, localIdentName: '[name]__[local]__[hash:base64:5]', } } }), }, // Add SASS support - compile all .global.scss files and pipe it to style.css { test: /\.global\.(scss|sass)$/, use: ExtractTextPlugin.extract({ use: [ { loader: 'css-loader', options: { minimize: true, } }, { loader: 'sass-loader' } ], fallback: 'style-loader', }) }, // Add SASS support - compile all other .scss files and pipe it to style.css { test: /^((?!\.global).)*\.(scss|sass)$/, use: ExtractTextPlugin.extract({ use: [{ loader: 'css-loader', options: { modules: true, minimize: true, importLoaders: 1, localIdentName: '[name]__[local]__[hash:base64:5]', } }, { loader: 'sass-loader' }] }), }, // WOFF Font { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, use: { loader: 'url-loader', options: { limit: 10000, mimetype: 'application/font-woff', } }, }, // WOFF2 Font { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, use: { loader: 'url-loader', options: { limit: 10000, mimetype: 'application/font-woff', } } }, // TTF Font { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, use: { loader: 'url-loader', options: { limit: 10000, mimetype: 'application/octet-stream' } } }, // EOT Font { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader', }, // SVG Font { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, use: { loader: 'url-loader', options: { limit: 10000, mimetype: 'image/svg+xml', } } }, // Common Image Formats { test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, use: 'url-loader', } ] }, plugins: [ /** * Create global constants which can be configured at compile time. * * Useful for allowing different behaviour between development builds and * release builds * * NODE_ENV should be production so that modules do not perform certain * development checks */ new webpack.EnvironmentPlugin({ NODE_ENV: 'production' }), new UglifyJSPlugin({ parallel: true, sourceMap: true }), new ExtractTextPlugin('style.css'), new BundleAnalyzerPlugin({ analyzerMode: process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', openAnalyzer: process.env.OPEN_ANALYZER === 'true' }), ], });