├── tools ├── upx ├── upx-ia32 ├── upx-x64 └── upx.exe ├── src ├── assets │ ├── images │ │ ├── icon.icns │ │ ├── icon.ico │ │ ├── icon.png │ │ ├── icon@2x.png │ │ ├── icon@16px.png │ │ ├── trayOSXTemplate.png │ │ └── trayOSXTemplate@2x.png │ ├── fonts │ │ ├── iconfont.eot │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ ├── iconfont.css │ │ └── iconfont.svg │ └── scripts │ │ ├── win32.bat │ │ └── linux.sh ├── index.html ├── settings.html ├── js │ ├── settings.js │ ├── main.js │ ├── backend │ │ ├── event.js │ │ ├── log.js │ │ ├── permission.js │ │ ├── language.js │ │ ├── io.js │ │ ├── nw.interface.js │ │ ├── hosts.js │ │ ├── manifest.js │ │ └── update.js │ ├── components │ │ ├── SnackBar.js │ │ ├── SearchBox.js │ │ ├── SidebarItem.js │ │ ├── Titlebar.js │ │ ├── HostsInfoDialog.js │ │ ├── Editor.js │ │ ├── Sidebar.js │ │ ├── Settings.js │ │ └── App.js │ ├── lang │ │ ├── zh-CN.js │ │ └── en-US.js │ └── constants.js ├── sass │ ├── dragula.scss │ ├── react-select.scss │ ├── codemirror.scss │ ├── normalize.scss │ └── main.scss └── browser.js ├── .gitignore ├── webpack.config.js ├── LICENSE ├── package.json ├── README.md └── gulpfile.js /tools/upx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/tools/upx -------------------------------------------------------------------------------- /tools/upx-ia32: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/tools/upx-ia32 -------------------------------------------------------------------------------- /tools/upx-x64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/tools/upx-x64 -------------------------------------------------------------------------------- /tools/upx.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/tools/upx.exe -------------------------------------------------------------------------------- /src/assets/images/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/src/assets/images/icon.icns -------------------------------------------------------------------------------- /src/assets/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/src/assets/images/icon.ico -------------------------------------------------------------------------------- /src/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/src/assets/images/icon.png -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/src/assets/fonts/iconfont.eot -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/src/assets/fonts/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/images/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/src/assets/images/icon@2x.png -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/src/assets/fonts/iconfont.woff -------------------------------------------------------------------------------- /src/assets/images/icon@16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/src/assets/images/icon@16px.png -------------------------------------------------------------------------------- /src/assets/images/trayOSXTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/src/assets/images/trayOSXTemplate.png -------------------------------------------------------------------------------- /src/assets/images/trayOSXTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppoffice/Hozz/HEAD/src/assets/images/trayOSXTemplate@2x.png -------------------------------------------------------------------------------- /src/assets/scripts/win32.bat: -------------------------------------------------------------------------------- 1 | attrib -R %WINDIR%\\System32\\drivers\\etc\\hosts 2 | Icacls %WINDIR%\\System32\\drivers\\etc\\hosts /grant "%username%":F -------------------------------------------------------------------------------- /src/assets/scripts/linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # add a new user group for hosts editing 4 | groupadd hozz 5 | usermod -a -G hozz root 6 | usermod -a -G hozz $1 7 | chgrp hozz /etc/hosts 8 | chmod g+w /etc/hosts -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/js/settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import Lang from './backend/language'; 5 | import Manifest from './backend/manifest'; 6 | 7 | import Settings from './components/Settings'; 8 | 9 | const titleDOM = document.getElementsByTagName('title')[0]; 10 | titleDOM.innerText = 'Settings'; 11 | 12 | Manifest.loadFromDisk().then((manifest) => { 13 | if (manifest.language) { 14 | Lang.setLocale(manifest.language); 15 | } 16 | ReactDOM.render(, document.getElementById('app')); 17 | }); -------------------------------------------------------------------------------- /src/sass/dragula.scss: -------------------------------------------------------------------------------- 1 | .gu-mirror { 2 | position: fixed !important; 3 | margin: 0 !important; 4 | z-index: 9999 !important; 5 | opacity: 0.8; 6 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)"; 7 | filter: alpha(opacity=80); 8 | } 9 | .gu-hide { 10 | display: none !important; 11 | } 12 | .gu-unselectable { 13 | -webkit-user-select: none !important; 14 | -moz-user-select: none !important; 15 | -ms-user-select: none !important; 16 | user-select: none !important; 17 | } 18 | .gu-transit { 19 | opacity: 0.2; 20 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)"; 21 | filter: alpha(opacity=20); 22 | } 23 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import update from './backend/update'; 5 | import Lang from './backend/language'; 6 | import Manifest from './backend/manifest'; 7 | import { APP_NAME } from './constants'; 8 | 9 | import App from './components/App'; 10 | 11 | const titleDOM = document.getElementsByTagName('title')[0]; 12 | titleDOM.innerText = APP_NAME; 13 | 14 | Manifest.loadFromDisk().then((manifest) => { 15 | if (manifest.language) { 16 | Lang.setLocale(manifest.language); 17 | } 18 | ReactDOM.render(, document.getElementById('app')); 19 | }); 20 | 21 | update(false); -------------------------------------------------------------------------------- /src/js/backend/event.js: -------------------------------------------------------------------------------- 1 | const events = new Map(); 2 | 3 | export default { 4 | on (event, listener) { 5 | if (!events.has(event)) { 6 | events.set(event, new Set()); 7 | } 8 | const listeners = events.get(event); 9 | listeners.add(listener); 10 | }, 11 | 12 | off (event, listener) { 13 | if (events.has(event)) { 14 | const listeners = events.get(event); 15 | listeners.delete(listener); 16 | } 17 | }, 18 | 19 | emit (event, data) { 20 | if (events.has(event)) { 21 | const listeners = events.get(event); 22 | for (let listener of listeners) { 23 | listener.call(null, data); 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # built files 37 | /app 38 | /build 39 | 40 | *~ 41 | 42 | # Jetbrains webstorm files 43 | /.idea 44 | -------------------------------------------------------------------------------- /src/js/backend/log.js: -------------------------------------------------------------------------------- 1 | import io from './io'; 2 | import { LOG } from '../constants'; 3 | 4 | /** 5 | * Format Date to String 6 | * @author: meizz 7 | */ 8 | const dateFormat = (date, fmt) => { 9 | var o = { 10 | "M+": date.getMonth() + 1, 11 | "d+": date.getDate(), 12 | "h+": date.getHours(), 13 | "m+": date.getMinutes(), 14 | "s+": date.getSeconds(), 15 | "q+": Math.floor((date.getMonth() + 3) / 3), 16 | "S": date.getMilliseconds() 17 | }; 18 | if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length)); 19 | for (var k in o) 20 | if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); 21 | return fmt; 22 | } 23 | 24 | export default function (error) { 25 | const date = dateFormat(new Date(), 'yyyy-M-d h:m:s.S'); 26 | io.appendFile(LOG, `[${ date }]\n${ error.stack }\n`); 27 | console.log(error); 28 | }; -------------------------------------------------------------------------------- /src/js/components/SnackBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | class SnackBar extends Component { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | const { type, text, actions, onDismiss } = this.props; 10 | const actionButtons = actions ? actions.map((action, index) => { 11 | return ({ action.name }) 12 | }) : null; 13 | return (
14 | { text } 15 |
16 | { actionButtons } 17 | 18 |
19 |
); 20 | } 21 | } 22 | 23 | SnackBar.propTypes = { 24 | type: PropTypes.string, 25 | text: PropTypes.string, 26 | actions: PropTypes.array, 27 | onDismiss: PropTypes.func, 28 | } 29 | 30 | export default SnackBar; 31 | -------------------------------------------------------------------------------- /src/js/backend/permission.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | import Sudoer from 'electron-sudo'; 4 | import { APP_NAME } from "../constants"; 5 | 6 | const OPTIONS = { 7 | name: APP_NAME, 8 | icns: path.join(global.__dirname, './assets/images/icon.icns') 9 | }; 10 | const ENV = { 'LANG': 'en_US.UTF-8' }; 11 | 12 | const enableFullAccess = () => { 13 | let command; 14 | const sudoer = new Sudoer(OPTIONS); 15 | switch (process.platform) { 16 | case 'win32': 17 | command = "\"" + path.join(global.__dirname, './assets/scripts/win32.bat') + "\""; 18 | break; 19 | case 'darwin': 20 | command = '/bin/chmod +a "`/usr/bin/whoami` allow read,write" /etc/hosts'; 21 | break; 22 | case 'linux': 23 | command = '/bin/sh ' + path.join(global.__dirname, './assets/scripts/linux.sh') + ' ' + process.env.USER; 24 | break; 25 | default: 26 | command = ''; 27 | break; 28 | } 29 | return sudoer.exec(command, { env: ENV }); 30 | }; 31 | 32 | export default { 33 | enableFullAccess, 34 | }; 35 | -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face {font-family: "iconfont"; 3 | src: url('iconfont.eot'); /* IE9*/ 4 | src: url('iconfont.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url('iconfont.woff') format('woff'), /* chrome, firefox */ 6 | url('iconfont.ttf') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 7 | url('iconfont.svg#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family:"iconfont" !important; 12 | font-size:16px; 13 | font-style:normal; 14 | -webkit-font-smoothing: antialiased; 15 | -webkit-text-stroke-width: 0.2px; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | .icon-guanbi:before { content: "\e602"; } 19 | .icon-sousuochaxun:before { content: "\e601"; } 20 | .icon-icon30:before { content: "\e607"; } 21 | .icon-icon31:before { content: "\e606"; } 22 | .icon-add:before { content: "\e600"; } 23 | .icon-shezhi:before { content: "\e605"; } 24 | .icon-ok:before { content: "\e60a"; } 25 | .icon-edit:before { content: "\e603"; } 26 | .icon-delete:before { content: "\e608"; } 27 | .icon-zuidahua:before { content: "\e609"; } 28 | .icon-cloud:before { content: "\e604"; } 29 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | cache: true, 6 | target: 'electron', 7 | devtool: 'source-map', 8 | entry: { 9 | main: './src/js/main', 10 | settings: './src/js/settings', 11 | }, 12 | output: { 13 | path: path.join(__dirname, 'app'), 14 | filename: '[name].js', 15 | chunkFilename: '[chunkhash].js', 16 | sourceMapFilename: '[name].map' 17 | }, 18 | node: { 19 | __dirname: false 20 | }, 21 | module: { 22 | loaders: [ 23 | { 24 | loader: 'babel-loader', 25 | include: [ 26 | path.resolve(__dirname, 'src/js'), 27 | ], 28 | 29 | // Only run `.js` and `.jsx` files through Babel 30 | test: /\.js|\.jsx?$/, 31 | 32 | // Options to configure babel with 33 | query: { 34 | presets: ['es2015', 'react'], 35 | } 36 | }, 37 | { 38 | loader: 'json-loader', 39 | test: /\.json?$/, 40 | } 41 | ] 42 | }, 43 | plugins: [ 44 | new webpack.optimize.DedupePlugin(), 45 | new webpack.optimize.UglifyJsPlugin({comments: false}), 46 | ] 47 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For Hozz software 4 | 5 | Copyright (c) 2013-present, PPOffice. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without modification, 9 | are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name PPOffice nor the names of its contributors may be used to 19 | endorse or promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 26 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hozz", 3 | "version": "0.1.4", 4 | "description": "A better way to manage your hosts.", 5 | "main": "browser.js", 6 | "scripts": { 7 | "start": "electron ./app/browser.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:ppoffice/Hozz.git" 12 | }, 13 | "keywords": [ 14 | "Electron", 15 | "hosts", 16 | "all-platform" 17 | ], 18 | "author": "PPOffice", 19 | "license": "BSD-3-Clause", 20 | "bugs": { 21 | "url": "https://github.com/ppoffice/Hozz/issues" 22 | }, 23 | "homepage": "https://github.com/ppoffice/Hozz#readme", 24 | "devDependencies": { 25 | "babel-core": "^6.3.26", 26 | "babel-loader": "^6.2.0", 27 | "babel-preset-es2015": "^6.3.13", 28 | "babel-preset-react": "^6.3.13", 29 | "classnames": "^2.2.1", 30 | "codemirror": "^5.10.0", 31 | "electron-packager": "^5.2.0", 32 | "electron-prebuilt": "^0.36.0", 33 | "gulp": "^3.9.0", 34 | "gulp-clean": "^0.3.1", 35 | "gulp-convert-newline": "0.0.3", 36 | "gulp-sass": "^2.1.1", 37 | "gulp-util": "^3.0.7", 38 | "json-loader": "^0.5.4", 39 | "jszip": "^2.5.0", 40 | "keymirror": "^0.1.1", 41 | "mkdirp": "^0.5.1", 42 | "node-fetch": "^1.3.3", 43 | "react": "^0.14.3", 44 | "react-codemirror": "^0.2.2", 45 | "react-dom": "^0.14.3", 46 | "react-dragula": "^1.1.9", 47 | "react-dropzone": "^3.3.0", 48 | "react-select": "^1.0.0-beta8", 49 | "uid": "0.0.2", 50 | "webpack": "^1.12.9" 51 | }, 52 | "dependencies": { 53 | "electron-sudo": "git+https://github.com/ppoffice/electron-sudo.git" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/js/components/SearchBox.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Lang from '../backend/language'; 4 | 5 | class SearchBox extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | text: '', 10 | }; 11 | } 12 | 13 | __onInputChange (e) { 14 | const { onTextChange } = this.props; 15 | this.setState({ text: e.target.value }); 16 | onTextChange && onTextChange(e.target.value); 17 | } 18 | 19 | __onClearClick () { 20 | const { onTextChange } = this.props; 21 | this.setState({ text: '' }); 22 | onTextChange && onTextChange(''); 23 | } 24 | 25 | render() { 26 | let __cxArray = ['searchbox']; 27 | const { text } = this.state; 28 | const { className } = this.props; 29 | if (className) { 30 | __cxArray = __cxArray.concat(...className.split(' ')); 31 | } 32 | return (
33 | 34 | 39 | { text ? : null } 41 |
); 42 | } 43 | } 44 | 45 | SearchBox.propTypes = { 46 | className: PropTypes.string, 47 | onTextChange: PropTypes.func, 48 | } 49 | 50 | export default SearchBox; 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hozz 2 | #### A Better Way to Manage Your Hosts. [Homepage](http://ppoffice.github.io/Hozz) 3 | ![](https://ooo.0o0.ooo/2016/01/01/56868691bd272.png) 4 | --- 5 | 6 | ## Features 7 | * Provide swift switch between different hosts files 8 | * Automatic update of online hosts 9 | * Import hosts file by dragging them into the sidebar 10 | * Export and import hosts files 11 | * Support hosts sorting through dragging 12 | * Export hosts file to [Surge](https://surge.run/manual/) config file 13 | 14 | ## Known Issues 15 | * Hosts will not save on sidebar status switch/edit button clicking 16 | * Wrong window height/width when maximized on Windows 17 | * Window get ghost shadows sometimes on OS X 18 | * Get black background on startup on Linux due to graphics issues ([Electron#2170](https://github.com/atom/electron/issues/2170)), and this will soon disappear 19 | * If the tray icon does not appears on Linux, you need to install `libappindicator1` according to [Electron#1347](https://github.com/atom/electron/issues/1347) 20 | 21 | ## Development 22 | 23 | ### Requirements: 24 | 25 | * Node.js 26 | * Gulp 27 | 28 | ### Get the code: 29 | ``` 30 | git clone https://github.com/ppoffice/Hozz.git 31 | cd Hozz 32 | npm install 33 | ``` 34 | 35 | ### Commands: 36 | 37 | * **gulp**: Compile, build and copy files to /app 38 | * **gulp clean**: Delete the built files, including /app and /build 39 | * **gulp watch**: Watch the /src directory and automatically build on file change 40 | * **gulp package**: Pack with Electron for releasing(ia32 and x64), need to run **gulp clean** before this 41 | * **gulp package-uncompressed**: Same as the former one but with no file deleted or compressed. This is for some legacy system like Windows 7 42 | 43 | ## Todos 44 | - [x] Multilanguage support 45 | - [ ] Manifest and hosts sync based on cloud services 46 | - [ ] Better text searching and editing experience 47 | -------------------------------------------------------------------------------- /src/js/components/SidebarItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import Lang from '../backend/language'; 5 | 6 | class SidebarItem extends Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | __updateState (e) { 12 | e.stopPropagation(); 13 | const { onStatusChange } = this.props; 14 | onStatusChange && onStatusChange(); 15 | } 16 | 17 | render() { 18 | const { item, active, onEdit, onClick, onRemove } = this.props; 19 | const classNames = cx({ 20 | 'sidebar-item': true, 21 | 'active': active, 22 | }); 23 | const statusClassNames = cx({ 24 | 'status': true, 25 | 'online': item.online, 26 | }); 27 | return (
28 | 29 |
30 |

{ item.name }

31 |

32 | { !!item.url ? : null} 33 | { Lang.get('main.hosts_rules', item.count) } 34 |

35 |
36 | { onEdit ? : null } 37 | { onRemove ? : null } 38 |
); 39 | } 40 | } 41 | 42 | SidebarItem.propTypes = { 43 | item: PropTypes.object, 44 | active: PropTypes.bool, 45 | onEdit: PropTypes.func, 46 | onClick: PropTypes.func, 47 | onRemove: PropTypes.func, 48 | onStatusChange: PropTypes.func, 49 | }; 50 | 51 | export default SidebarItem; 52 | -------------------------------------------------------------------------------- /src/js/backend/language.js: -------------------------------------------------------------------------------- 1 | import enUS from '../lang/en-US.js'; 2 | import zhCN from '../lang/zh-CN.js'; 3 | 4 | const getNavigatorLanguage = () => { 5 | let languages = navigator.languages; 6 | for (let i = 0; i < languages.length; i++) { 7 | let locale = getUnifiedLocale(languages[i]); 8 | if (locale) { 9 | return locale; 10 | } 11 | } 12 | } 13 | 14 | const getUnifiedLocale = (locale) => { 15 | for (let key in lang) { 16 | if (key.includes(locale)) 17 | { 18 | return key; 19 | } 20 | } 21 | } 22 | 23 | let locale = getNavigatorLanguage() || 'en-US'; 24 | let lang = { 25 | 'en-US': { name: 'English(US)', content: enUS }, 26 | 'zh-CN': { name: '简体中文', content: zhCN }, 27 | }; 28 | 29 | export default { 30 | setLocale (__locale) { 31 | locale = getUnifiedLocale(__locale) || 'en-US'; 32 | }, 33 | 34 | getCurrentLocale () { 35 | for (let key in lang) { 36 | if (key === locale) { 37 | return { value: key, label: lang[key]['name'] }; 38 | } 39 | } 40 | return { value: 'en-US', label: 'English(US)' }; 41 | }, 42 | 43 | getLocales () { 44 | const locales = []; 45 | for (let key in lang) { 46 | locales.push({ value: key, label: lang[key]['name'] }); 47 | } 48 | return locales; 49 | }, 50 | 51 | get (key, ...fills) { 52 | const domains = key.split('.'); 53 | let value = lang[locale]['content'] || lang['en-US']['content']; 54 | if (domains.length > 1) { 55 | for (let i = 0; i < domains.length; i++) { 56 | if (!value[domains[i]]) { 57 | value = domains[i]; 58 | break; 59 | } 60 | value = value[domains[i]]; 61 | } 62 | } 63 | const __fills = [].concat(fills); 64 | return value.replace(/\$\$[\d]+/g, (match) => { 65 | if (__fills.length) { 66 | return __fills.shift(); 67 | } 68 | return match; 69 | }); 70 | }, 71 | } -------------------------------------------------------------------------------- /src/js/lang/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | 'ok': '确定', 4 | 'exit': '退出', 5 | 'cancel': '取消', 6 | 'success': '成功', 7 | 'warning': '警告', 8 | 9 | 'settings': '设置', 10 | 11 | 'name': '名称', 12 | 'search': '搜索', 13 | 14 | 'text': '文本文件', 15 | 'all_files': '所有文件', 16 | 17 | 'update_complete': '更新完成', 18 | 'no_update_found': '未找到更新', 19 | 'update_available': '更新可用', 20 | 'found_new_version': '发现新版本: $$1', 21 | 'using_latest_release': '你现在正使用最新版本。', 22 | 'confirm_update': '你想要升级到最新版么?', 23 | 'restart_to_update': '请重新启动$$1使更新生效', 24 | 25 | 'update_failed': '更新失败', 26 | 'check_update_failed': '检查更新失败。', 27 | 'apply_update_failed': '应用更新失败。', 28 | 'download_update_failed': '下载更新失败。', 29 | 'cannot_find_core_package': '无法找到核心更新包。', 30 | 'go_to_homepage': '请前往$$1下载最新更新。', 31 | }, 32 | main: { 33 | 'grant_permission': '授予权限', 34 | 'dont_have_permission': '你没有写入系统Hosts文件的权限。', 35 | 'have_to_logout_for_permission': '你需要注销并重新登录来使权限变更生效。', 36 | 37 | 'edit_hosts': '编辑Hosts', 38 | 'create_new_hosts': '新建Hosts', 39 | 'remote_source_url': '远程Hosts地址(可选)', 40 | 41 | 'hosts_rules': '$$1条记录', 42 | }, 43 | settings: { 44 | 'import': '导入', 45 | 'import_error': '导入失败', 46 | 'import_from_zip': '从Zip中导入', 47 | 'import_complete': '导入完成', 48 | 'confirm_continue': '你想要继续么?', 49 | 'confirm_a_valid_file': '你打开的不是一个有效的备份文件。', 50 | 'overridden_warning': '你现有的文件将会被覆盖。', 51 | 52 | 'export': '导出', 53 | 'export_to_zip': '导出到Zip', 54 | 'export_to_surge': '导出到Surge', 55 | 56 | 'language': '语言', 57 | 'language_changed': '语言已更改', 58 | 59 | 'about': '关于', 60 | 'homepage': '主页', 61 | 'check_update': '检查更新', 62 | 'checking_update': '检查更新中', 63 | 'applying_update': '应用更新中', 64 | 'current_version': '当前版本: $$1', 65 | 'downloading_update': '下载更新中', 66 | 67 | 'please_restart_app': '请重新启动$$1使变更生效。', 68 | }, 69 | } -------------------------------------------------------------------------------- /src/js/backend/io.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | import fetch from 'node-fetch'; 3 | 4 | const io = {}; 5 | 6 | const promisefy = (method) => { 7 | return (...args) => { 8 | return new Promise((resolve, reject) => { 9 | fs[method](...args, (...__args) => { 10 | const error = __args.splice(0, 1)[0]; 11 | if (error) { 12 | reject(error); 13 | } else { 14 | resolve(...__args); 15 | } 16 | }); 17 | }); 18 | } 19 | } 20 | const fsMethods = ['readFile', 'writeFile', 'appendFile', 'unlink'].forEach((method) => { 21 | io[method] = promisefy(method); 22 | }); 23 | 24 | const readDropFiles = (files) => { 25 | return files.map((file) => { 26 | if (!file.type.match('text.*')) { 27 | return Promise.resolve(null); 28 | } 29 | const reader = new FileReader(); 30 | return new Promise((resolve, reject) => { 31 | reader.onload = (e) => { 32 | resolve({ 33 | name: file.name, 34 | text: e.target.result 35 | }); 36 | }; 37 | reader.onerror = (error) => { 38 | reject(error); 39 | }; 40 | reader.readAsText(file); 41 | }); 42 | }); 43 | } 44 | 45 | const requestUrl = (url) => { 46 | return fetch(url).then((response) => { 47 | return Promise.resolve(response.text()); 48 | }); 49 | } 50 | 51 | const downloadUrl = (url) => { 52 | return fetch(url).then((response) => { 53 | return new Promise((resolve, reject) => { 54 | const bufs = []; 55 | const readable = response.body; 56 | readable.on('data', (b) => { 57 | bufs.push(b); 58 | }); 59 | readable.on('end', () => { 60 | resolve(Buffer.concat(bufs)); 61 | }); 62 | readable.on('error', (e) => { 63 | reject(e); 64 | }); 65 | }); 66 | }); 67 | } 68 | 69 | export default Object.assign(io, { 70 | requestUrl, 71 | downloadUrl, 72 | readDropFiles, 73 | }); -------------------------------------------------------------------------------- /src/js/components/Titlebar.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import event from '../backend/event'; 4 | import { EVENT } from '../constants'; 5 | 6 | const appContainer = document.getElementById('app'); 7 | 8 | class Titlebar extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | isMaximized: false, 13 | }; 14 | } 15 | 16 | __onWindowMaximize () { 17 | this.setState({ isMaximized: true }); 18 | appContainer.classList.add('maximized'); 19 | } 20 | 21 | __onWindowUnmaximize () { 22 | this.setState({ isMaximized: false }); 23 | appContainer.classList.remove('maximized'); 24 | } 25 | 26 | componentDidMount () { 27 | event.on(EVENT.WINDOW_MAXIMIZED, this.__onWindowMaximize.bind(this)); 28 | event.on(EVENT.WINDOW_UNMAXIMIZED, this.__onWindowUnmaximize.bind(this)); 29 | } 30 | 31 | componentWillUnmount () { 32 | event.off(EVENT.WINDOW_MAXIMIZED, this.__onWindowMaximize.bind(this)); 33 | event.off(EVENT.WINDOW_UNMAXIMIZED, this.__onWindowUnmaximize.bind(this)); 34 | } 35 | 36 | render() { 37 | const { isMaximized } = this.state; 38 | const { closeAsHide, disableMaximize } = this.props; 39 | let maximizeButton = null; 40 | if (!disableMaximize) { 41 | maximizeButton = isMaximized ? 42 | : 43 | 44 | } 45 | return (
46 |
47 | { this.props.title } 48 |
49 |
50 | 51 | { maximizeButton } 52 | { closeAsHide ? 53 | : 54 | } 55 |
56 |
); 57 | } 58 | } 59 | 60 | Titlebar.propTypes = { 61 | title: PropTypes.string, 62 | closeAsHide: PropTypes.bool, 63 | disableMaximize: PropTypes.bool, 64 | }; 65 | 66 | export default Titlebar; 67 | -------------------------------------------------------------------------------- /src/js/lang/en-US.js: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | 'ok': 'OK', 4 | 'exit': 'Exit', 5 | 'cancel': 'Cancel', 6 | 'success': 'Success', 7 | 'warning': 'Warning', 8 | 9 | 'settings': 'Settings', 10 | 11 | 'name': 'Name', 12 | 'search': 'Search', 13 | 14 | 'text': 'Text', 15 | 'all_files': 'All Files', 16 | 17 | 'update_complete': 'Update Complete', 18 | 'no_update_found': 'No Update Found', 19 | 'update_available': 'Update Available', 20 | 'found_new_version': 'Found New Version: $$1', 21 | 'using_latest_release': 'You are using the latest release.', 22 | 'confirm_update': 'Do you want to update to the latest version?', 23 | 'restart_to_update': 'Please restart $$1 for this update to take effect.', 24 | 25 | 'update_failed': 'Update failed', 26 | 'check_update_failed': 'Check update failed.', 27 | 'apply_update_failed': 'Apply update failed.', 28 | 'download_update_failed': 'Download update failed.', 29 | 'cannot_find_core_package': 'Cannot find core package.', 30 | 'go_to_homepage': 'Please go to $$1 to download latest release.', 31 | }, 32 | main: { 33 | 'grant_permission': 'Grant Permission', 34 | 'dont_have_permission': 'You don\'t have the permission to write to hosts file.', 35 | 'have_to_logout_for_permission': 'You have to logout and login again for the permission change to take effect.', 36 | 37 | 'edit_hosts': 'Edit Hosts', 38 | 'create_new_hosts': 'Create New Hosts', 39 | 'remote_source_url': 'Remote Source Url (Optional)', 40 | 41 | 'hosts_rules': '$$1 Rules', 42 | }, 43 | settings: { 44 | 'import': 'Import', 45 | 'import_error': 'Import error', 46 | 'import_from_zip': 'Import from Zip', 47 | 'import_complete': 'Import complete', 48 | 'confirm_continue': 'Do you want to continue?', 49 | 'confirm_a_valid_file': 'Did you open a valid file?', 50 | 'overridden_warning': 'Your current files will be overridden.', 51 | 52 | 'export': 'Export', 53 | 'export_to_zip': 'Export to Zip', 54 | 'export_to_surge': 'Export to Surge', 55 | 56 | 'language': 'Language', 57 | 'language_changed': 'Language changed', 58 | 59 | 'about': 'About', 60 | 'homepage': 'Homepage', 61 | 'check_update': 'Check Update', 62 | 'checking_update': 'Checking Update', 63 | 'applying_update': 'Applying Update', 64 | 'current_version': 'Current Version: $$1', 65 | 'downloading_update': 'Downloading Update', 66 | 67 | 'please_restart_app': 'Please restart $$1 for this change to take effect.', 68 | }, 69 | } -------------------------------------------------------------------------------- /src/js/components/HostsInfoDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Lang from '../backend/language'; 4 | 5 | class HostsInfoDialog extends Component { 6 | constructor(props) { 7 | super(props); 8 | const { url, name } = props; 9 | this.state = { 10 | url, 11 | name, 12 | }; 13 | } 14 | 15 | __onNameChange (e) { 16 | const { onInputChange } = this.props; 17 | this.setState({ name: e.target.value }); 18 | onInputChange && onInputChange(e.target.value, this.state.url); 19 | } 20 | 21 | __onUrlChange (e) { 22 | const { onInputChange } = this.props; 23 | this.setState({ url: e.target.value }); 24 | onInputChange && onInputChange(this.state.name, e.target.value); 25 | } 26 | 27 | __onPressEnter (e) { 28 | const { onHostDialogOK } = this.props; 29 | onHostDialogOK && e.keyCode === 13 && onHostDialogOK(); 30 | } 31 | 32 | render() { 33 | const { name, url, onDismiss } = this.props; 34 | return (
35 |
36 |
37 | { 38 | !name ? 39 | Lang.get('main.create_new_hosts') : 40 | Lang.get('main.edit_hosts') } 41 | 42 |
43 |
44 | 50 | 56 |
57 |
58 |
59 |
); 60 | } 61 | } 62 | 63 | HostsInfoDialog.propTypes = { 64 | url: PropTypes.string, 65 | name: PropTypes.string, 66 | onDismiss: PropTypes.func, 67 | onInputChange: PropTypes.func, 68 | onHostDialogOK: PropTypes.func, 69 | } 70 | 71 | export default HostsInfoDialog; 72 | -------------------------------------------------------------------------------- /src/js/backend/nw.interface.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const electron = global.require('electron'); 3 | export const remote = electron.remote; 4 | const Menu = remote.Menu; 5 | const Tray = remote.Tray; 6 | 7 | export const app = remote.app; 8 | export const shell = remote.shell; 9 | export const dialog = remote.dialog; 10 | export const ipcRenderer = electron.ipcRenderer; 11 | export const BrowserWindow = remote.BrowserWindow; 12 | 13 | import log from './log'; 14 | import event from './event'; 15 | import Lang from './language'; 16 | import { APP_NAME, EVENT } from '../constants'; 17 | 18 | const terminate = remote.getGlobal('terminate'); 19 | const settingsWindow = remote.getGlobal('settingsWindow'); 20 | 21 | const window = () => { 22 | return remote.getCurrentWindow(); 23 | } 24 | 25 | const focusWindow = (browserWindow) => { 26 | if (!browserWindow.isVisible()) { 27 | browserWindow.show(); 28 | } 29 | if (browserWindow.isMinimized()) { 30 | browserWindow.restore(); 31 | } 32 | browserWindow.focus(); 33 | setDockIconVisibility(); 34 | } 35 | 36 | const focusCurrentWindow = () => { 37 | focusWindow(window()); 38 | } 39 | 40 | const setDockIconVisibility = () => { 41 | const windows = BrowserWindow.getAllWindows(); 42 | if (windows.some(win => win.isVisible())) { 43 | app.dock.show(); 44 | } else { 45 | app.dock.hide(); 46 | } 47 | } 48 | 49 | let appIcon; 50 | 51 | event.on(EVENT.SET_HOSTS_MENU, (__menus) => { 52 | if (!appIcon) { 53 | if (process.platform === 'darwin') { 54 | appIcon = new Tray(path.join(global.__dirname, './assets/images/trayOSXTemplate.png')); 55 | } else { 56 | appIcon = new Tray(path.join(global.__dirname, './assets/images/icon@16px.png')); 57 | } 58 | appIcon.setToolTip(APP_NAME); 59 | appIcon.on('click', focusCurrentWindow); 60 | } 61 | const menus = [ 62 | { label: APP_NAME, click: focusCurrentWindow }, 63 | { type: 'separator' }, 64 | ...__menus, 65 | { type: 'separator' }, 66 | { label: Lang.get('common.exit'), click: () => { 67 | terminate(); 68 | } } 69 | ]; 70 | const contextMenu = Menu.buildFromTemplate(menus); 71 | appIcon.setContextMenu(contextMenu); 72 | }); 73 | 74 | event.on(EVENT.CLOSE_WINDOW, () => { 75 | window().close(); 76 | }); 77 | event.on(EVENT.HIDE_WINDOW, () => { 78 | window().hide(); 79 | setDockIconVisibility(); 80 | }); 81 | event.on(EVENT.MINIMIZE_WINDOW, () => { 82 | window().minimize(); 83 | }); 84 | event.on(EVENT.MAXIMIZE_WINDOW, () => { 85 | window().isMaximized() ? window().unmaximize() : window().maximize(); 86 | }); 87 | 88 | event.on(EVENT.OPEN_SETTINGS_WINDOW, () => { 89 | if (settingsWindow) { 90 | focusWindow(settingsWindow); 91 | } 92 | }); 93 | 94 | event.on(EVENT.OPEN_EXTERNAL_URL, (url) => { 95 | shell.openExternal(url); 96 | }); 97 | 98 | window().on('maximize', event.emit.bind(null, EVENT.WINDOW_MAXIMIZED)); 99 | window().on('unmaximize', event.emit.bind(null, EVENT.WINDOW_UNMAXIMIZED)); 100 | 101 | export default {}; -------------------------------------------------------------------------------- /src/js/backend/hosts.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | import UID from 'uid'; 3 | 4 | import io from './io'; 5 | import log from './log'; 6 | import { HOSTS_COUNT_MATHER, 7 | TOTAL_HOSTS_UID, 8 | WORKSPACE } from '../constants'; 9 | 10 | const countRules = (text) => { 11 | let ret = null; 12 | let count = 0; 13 | while ((ret = HOSTS_COUNT_MATHER.exec(text)) !== null) { 14 | count++; 15 | } 16 | return count; 17 | } 18 | 19 | class Hosts { 20 | constructor (options) { 21 | const { index, uid, name, online, url, count, text } = options; 22 | this.index = index || 0; 23 | this.uid = uid || UID(16); 24 | this.name = name || ''; 25 | this.online = online || false; 26 | this.url = url || ''; 27 | this.count = count || 0; 28 | if (uid === TOTAL_HOSTS_UID) { 29 | this.text = text; 30 | } else { 31 | this.setText(text || ''); 32 | } 33 | this.isSyncing = false; 34 | } 35 | 36 | toObject () { 37 | return { 38 | uid: this.uid, 39 | url: this.url, 40 | name: this.name, 41 | text: this.text, 42 | index: this.index, 43 | online: this.online, 44 | count: this.count, 45 | }; 46 | } 47 | 48 | setText (text) { 49 | this.text = text; 50 | this.count = countRules(text); 51 | } 52 | 53 | toggleStatus () { 54 | this.online = !this.online; 55 | } 56 | 57 | stashStatus () { 58 | if (typeof(this.__online) === 'undefined') { 59 | this.__online = this.online; 60 | this.online = false; 61 | } 62 | } 63 | 64 | popStatus () { 65 | if (typeof(this.__online) !== 'undefined') { 66 | this.online = this.__online; 67 | delete this.__online; 68 | } 69 | } 70 | 71 | save () { 72 | if (!this.uid || this.uid === TOTAL_HOSTS_UID) { 73 | return Promise.resolve(); 74 | } 75 | return io.writeFile(path.join(WORKSPACE, this.uid), this.text); 76 | } 77 | 78 | remove () { 79 | if (!this.uid || this.uid === TOTAL_HOSTS_UID) { 80 | return Promise.resolve(); 81 | } 82 | return io.unlink(path.join(WORKSPACE, this.uid)); 83 | } 84 | 85 | load () { 86 | if (this.uid && this.uid !== TOTAL_HOSTS_UID) { 87 | return io.readFile(path.join(WORKSPACE, this.uid), 'utf-8').then((text) => { 88 | this.setText(text); 89 | return Promise.resolve(); 90 | }).catch(log); 91 | } else { 92 | return Promise.resolve(); 93 | } 94 | } 95 | 96 | updateFromUrl () { 97 | if (this.url) { 98 | this.isSyncing = true; 99 | return io.requestUrl(this.url).then((text) => { 100 | this.setText(text); 101 | this.isSyncing = false; 102 | return this.save(); 103 | }).catch((error) => { 104 | log(error); 105 | this.isSyncing = false; 106 | return Promise.resolve(); 107 | }); 108 | } else { 109 | return Promise.resolve(); 110 | } 111 | } 112 | } 113 | 114 | Hosts.createFromText = (text) => { 115 | return new Hosts({ 116 | name: 'New Hosts', 117 | online: false, 118 | url: '', 119 | text, 120 | }); 121 | } 122 | 123 | export default Hosts; -------------------------------------------------------------------------------- /src/js/components/Editor.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import CodeMirror from 'codemirror'; 3 | import ReactCodeMirror from 'react-codemirror'; 4 | 5 | import { HOSTS_MATHER } from '../constants'; 6 | 7 | CodeMirror.defineMode('hosts', function () { 8 | return { 9 | token: function (stream) { 10 | const c = stream.peek(); 11 | let token_name; 12 | if (c === '#') { 13 | token_name = 'comment'; 14 | stream.skipToEnd(); 15 | } else if (!stream.string.match(HOSTS_MATHER)) { 16 | token_name = 'error'; 17 | stream.skipToEnd(); 18 | } else { 19 | if (!stream.skipTo('#')) { 20 | token_name = stream.skipToEnd(); 21 | } 22 | } 23 | return token_name; 24 | }, 25 | lineComment: '#' 26 | }; 27 | }); 28 | 29 | const codemirrorOptions = (saveCallBack) => { 30 | return { 31 | mode: 'hosts', 32 | lineNumbers: true, 33 | extraKeys: { 34 | "Ctrl-/": function(instance) { 35 | if (instance.options.readOnly) return; 36 | let doc = instance.doc; 37 | var cursor = doc.getCursor(); 38 | let lineHandle = doc.getLineHandle(cursor.line); 39 | if (lineHandle.text.trim().startsWith("#")) { 40 | lineHandle.text = lineHandle.text.trim().substring(1); 41 | } else { 42 | lineHandle.text = '#' + lineHandle.text; 43 | } 44 | doc.replaceRange(lineHandle.text, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineHandle.text.length}); 45 | doc.setCursor(cursor.line, lineHandle.text.length); 46 | }, 47 | "Ctrl-S": function(instance) { 48 | if (instance.options.readOnly) return; 49 | saveCallBack(); 50 | } 51 | } 52 | } 53 | }; 54 | 55 | class Editor extends Component { 56 | constructor(props) { 57 | super(props); 58 | this.codemirrorOptions = codemirrorOptions(this.__onFocusChange.bind(this, false)); 59 | this.state = { 60 | value: props.value 61 | }; 62 | } 63 | 64 | componentWillReceiveProps (props) { 65 | this.setState({ value: props.value }); 66 | this.codemirrorOptions = Object.assign({}, codemirrorOptions, { 67 | readOnly: props.readOnly 68 | }); 69 | } 70 | 71 | componentWillUnmount () { 72 | const { uid, onTextShouldUpdate } = this.props; 73 | onTextShouldUpdate && onTextShouldUpdate(uid, this.state.value); 74 | } 75 | 76 | __onChange (value) { 77 | this.setState({ value }); 78 | } 79 | 80 | __onFocusChange (focused) { 81 | if (!focused) { 82 | const { value } = this.state; 83 | const { uid, onTextShouldUpdate } = this.props; 84 | onTextShouldUpdate && onTextShouldUpdate(uid, value); 85 | } 86 | } 87 | 88 | render() { 89 | return (
90 | 96 |
); 97 | } 98 | } 99 | 100 | Editor.propTypes = { 101 | uid: PropTypes.string, 102 | value: PropTypes.string, 103 | readOnly: PropTypes.bool, 104 | onTextShouldUpdate: PropTypes.func, 105 | } 106 | 107 | export default Editor; 108 | -------------------------------------------------------------------------------- /src/js/constants.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | import keymirror from 'keymirror'; 4 | 5 | export const APP_NAME = 'Hozz'; 6 | export const APP_VERSION = '0.1.4'; 7 | export const APP_AUTHER = 'PPOffice'; 8 | export const APP_HOMEPAGE = 'https://ppoffice.github.io/Hozz'; 9 | export const APP_RELEASES_URL = `https://api.github.com/repos/ppoffice/${ APP_NAME }/releases`; 10 | 11 | export const HOSTS_MATHER = 12 | /^\s*((?:(?:25[0-5]|2[0-4]\d|[0-1]?\d{1,2})\.){3}(?:25[0-5]|2[0-4]\d|[0-1]?\d{1,2})|(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}(?:(?:[0-9A-Fa-f]{1,4})|:))|(?:(?:[0-9A-Fa-f]{1,4}:){6}(?::|(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})|(?::[0-9A-Fa-f]{1,4})))|(?:(?:[0-9A-Fa-f]{1,4}:){5}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:){4}(?::[0-9A-Fa-f]{1,4}){0,1}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:){3}(?::[0-9A-Fa-f]{1,4}){0,2}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:){2}(?::[0-9A-Fa-f]{1,4}){0,3}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:)(?::[0-9A-Fa-f]{1,4}){0,4}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?::(?::[0-9A-Fa-f]{1,4}){0,5}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})))(?:%.+)?)(\s+([\*|\w|\.|-]+))+?\s*(?:#.*)*$/i; 13 | export const HOSTS_COUNT_MATHER = 14 | /^\s*((?:(?:25[0-5]|2[0-4]\d|[0-1]?\d{1,2})\.){3}(?:25[0-5]|2[0-4]\d|[0-1]?\d{1,2})|(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}(?:(?:[0-9A-Fa-f]{1,4})|:))|(?:(?:[0-9A-Fa-f]{1,4}:){6}(?::|(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})|(?::[0-9A-Fa-f]{1,4})))|(?:(?:[0-9A-Fa-f]{1,4}:){5}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:){4}(?::[0-9A-Fa-f]{1,4}){0,1}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:){3}(?::[0-9A-Fa-f]{1,4}){0,2}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:){2}(?::[0-9A-Fa-f]{1,4}){0,3}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:)(?::[0-9A-Fa-f]{1,4}){0,4}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?::(?::[0-9A-Fa-f]{1,4}){0,5}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})))(?:%.+)?)(\s+([\*|\w|\.|-]+))+?\s*(?:#.*)*$/img; 15 | 16 | export const USER_HOME = process.platform === 'win32' ? process.env.USERPROFILE || '' : process.env.HOME || process.env.HOMEPATH || ''; 17 | export const WORKSPACE = path.join(USER_HOME, '.' + APP_NAME); 18 | export const MANIFEST = path.join(WORKSPACE, './manifest.json'); 19 | export const LOG = path.join(WORKSPACE, './log.txt'); 20 | 21 | export const TOTAL_HOSTS_UID = 'TOTAL_HOSTS_UID'; 22 | export const NO_PERM_ERROR_TAG = 'EACCES'; 23 | export const NO_PERM_ERROR_TAG_WIN32 = 'EPERM'; 24 | 25 | export const EVENT = keymirror({ 26 | HIDE_WINDOW: null, 27 | CLOSE_WINDOW: null, 28 | SET_HOSTS_MENU: null, 29 | MINIMIZE_WINDOW: null, 30 | MAXIMIZE_WINDOW: null, 31 | OPEN_EXTERNAL_URL: null, 32 | OPEN_SETTINGS_WINDOW: null, 33 | 34 | WINDOW_MAXIMIZED: null, 35 | WINDOW_UNMAXIMIZED: null, 36 | 37 | INITIAL_CLOUD_HOSTS_UPDATED: null, 38 | }); 39 | 40 | export const SURGE_HOSTS_HEADER = ` 41 | [General] 42 | skip-proxy = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, localhost, *.local 43 | bypass-tun = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12 44 | loglevel = notify 45 | 46 | [Rule] 47 | FINAL,DIRECT 48 | 49 | [Host] 50 | `; -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const electron = require('electron'); 4 | 5 | const app = electron.app; 6 | const Menu = electron.Menu; 7 | const BrowserWindow = electron.BrowserWindow; 8 | 9 | const menuTemplate = [ 10 | { 11 | label: 'Hozz', 12 | submenu: [ 13 | { 14 | label: 'Exit', 15 | accelerator: 'Cmd+Q', 16 | click: function () { 17 | global.terminate(); 18 | } 19 | } 20 | ] 21 | }, 22 | { 23 | label: 'Edit', 24 | submenu: [ 25 | { 26 | label: 'Undo', 27 | accelerator: 'Cmd+Z', 28 | selector: 'undo:' 29 | }, 30 | { 31 | label: 'Redo', 32 | accelerator: 'Shift+Cmd+Z', 33 | selector: 'redo:' 34 | }, 35 | { 36 | type: 'separator' 37 | }, 38 | { 39 | label: 'Cut', 40 | accelerator: 'Cmd+X', 41 | selector: 'cut:' 42 | }, 43 | { 44 | label: 'Copy', 45 | accelerator: 'Cmd+C', 46 | selector: 'copy:' 47 | }, 48 | { 49 | label: 'Paste', 50 | accelerator: 'Cmd+V', 51 | selector: 'paste:' 52 | }, 53 | { 54 | label: 'Select All', 55 | accelerator: 'Cmd+A', 56 | selector: 'selectAll:' 57 | } 58 | ] 59 | }, 60 | ]; 61 | const menu = Menu.buildFromTemplate(menuTemplate); 62 | 63 | // Keep a global reference of the window object, if you don't, the window will 64 | // be closed automatically when the JavaScript object is garbage collected. 65 | let mainWindow; 66 | let settingsWindow; 67 | let shouldQuit = false; 68 | 69 | global.updateStatus = (function () { 70 | let status = ''; 71 | return { 72 | get () { 73 | return status; 74 | }, 75 | 76 | set (value) { 77 | status = value; 78 | if (settingsWindow) { 79 | settingsWindow.webContents.send('UPDATE_STATUS', value); 80 | } 81 | } 82 | }; 83 | })(); 84 | 85 | global.terminate = function () { 86 | shouldQuit = true; 87 | app.quit(); 88 | }; 89 | 90 | if (process.platform === 'linux') { 91 | app.commandLine.appendSwitch('enable-transparent-visuals'); 92 | app.commandLine.appendSwitch('disable-gpu'); 93 | } 94 | 95 | // Someone tried to run a second instance, we should focus our window 96 | var shouldStartInstance = app.makeSingleInstance(function(commandLine, workingDirectory) { 97 | if (mainWindow) { 98 | if (!mainWindow.isVisible()) { 99 | mainWindow.show(); 100 | } 101 | if (mainWindow.isMinimized()) { 102 | mainWindow.restore(); 103 | } 104 | mainWindow.focus(); 105 | } 106 | return true; 107 | }); 108 | 109 | if (shouldStartInstance) { 110 | app.quit(); 111 | return; 112 | } 113 | 114 | app.on('ready', function () { 115 | mainWindow = new BrowserWindow({ 116 | width: 960, 117 | height: 640, 118 | frame: false, 119 | transparent: true, 120 | icon: path.join(__dirname, './assets/images/icon.png'), 121 | }); 122 | mainWindow.loadURL('file://' + __dirname + '/index.html'); 123 | // mainWindow.webContents.openDevTools(); 124 | mainWindow.on('close', function(e) { 125 | if (!shouldQuit) { 126 | e.preventDefault(); 127 | mainWindow.hide(); 128 | } 129 | }); 130 | mainWindow.on('closed', function() { 131 | mainWindow = null; 132 | }); 133 | 134 | settingsWindow = new BrowserWindow({ 135 | width: 600, 136 | height: 480, 137 | frame: false, 138 | resizable: false, 139 | transparent: true, 140 | icon: path.join(__dirname, './assets/images/icon.png'), 141 | }); 142 | settingsWindow.loadURL('file://' + __dirname + '/settings.html'); 143 | settingsWindow.hide(); 144 | // settingsWindow.webContents.openDevTools(); 145 | settingsWindow.on('close', function(e) { 146 | if (!shouldQuit) { 147 | e.preventDefault(); 148 | settingsWindow.hide(); 149 | } 150 | }); 151 | settingsWindow.on('closed', function() { 152 | settingsWindow = null; 153 | }); 154 | 155 | global.settingsWindow = settingsWindow; 156 | 157 | if (process.platform == "darwin") { 158 | Menu.setApplicationMenu(menu); 159 | } 160 | }); 161 | -------------------------------------------------------------------------------- /src/js/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import event from '../backend/event'; 4 | import { EVENT, TOTAL_HOSTS_UID } from '../constants'; 5 | 6 | import SearchBox from './SearchBox'; 7 | import SidebarItem from './SidebarItem'; 8 | import HostsInfoDialog from './HostsInfoDialog'; 9 | 10 | class Sidebar extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | isAddingHosts: false, 15 | isEditingHosts: false, 16 | nextHosts: { name: '', url: '' }, 17 | } 18 | } 19 | 20 | componentWillReceiveProps (nextProps) { 21 | const { editingHosts } = nextProps; 22 | const { isAddingHosts, isEditingHosts } = this.state; 23 | if (!isAddingHosts && !isEditingHosts && nextProps.editingHosts) { 24 | this.setState({ 25 | isEditingHosts: true, 26 | nextHosts: { 27 | url: editingHosts.url, 28 | name: editingHosts.name, 29 | }, 30 | }); 31 | } 32 | } 33 | 34 | __onItemClick (item) { 35 | const { onItemClick } = this.props; 36 | onItemClick && onItemClick(item); 37 | } 38 | 39 | __onHostsDialogOKClick () { 40 | const { isAddingHosts, isEditingHosts, nextHosts } = this.state; 41 | const { editingHosts, onAddHostsClick, onUpdateHostsClick } = this.props; 42 | if (isAddingHosts && onAddHostsClick) { 43 | onAddHostsClick(nextHosts); 44 | } else if (isEditingHosts && onUpdateHostsClick) { 45 | editingHosts.url = nextHosts.url.trim(); 46 | editingHosts.name = nextHosts.name.trim(); 47 | onUpdateHostsClick(editingHosts); 48 | } 49 | this.setState({ 50 | isAddingHosts: false, 51 | isEditingHosts: false, 52 | nextHosts: { url: '', name: '' } 53 | }); 54 | } 55 | 56 | __onHostsDialogAddClick () { 57 | this.setState({ 58 | isAddingHosts: true, 59 | nextHosts: { url: '', name: '' }, 60 | }); 61 | } 62 | 63 | __onDialogInputChange (name, url) { 64 | this.setState({ nextHosts: { name, url } }); 65 | } 66 | 67 | __onDialogDismiss () { 68 | const { isAddingHosts, isEditingHosts, nextHosts } = this.state; 69 | const { editingHosts, onAddHostsClick, onUpdateHostsClick } = this.props; 70 | if (isAddingHosts && onAddHostsClick) { 71 | onAddHostsClick(null); 72 | } else if (isEditingHosts && onUpdateHostsClick) { 73 | onUpdateHostsClick(null); 74 | } 75 | this.setState({ 76 | isAddingHosts: false, 77 | isEditingHosts: false, 78 | nextHosts: { url: '', name: '' } 79 | }); 80 | } 81 | 82 | __onSettingsClick () { 83 | event.emit(EVENT.OPEN_SETTINGS_WINDOW); 84 | } 85 | 86 | __renderSidebarItem (item) { 87 | const { activeUid, onItemEdit, onItemRemove, onItemStatusChange } = this.props; 88 | if (!item) { 89 | return null; 90 | } 91 | return () 99 | } 100 | 101 | render() { 102 | const { isAddingHosts, isEditingHosts } = this.state; 103 | const { list, totalHosts, editingHosts, onSearchChange } = this.props; 104 | const sidebarItems = list.map((item, index) => { 105 | return this.__renderSidebarItem(item); 106 | }); 107 | const addHostsButton = isAddingHosts || isEditingHosts ? 108 | : 109 | ; 110 | return (
111 | 112 |
113 | { this.__renderSidebarItem(totalHosts) } 114 |
115 | { sidebarItems } 116 |
117 |
118 |
119 |
120 | { addHostsButton } 121 | 122 |
123 |
124 | { isAddingHosts || isEditingHosts ? 125 | : null } 131 |
); 132 | } 133 | } 134 | 135 | Sidebar.propTypes = { 136 | list: PropTypes.array, 137 | onItemEdit: PropTypes.func, 138 | onItemClick: PropTypes.func, 139 | activeUid: PropTypes.string, 140 | onItemRemove: PropTypes.func, 141 | totalHosts: PropTypes.object, 142 | editingHosts: PropTypes.object, 143 | onSearchChange: PropTypes.func, 144 | onAddHostsClick: PropTypes.func, 145 | onUpdateHostsClick: PropTypes.func, 146 | onItemStatusChange: PropTypes.func, 147 | }; 148 | 149 | export default Sidebar; 150 | -------------------------------------------------------------------------------- /src/js/backend/manifest.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const mkdirp = require('mkdirp'); 3 | 4 | import io from './io'; 5 | import log from './log'; 6 | import Hosts from './hosts'; 7 | import { MANIFEST, 8 | WORKSPACE, 9 | TOTAL_HOSTS_UID, 10 | NO_PERM_ERROR_TAG, 11 | NO_PERM_ERROR_TAG_WIN32 } from '../constants'; 12 | 13 | try { 14 | mkdirp.sync(WORKSPACE); 15 | } catch (e) { 16 | log('Make workspace folder failed: ', e); 17 | } 18 | 19 | const sysHostsPath = () => { 20 | if (process.platform === 'win32') { 21 | return path.join(process.env.SYSTEMROOT, './system32/drivers/etc/hosts'); 22 | } else { 23 | return '/etc/hosts'; 24 | } 25 | } 26 | 27 | class Manifest { 28 | constructor (options) { 29 | const { online, language, hosts } = options; 30 | this.hosts = new Map(); 31 | if (hosts instanceof Map) { 32 | this.hosts = hosts; 33 | } else if (Array.isArray(hosts)) { 34 | this.hosts = new Map(); 35 | hosts.forEach((hostsObj) => { 36 | const __hosts = new Hosts(hostsObj); 37 | this.hosts.set(__hosts.uid, __hosts); 38 | }); 39 | } 40 | this.online = typeof(online) === 'undefined' ? true : online; 41 | this.language = typeof(language) === 'undefined' ? navigator.language : language; 42 | } 43 | 44 | getHostsByUid (uid) { 45 | return this.hosts.get(uid); 46 | } 47 | 48 | setHostsByUid (uid, hosts) { 49 | return this.hosts.set(uid, hosts); 50 | } 51 | 52 | getHostsList () { 53 | return Array.from(this.hosts.values()).sort((A, B) => { 54 | return (A.index | 0) - (B.index | 0); 55 | }); 56 | } 57 | 58 | sortHosts () { 59 | this.getHostsList().forEach((hosts, index) => { 60 | hosts.index = index; 61 | }); 62 | } 63 | 64 | addHosts (hosts) { 65 | this.sortHosts(); 66 | hosts.index = this.getHostsList().length; 67 | this.hosts.set(hosts.uid, hosts); 68 | return this; 69 | } 70 | 71 | removeHosts (hosts) { 72 | this.hosts.delete(hosts.uid); 73 | this.sortHosts(); 74 | return this; 75 | } 76 | 77 | moveHostsIndex (fromIndex, toIndex) { 78 | if (fromIndex === toIndex || 79 | fromIndex < 0 || 80 | toIndex > this.getHostsList().length) { 81 | return; 82 | } 83 | const list = this.getHostsList(); 84 | list.splice(toIndex, 0, list.splice(fromIndex, 1)[0]); 85 | list.forEach((hosts, index) => { 86 | hosts.index = index; 87 | }); 88 | } 89 | 90 | getMergedHosts () { 91 | let totalCount = 0; 92 | let totalHostsText = ''; 93 | for (let hosts of this.getHostsList()) { 94 | if (!this.online) { 95 | hosts.stashStatus(); 96 | } else { 97 | hosts.popStatus(); 98 | } 99 | if (hosts.online) { 100 | totalHostsText += hosts.text + '\n'; 101 | totalCount += hosts.count; 102 | } 103 | } 104 | return new Hosts({ 105 | uid: TOTAL_HOSTS_UID, 106 | name: 'All', 107 | count: totalCount, 108 | text: totalHostsText, 109 | online: this.online, 110 | }); 111 | } 112 | 113 | toSimpleObject () { 114 | const __manifest = Object.assign({}, this); 115 | const simpleHosts = this.getHostsList().map((hosts) => { 116 | const __hosts = hosts.toObject(); 117 | delete __hosts.text; 118 | if (typeof(hosts.__online) !== 'undefined') { 119 | __hosts.online = hosts.__online; 120 | } 121 | return __hosts; 122 | }); 123 | __manifest.hosts = simpleHosts; 124 | return __manifest; 125 | } 126 | 127 | commit () { 128 | return io.writeFile(MANIFEST, JSON.stringify(this.toSimpleObject())); 129 | } 130 | 131 | loadSysHosts () { 132 | return io.readFile(sysHostsPath(), 'utf-8').then((text) => { 133 | return Promise.resolve(Hosts.createFromText(text)); 134 | }).catch((e) => { 135 | log(e); 136 | return Promise.resolve(null); 137 | }); 138 | } 139 | 140 | saveSysHosts (hosts) { 141 | return io.writeFile(sysHostsPath(), this.online ? hosts.text : '').catch((error) => { 142 | if (error && 143 | error.message && 144 | (error.message.indexOf(NO_PERM_ERROR_TAG) > -1 || 145 | error.message.indexOf(NO_PERM_ERROR_TAG_WIN32) > -1)) { 146 | return Promise.reject(error); 147 | } 148 | log(error); 149 | return Promise.resolve(); 150 | }); 151 | } 152 | } 153 | 154 | Manifest.loadFromDisk = () => { 155 | return io.readFile(MANIFEST, 'utf-8').then((text) => { 156 | try { 157 | return Promise.resolve(JSON.parse(text)); 158 | } catch (e) { 159 | return Promise.resolve({}); 160 | } 161 | }).catch(() => { 162 | return Promise.resolve({}); 163 | }).then((json) => { 164 | const { hosts } = json; 165 | const manifest = new Manifest(json); 166 | const hostsMap = new Map(); 167 | if (Array.isArray(hosts)) { 168 | const hostsPromises = hosts.map((item) => { 169 | const __hosts = new Hosts(item); 170 | hostsMap.set(__hosts.uid, __hosts); 171 | return __hosts.load(); 172 | }); 173 | return Promise.all(hostsPromises).then(() => { 174 | manifest.hosts = hostsMap; 175 | return Promise.resolve(manifest); 176 | }); 177 | } else { 178 | return manifest.loadSysHosts().then((hosts) => { 179 | hosts.online = true; 180 | hosts.name = 'Default Hosts'; 181 | hosts.save(); 182 | manifest.addHosts(hosts).commit(); 183 | return Promise.resolve(manifest); 184 | }); 185 | } 186 | }); 187 | } 188 | 189 | export default Manifest; -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20120731 at Tue Dec 29 22:45:08 2015 6 | By Ads 7 | 8 | 9 | 10 | 24 | 26 | 28 | 30 | 32 | 36 | 38 | 41 | 43 | 46 | 50 | 58 | 60 | 62 | 65 | 67 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/js/backend/update.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const mkdirp = require('mkdirp'); 3 | 4 | import JSZip from 'jszip'; 5 | 6 | import io from '../backend/io'; 7 | import log from '../backend/log'; 8 | import Lang from '../backend/language'; 9 | import { remote, dialog } from '../backend/nw.interface'; 10 | import { APP_NAME, 11 | APP_VERSION, 12 | APP_HOMEPAGE, 13 | APP_RELEASES_URL } from '../constants'; 14 | 15 | const terminate = remote.getGlobal('terminate'); 16 | const updateStatus = remote.getGlobal('updateStatus'); 17 | 18 | const isNewVersion = (v1, v2) => { 19 | if (v1 != v2) { 20 | let v1slices = v1.split('.'); 21 | let v2slices = v2.split('.'); 22 | let length = Math.min(v1slices.length, v2slices.length); 23 | for (let i = 0; i < length; i++) { 24 | let v1clip = parseInt(v1slices[i]); 25 | let v2clip = parseInt(v2slices[i]); 26 | if (isNaN(v2clip) || v1clip > v2clip) { 27 | return false; 28 | } else if (v1clip < v2clip) { 29 | return true; 30 | } 31 | } 32 | } 33 | return false; 34 | } 35 | 36 | const checkUpdate = (showNoUpdateFoundDialog = true) => { 37 | updateStatus.set('checking'); 38 | return io.requestUrl(APP_RELEASES_URL).then((json) => { 39 | const releases = JSON.parse(json).sort((A, B) => B.id - A.id); 40 | const latestRelease = releases[0]; 41 | if (!latestRelease || !latestRelease.tag_name) { 42 | return Promise.reject(new Error('Cannot find valid latest release')); 43 | } 44 | const latestVersion = latestRelease.tag_name[0] === 'v' ? 45 | latestRelease.tag_name.slice(1) : 46 | latestRelease.tag_name; 47 | if (isNewVersion(APP_VERSION, latestVersion)) { 48 | const confirm = dialog.showMessageBox({ 49 | type: 'info', 50 | title: Lang.get('common.update_available'), 51 | buttons: [Lang.get('common.ok'), Lang.get('common.cancel')], 52 | message: Lang.get('common.found_new_version', latestVersion), 53 | detail: `${ latestRelease.body || '' }\n\n` + Lang.get('common.confirm_update'), 54 | }); 55 | if (confirm === 0) { 56 | return Promise.resolve(latestRelease); 57 | } 58 | } else { 59 | showNoUpdateFoundDialog && dialog.showMessageBox({ 60 | type: 'info', 61 | buttons: [Lang.get('common.ok')], 62 | title: Lang.get('common.success'), 63 | message: Lang.get('common.no_update_found'), 64 | detail: Lang.get('common.using_latest_release'), 65 | }); 66 | } 67 | return Promise.resolve(null); 68 | }).catch((e) => { 69 | log(e); 70 | if (showNoUpdateFoundDialog) { 71 | dialog.showErrorBox( 72 | Lang.get('common.update_failed'), 73 | Lang.get('common.check_update_failed') + ' ' + Lang.get('common.go_to_homepage', APP_HOMEPAGE) 74 | ); 75 | } 76 | return Promise.resolve(null); 77 | }); 78 | } 79 | 80 | const downloadUpdate = (release) => { 81 | let corePackage = null; 82 | if (!release) { 83 | return Promise.resolve(null); 84 | } 85 | try { 86 | const assets = release ? release.assets : []; 87 | for (let asset of assets) { 88 | if (asset.name === 'core.zip') { 89 | corePackage = asset; 90 | break; 91 | } 92 | } 93 | } catch (e) {} 94 | if (corePackage) { 95 | updateStatus.set('downloading'); 96 | return io.downloadUrl(corePackage.browser_download_url).then((buffer) => { 97 | const zip = new JSZip(buffer); 98 | if (!zip || !zip.files) { 99 | return Promise.reject(new Error('Invalid update zip file!')); 100 | } 101 | return Promise.resolve(zip); 102 | }).catch((e) => { 103 | log(e); 104 | dialog.showErrorBox( 105 | Lang.get('common.update_failed'), 106 | Lang.get('common.download_update_failed') + ' ' + Lang.get('common.go_to_homepage', APP_HOMEPAGE) 107 | ); 108 | return Promise.resolve(null); 109 | }); 110 | } else { 111 | updateStatus.set(''); 112 | dialog.showErrorBox( 113 | Lang.get('common.update_failed'), 114 | Lang.get('common.cannot_find_core_package') + ' ' + Lang.get('common.go_to_homepage', APP_HOMEPAGE) 115 | ); 116 | return Promise.resolve(null); 117 | } 118 | } 119 | 120 | const applyUpdate = (zip) => { 121 | if (!zip) { 122 | return Promise.resolve(false); 123 | } 124 | updateStatus.set('applying'); 125 | const promises = []; 126 | const { files } = zip; 127 | for (let filename in files) { 128 | if (files.hasOwnProperty(filename)) { 129 | const file = files[filename]; 130 | if (file.dir) { 131 | mkdirp.sync(path.join(global.__dirname, filename)); 132 | } else { 133 | const buffer = files[filename].asNodeBuffer(); 134 | promises.push(io.writeFile(path.join(global.__dirname, filename), buffer)); 135 | } 136 | } 137 | } 138 | return Promise.all(promises).then(() => { 139 | dialog.showMessageBox({ 140 | buttons: ['OK'], 141 | type: 'info', 142 | title: Lang.get('common.success'), 143 | message: Lang.get('common.update_complete'), 144 | detail: Lang.get('common.restart_to_update', APP_NAME), 145 | }); 146 | return Promise.resolve(true); 147 | }).catch((e) => { 148 | log(e); 149 | dialog.showErrorBox( 150 | Lang.get('common.update_failed'), 151 | Lang.get('common.apply_update_failed') + ' ' + Lang.get('common.go_to_homepage', APP_HOMEPAGE) 152 | ); 153 | return Promise.resolve(false); 154 | }); 155 | } 156 | 157 | export default function (interactive = true) { 158 | if (updateStatus.get()) { 159 | return; 160 | } 161 | checkUpdate(interactive) 162 | .then(downloadUpdate) 163 | .then(applyUpdate) 164 | .then((result) => { 165 | updateStatus.set('') 166 | if (result) { 167 | terminate(); 168 | } 169 | }); 170 | }; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var gulp = require('gulp'); 3 | var sass = require('gulp-sass'); 4 | var process = require('process'); 5 | var gutil = require('gulp-util'); 6 | var webpack = require('webpack'); 7 | var clean = require('gulp-clean'); 8 | var packager = require('electron-packager'); 9 | var childProcess = require('child_process'); 10 | var convertNewline = require("gulp-convert-newline"); 11 | 12 | var packageInfo = require('./package.json'); 13 | var webpackConfig = require('./webpack.config.js'); 14 | 15 | var APP_NAME = packageInfo.name; 16 | 17 | gulp.task('clean', function (callback) { 18 | gulp.src(['./app', './build'], {read: false}) 19 | .pipe(clean()); 20 | callback(); 21 | }); 22 | 23 | gulp.task('copy', function () { 24 | gulp.src(['./src/browser.js', './src/app.config.js', './src/*.html']) 25 | .pipe(gulp.dest('./app')); 26 | gulp.src(['./src/assets/*', './src/assets/*/**', '!./src/assets/scripts/*.sh']) 27 | .pipe(gulp.dest('./app/assets')); 28 | gulp.src(['./package.json']).pipe(gulp.dest('./app')); 29 | gulp.src(['./node_modules/electron-sudo/src/bin/*', './node_modules/electron-sudo/src/bin/*/**']) 30 | .pipe(gulp.dest('./app/bin')); 31 | gulp.src('./src/assets/scripts/*.sh') 32 | .pipe(convertNewline()) 33 | .pipe(gulp.dest('./app/assets/scripts')); 34 | }); 35 | 36 | gulp.task('webpack', function (callback) { 37 | webpack(webpackConfig, function (err, stats) { 38 | if (err) { 39 | throw new gutil.PluginError('webpack', err); 40 | } 41 | gutil.log('[webpack]', stats.toString({ modules: false, colors: true })); 42 | callback(); 43 | }); 44 | }); 45 | 46 | gulp.task('sass', function () { 47 | gulp.src('./src/sass/main.scss') 48 | .pipe( 49 | sass({ 50 | includePaths: [path.join(__dirname, './src/sass')], 51 | outputStyle: 'compressed' 52 | }) 53 | .on('error', sass.logError)) 54 | .pipe(gulp.dest('./app')); 55 | }); 56 | 57 | gulp.task('build', ['copy', 'sass', 'webpack']); 58 | 59 | var deleteUselessFiles = function (platform, distPath) { 60 | var filesToBeRemoved = []; 61 | switch (platform) { 62 | case 'win32': 63 | filesToBeRemoved = [ 64 | '*.html', 65 | 'LICENSE', 66 | 'version', 67 | 'pdf.dll', 68 | 'locales/*.*', 69 | 'xinput1_3.dll', 70 | 'd3dcompiler.dll', 71 | 'vccorlib120.dll', 72 | 'snapshot_blob.bin', 73 | 'd3dcompiler_47.dll', 74 | './resources/default_app', 75 | 'ui_resources_200_percent.pak', 76 | 'content_resources_200_percent.pak', 77 | ]; 78 | break; 79 | case 'darwin': 80 | filesToBeRemoved = [ 81 | '*.html', 82 | 'LICENSE', 83 | 'version', 84 | '/' + APP_NAME + '.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources/snapshot_blob.bin', 85 | ]; 86 | break; 87 | case 'linux': 88 | filesToBeRemoved = [ 89 | '*.html', 90 | 'LICENSE', 91 | 'version', 92 | 'locales/*.*', 93 | 'snapshot_blob.bin', 94 | './resources/default_app', 95 | ]; 96 | break; 97 | } 98 | filesToBeRemoved = filesToBeRemoved.map((file) => { 99 | return path.join(distPath, file); 100 | }); 101 | console.log('Removed unnecessary files.'); 102 | return gulp.src(filesToBeRemoved).pipe(clean()); 103 | } 104 | 105 | var compressFiles = function (platform, distPath, callback) { 106 | var upx = ''; 107 | var filesToBeCompressed = []; 108 | switch (platform) { 109 | case 'win32': 110 | upx = path.join(__dirname, 'tools/upx.exe') 111 | filesToBeCompressed = [ 112 | 'node.dll', 113 | 'libEGL.dll', 114 | 'msvcr120.dll', 115 | 'msvcp120.dll', 116 | 'libGLESv2.dll', 117 | APP_NAME + '.exe', 118 | ]; 119 | break; 120 | case 'darwin': 121 | upx = path.join(__dirname, 'tools/upx'); 122 | break; 123 | case 'linux': 124 | upx = path.join(__dirname, 'tools/upx-' + process.arch); 125 | filesToBeCompressed = [ 126 | APP_NAME, 127 | 'libnode.so', 128 | ]; 129 | break; 130 | } 131 | console.log('Compressing executables...'); 132 | filesToBeCompressed.forEach((file) => { 133 | var fullPath = path.join(distPath, file); 134 | childProcess.exec(upx + ' -9 ' + fullPath, function (error, stdout, stderr) { 135 | if (error) { 136 | gutil.log(error); 137 | } 138 | gutil.log(stdout, stderr); 139 | }); 140 | }); 141 | } 142 | 143 | var buildPackage = function (platform, arch, callback) { 144 | var icon; 145 | switch (platform) { 146 | case 'win32': 147 | icon = './app/assets/images/icon.ico'; 148 | break; 149 | case 'darwin': 150 | icon = './app/assets/images/icon.icns'; 151 | break; 152 | default: 153 | icon = './app/assets/images/icon.png'; 154 | break; 155 | } 156 | packager({ 157 | arch: arch, 158 | icon: icon, 159 | dir: './app', 160 | out: './build', 161 | name: APP_NAME, 162 | version: '0.36.2', 163 | platform: platform, 164 | }, function (err, appPath) { 165 | if (appPath) { 166 | var distPath = appPath[0]; 167 | console.log(distPath) 168 | callback && callback(platform, arch, distPath); 169 | } 170 | }); 171 | } 172 | 173 | var afterPackage = function (platform, arch, distPath) { 174 | deleteUselessFiles(platform, distPath); 175 | compressFiles(platform, distPath); 176 | } 177 | 178 | gulp.task('package', ['build'], function (callback) { 179 | gulp.src('./app/*.map').pipe(clean()); 180 | if (process.arch !== 'ia32') { 181 | buildPackage(process.platform, process.arch, afterPackage); 182 | } 183 | if (process.platform !== 'darwin') { 184 | buildPackage(process.platform, 'ia32', afterPackage); 185 | } 186 | }); 187 | 188 | gulp.task('package-uncompressed', ['build'], function (callback) { 189 | gulp.src('./app/*.map').pipe(clean()); 190 | if (process.arch !== 'ia32') { 191 | buildPackage(process.platform, process.arch); 192 | } 193 | if (process.platform !== 'darwin') { 194 | buildPackage(process.platform, 'ia32'); 195 | } 196 | }); 197 | 198 | gulp.task('watch', ['build'], function () { 199 | gulp.watch('./src/sass/**/*.scss', ['sass']); 200 | gulp.watch(['./src/js/**/*.*', 201 | './src/app.config.js'], ['webpack']); 202 | gulp.watch(['./src/*.html', 203 | './package.json', 204 | './src/browser.js', 205 | './src/assets/**/*.*', 206 | './src/app.config.js', 207 | './node_modules/electron-sudo/**'], ['copy']); 208 | }); 209 | 210 | gulp.task('default', ['build']); 211 | -------------------------------------------------------------------------------- /src/sass/react-select.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * React Select 3 | * ============ 4 | * Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/ 5 | * https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs 6 | * MIT License: https://github.com/keystonejs/react-select 7 | */ 8 | .Select { 9 | position: relative; 10 | } 11 | .Select, 12 | .Select div, 13 | .Select input, 14 | .Select span { 15 | -webkit-box-sizing: border-box; 16 | -moz-box-sizing: border-box; 17 | box-sizing: border-box; 18 | } 19 | .Select.is-disabled > .Select-control { 20 | background-color: #f9f9f9; 21 | } 22 | .Select.is-disabled > .Select-control:hover { 23 | box-shadow: none; 24 | } 25 | .Select.is-disabled .Select-arrow-zone { 26 | cursor: default; 27 | pointer-events: none; 28 | } 29 | .Select-control { 30 | background-color: #fff; 31 | border-color: #d9d9d9 #ccc #b3b3b3; 32 | border-radius: 4px; 33 | border: 1px solid #ccc; 34 | color: #333; 35 | cursor: default; 36 | display: table; 37 | height: 36px; 38 | outline: none; 39 | overflow: hidden; 40 | position: relative; 41 | width: 100%; 42 | } 43 | .Select-control:hover { 44 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); 45 | } 46 | .is-searchable.is-open > .Select-control { 47 | cursor: text; 48 | } 49 | .is-open > .Select-control { 50 | border-bottom-right-radius: 0; 51 | border-bottom-left-radius: 0; 52 | background: #fff; 53 | border-color: #b3b3b3 #ccc #d9d9d9; 54 | } 55 | .is-open > .Select-control > .Select-arrow { 56 | border-color: transparent transparent #999; 57 | border-width: 0 5px 5px; 58 | } 59 | .is-searchable.is-focused:not(.is-open) > .Select-control { 60 | cursor: text; 61 | } 62 | .is-focused:not(.is-open) > .Select-control { 63 | border-color: #007eff; 64 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 3px rgba(0, 126, 255, 0.1); 65 | } 66 | .Select-placeholder, 67 | :not(.Select--multi) > .Select-control .Select-value { 68 | bottom: 0; 69 | color: #aaa; 70 | left: 0; 71 | line-height: 34px; 72 | padding-left: 10px; 73 | padding-right: 10px; 74 | position: absolute; 75 | right: 0; 76 | top: 0; 77 | max-width: 100%; 78 | overflow: hidden; 79 | text-overflow: ellipsis; 80 | white-space: nowrap; 81 | } 82 | .has-value:not(.Select--multi) > .Select-control > .Select-value .Select-value-label, 83 | .has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value .Select-value-label { 84 | color: #333; 85 | } 86 | .has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label, 87 | .has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label { 88 | cursor: pointer; 89 | text-decoration: none; 90 | } 91 | .has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:hover, 92 | .has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:hover, 93 | .has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:focus, 94 | .has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:focus { 95 | color: #007eff; 96 | outline: none; 97 | text-decoration: underline; 98 | } 99 | .Select-input { 100 | height: 34px; 101 | padding-left: 10px; 102 | padding-right: 10px; 103 | vertical-align: middle; 104 | } 105 | .Select-input > input { 106 | background: none transparent; 107 | border: 0 none; 108 | box-shadow: none; 109 | cursor: default; 110 | display: inline-block; 111 | font-family: inherit; 112 | font-size: inherit; 113 | height: 34px; 114 | margin: 0; 115 | outline: none; 116 | padding: 0; 117 | -webkit-appearance: none; 118 | } 119 | .is-focused .Select-input > input { 120 | cursor: text; 121 | } 122 | .has-value.is-pseudo-focused .Select-input { 123 | opacity: 0; 124 | } 125 | .Select-control:not(.is-searchable) > .Select-input { 126 | outline: none; 127 | } 128 | .Select-loading-zone { 129 | cursor: pointer; 130 | display: table-cell; 131 | position: relative; 132 | text-align: center; 133 | vertical-align: middle; 134 | width: 16px; 135 | } 136 | .Select-loading { 137 | -webkit-animation: Select-animation-spin 400ms infinite linear; 138 | -o-animation: Select-animation-spin 400ms infinite linear; 139 | animation: Select-animation-spin 400ms infinite linear; 140 | width: 16px; 141 | height: 16px; 142 | box-sizing: border-box; 143 | border-radius: 50%; 144 | border: 2px solid #ccc; 145 | border-right-color: #333; 146 | display: inline-block; 147 | position: relative; 148 | vertical-align: middle; 149 | } 150 | .Select-clear-zone { 151 | -webkit-animation: Select-animation-fadeIn 200ms; 152 | -o-animation: Select-animation-fadeIn 200ms; 153 | animation: Select-animation-fadeIn 200ms; 154 | color: #999; 155 | cursor: pointer; 156 | display: table-cell; 157 | position: relative; 158 | text-align: center; 159 | vertical-align: middle; 160 | width: 17px; 161 | } 162 | .Select-clear-zone:hover { 163 | color: #D0021B; 164 | } 165 | .Select-clear { 166 | display: inline-block; 167 | font-size: 18px; 168 | line-height: 1; 169 | } 170 | .Select--multi .Select-clear-zone { 171 | width: 17px; 172 | } 173 | .Select-arrow-zone { 174 | cursor: pointer; 175 | display: table-cell; 176 | position: relative; 177 | text-align: center; 178 | vertical-align: middle; 179 | width: 25px; 180 | padding-right: 5px; 181 | } 182 | .Select-arrow { 183 | border-color: #999 transparent transparent; 184 | border-style: solid; 185 | border-width: 5px 5px 2.5px; 186 | display: inline-block; 187 | height: 0; 188 | width: 0; 189 | } 190 | .is-open .Select-arrow, 191 | .Select-arrow-zone:hover > .Select-arrow { 192 | border-top-color: #666; 193 | } 194 | @-webkit-keyframes Select-animation-fadeIn { 195 | from { 196 | opacity: 0; 197 | } 198 | to { 199 | opacity: 1; 200 | } 201 | } 202 | @keyframes Select-animation-fadeIn { 203 | from { 204 | opacity: 0; 205 | } 206 | to { 207 | opacity: 1; 208 | } 209 | } 210 | .Select-menu-outer { 211 | border-bottom-right-radius: 4px; 212 | border-bottom-left-radius: 4px; 213 | background-color: #fff; 214 | border: 1px solid #ccc; 215 | border-top-color: #e6e6e6; 216 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); 217 | box-sizing: border-box; 218 | margin-top: -1px; 219 | max-height: 200px; 220 | position: absolute; 221 | top: 100%; 222 | width: 100%; 223 | z-index: 1; 224 | -webkit-overflow-scrolling: touch; 225 | } 226 | .Select-menu { 227 | max-height: 198px; 228 | overflow-y: auto; 229 | } 230 | .Select-option { 231 | box-sizing: border-box; 232 | background-color: #fff; 233 | color: #666666; 234 | cursor: pointer; 235 | display: block; 236 | padding: 8px 10px; 237 | } 238 | .Select-option:last-child { 239 | border-bottom-right-radius: 4px; 240 | border-bottom-left-radius: 4px; 241 | } 242 | .Select-option.is-focused { 243 | background-color: rgba(0, 126, 255, 0.08); 244 | color: #333; 245 | } 246 | .Select-option.is-disabled { 247 | color: #cccccc; 248 | cursor: default; 249 | } 250 | .Select-noresults { 251 | box-sizing: border-box; 252 | color: #999999; 253 | cursor: default; 254 | display: block; 255 | padding: 8px 10px; 256 | } 257 | .Select--multi .Select-input { 258 | vertical-align: middle; 259 | margin-left: 10px; 260 | padding: 0; 261 | } 262 | .Select--multi.has-value .Select-input { 263 | margin-left: 5px; 264 | } 265 | .Select--multi .Select-value { 266 | background-color: rgba(0, 126, 255, 0.08); 267 | border-radius: 2px; 268 | border: 1px solid rgba(0, 126, 255, 0.24); 269 | color: #007eff; 270 | display: inline-block; 271 | font-size: 0.9em; 272 | line-height: 1.4; 273 | margin-left: 5px; 274 | margin-top: 5px; 275 | vertical-align: top; 276 | } 277 | .Select--multi .Select-value-icon, 278 | .Select--multi .Select-value-label { 279 | display: inline-block; 280 | vertical-align: middle; 281 | } 282 | .Select--multi .Select-value-label { 283 | border-bottom-right-radius: 2px; 284 | border-top-right-radius: 2px; 285 | cursor: default; 286 | padding: 2px 5px; 287 | } 288 | .Select--multi a.Select-value-label { 289 | color: #007eff; 290 | cursor: pointer; 291 | text-decoration: none; 292 | } 293 | .Select--multi a.Select-value-label:hover { 294 | text-decoration: underline; 295 | } 296 | .Select--multi .Select-value-icon { 297 | cursor: pointer; 298 | border-bottom-left-radius: 2px; 299 | border-top-left-radius: 2px; 300 | border-right: 1px solid rgba(0, 126, 255, 0.24); 301 | padding: 1px 5px 3px; 302 | } 303 | .Select--multi .Select-value-icon:hover, 304 | .Select--multi .Select-value-icon:focus { 305 | background-color: rgba(0, 113, 230, 0.08); 306 | color: #0071e6; 307 | } 308 | .Select--multi .Select-value-icon:active { 309 | background-color: rgba(0, 126, 255, 0.24); 310 | } 311 | .Select--multi.is-disabled .Select-value { 312 | background-color: #fcfcfc; 313 | border: 1px solid #e3e3e3; 314 | color: #333; 315 | } 316 | .Select--multi.is-disabled .Select-value-icon { 317 | cursor: not-allowed; 318 | border-right: 1px solid #e3e3e3; 319 | } 320 | .Select--multi.is-disabled .Select-value-icon:hover, 321 | .Select--multi.is-disabled .Select-value-icon:focus, 322 | .Select--multi.is-disabled .Select-value-icon:active { 323 | background-color: #fcfcfc; 324 | } 325 | @keyframes Select-animation-spin { 326 | to { 327 | transform: rotate(1turn); 328 | } 329 | } 330 | @-webkit-keyframes Select-animation-spin { 331 | to { 332 | -webkit-transform: rotate(1turn); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /src/sass/codemirror.scss: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family: monospace; 6 | height: 300px; 7 | color: black; 8 | } 9 | 10 | /* PADDING */ 11 | 12 | .CodeMirror-lines { 13 | padding: 4px 0; /* Vertical padding around content */ 14 | } 15 | .CodeMirror pre { 16 | padding: 0 4px; /* Horizontal padding of content */ 17 | } 18 | 19 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 20 | background-color: white; /* The little square between H and V scrollbars */ 21 | } 22 | 23 | /* GUTTER */ 24 | 25 | .CodeMirror-gutters { 26 | border-right: 1px solid #ddd; 27 | background-color: #f7f7f7; 28 | white-space: nowrap; 29 | } 30 | .CodeMirror-linenumbers {} 31 | .CodeMirror-linenumber { 32 | padding: 0 3px 0 5px; 33 | min-width: 20px; 34 | text-align: right; 35 | color: #999; 36 | white-space: nowrap; 37 | } 38 | 39 | .CodeMirror-guttermarker { color: black; } 40 | .CodeMirror-guttermarker-subtle { color: #999; } 41 | 42 | /* CURSOR */ 43 | 44 | .CodeMirror-cursor { 45 | border-left: 1px solid black; 46 | border-right: none; 47 | width: 0; 48 | } 49 | /* Shown when moving in bi-directional text */ 50 | .CodeMirror div.CodeMirror-secondarycursor { 51 | border-left: 1px solid silver; 52 | } 53 | .cm-fat-cursor .CodeMirror-cursor { 54 | width: auto; 55 | border: 0; 56 | background: #7e7; 57 | } 58 | .cm-fat-cursor div.CodeMirror-cursors { 59 | z-index: 1; 60 | } 61 | 62 | .cm-animate-fat-cursor { 63 | width: auto; 64 | border: 0; 65 | -webkit-animation: blink 1.06s steps(1) infinite; 66 | -moz-animation: blink 1.06s steps(1) infinite; 67 | animation: blink 1.06s steps(1) infinite; 68 | background-color: #7e7; 69 | } 70 | @-moz-keyframes blink { 71 | 0% {} 72 | 50% { background-color: transparent; } 73 | 100% {} 74 | } 75 | @-webkit-keyframes blink { 76 | 0% {} 77 | 50% { background-color: transparent; } 78 | 100% {} 79 | } 80 | @keyframes blink { 81 | 0% {} 82 | 50% { background-color: transparent; } 83 | 100% {} 84 | } 85 | 86 | /* Can style cursor different in overwrite (non-insert) mode */ 87 | .CodeMirror-overwrite .CodeMirror-cursor {} 88 | 89 | .cm-tab { display: inline-block; text-decoration: inherit; } 90 | 91 | .CodeMirror-ruler { 92 | border-left: 1px solid #ccc; 93 | position: absolute; 94 | } 95 | 96 | /* DEFAULT THEME */ 97 | 98 | .cm-s-default .cm-header {color: blue;} 99 | .cm-s-default .cm-quote {color: #090;} 100 | .cm-negative {color: #d44;} 101 | .cm-positive {color: #292;} 102 | .cm-header, .cm-strong {font-weight: bold;} 103 | .cm-em {font-style: italic;} 104 | .cm-link {text-decoration: underline;} 105 | .cm-strikethrough {text-decoration: line-through;} 106 | 107 | .cm-s-default .cm-keyword {color: #708;} 108 | .cm-s-default .cm-atom {color: #219;} 109 | .cm-s-default .cm-number {color: #164;} 110 | .cm-s-default .cm-def {color: #00f;} 111 | .cm-s-default .cm-variable, 112 | .cm-s-default .cm-punctuation, 113 | .cm-s-default .cm-property, 114 | .cm-s-default .cm-operator {} 115 | .cm-s-default .cm-variable-2 {color: #05a;} 116 | .cm-s-default .cm-variable-3 {color: #085;} 117 | .cm-s-default .cm-comment {color: #a50;} 118 | .cm-s-default .cm-string {color: #a11;} 119 | .cm-s-default .cm-string-2 {color: #f50;} 120 | .cm-s-default .cm-meta {color: #555;} 121 | .cm-s-default .cm-qualifier {color: #555;} 122 | .cm-s-default .cm-builtin {color: #30a;} 123 | .cm-s-default .cm-bracket {color: #997;} 124 | .cm-s-default .cm-tag {color: #170;} 125 | .cm-s-default .cm-attribute {color: #00c;} 126 | .cm-s-default .cm-hr {color: #999;} 127 | .cm-s-default .cm-link {color: #00c;} 128 | 129 | .cm-s-default .cm-error {color: #f00;} 130 | .cm-invalidchar {color: #f00;} 131 | 132 | .CodeMirror-composing { border-bottom: 2px solid; } 133 | 134 | /* Default styles for common addons */ 135 | 136 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} 137 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} 138 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 139 | .CodeMirror-activeline-background {background: #e8f2ff;} 140 | 141 | /* STOP */ 142 | 143 | /* The rest of this file contains styles related to the mechanics of 144 | the editor. You probably shouldn't touch them. */ 145 | 146 | .CodeMirror { 147 | position: relative; 148 | overflow: hidden; 149 | background: white; 150 | } 151 | 152 | .CodeMirror-scroll { 153 | overflow: scroll !important; /* Things will break if this is overridden */ 154 | /* 30px is the magic margin used to hide the element's real scrollbars */ 155 | /* See overflow: hidden in .CodeMirror */ 156 | margin-bottom: -30px; margin-right: -30px; 157 | padding-bottom: 30px; 158 | height: 100%; 159 | outline: none; /* Prevent dragging from highlighting the element */ 160 | position: relative; 161 | } 162 | .CodeMirror-sizer { 163 | position: relative; 164 | border-right: 30px solid transparent; 165 | } 166 | 167 | /* The fake, visible scrollbars. Used to force redraw during scrolling 168 | before actual scrolling happens, thus preventing shaking and 169 | flickering artifacts. */ 170 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 171 | position: absolute; 172 | z-index: 6; 173 | display: none; 174 | } 175 | .CodeMirror-vscrollbar { 176 | right: 0; top: 0; 177 | overflow-x: hidden; 178 | overflow-y: scroll; 179 | } 180 | .CodeMirror-hscrollbar { 181 | bottom: 0; left: 0; 182 | overflow-y: hidden; 183 | overflow-x: scroll; 184 | } 185 | .CodeMirror-scrollbar-filler { 186 | right: 0; bottom: 0; 187 | } 188 | .CodeMirror-gutter-filler { 189 | left: 0; bottom: 0; 190 | } 191 | 192 | .CodeMirror-gutters { 193 | position: absolute; left: 0; top: 0; 194 | z-index: 3; 195 | } 196 | .CodeMirror-gutter { 197 | white-space: normal; 198 | height: 100%; 199 | display: inline-block; 200 | margin-bottom: -30px; 201 | /* Hack to make IE7 behave */ 202 | *zoom:1; 203 | *display:inline; 204 | } 205 | .CodeMirror-gutter-wrapper { 206 | position: absolute; 207 | z-index: 4; 208 | background: none !important; 209 | border: none !important; 210 | } 211 | .CodeMirror-gutter-background { 212 | position: absolute; 213 | top: 0; bottom: 0; 214 | z-index: 4; 215 | } 216 | .CodeMirror-gutter-elt { 217 | position: absolute; 218 | cursor: default; 219 | z-index: 4; 220 | } 221 | .CodeMirror-gutter-wrapper { 222 | -webkit-user-select: none; 223 | -moz-user-select: none; 224 | user-select: none; 225 | } 226 | 227 | .CodeMirror-lines { 228 | cursor: text; 229 | min-height: 1px; /* prevents collapsing before first draw */ 230 | } 231 | .CodeMirror pre { 232 | /* Reset some styles that the rest of the page might have set */ 233 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 234 | border-width: 0; 235 | background: transparent; 236 | font-family: inherit; 237 | font-size: inherit; 238 | margin: 0; 239 | white-space: pre; 240 | word-wrap: normal; 241 | line-height: inherit; 242 | color: inherit; 243 | z-index: 2; 244 | position: relative; 245 | overflow: visible; 246 | -webkit-tap-highlight-color: transparent; 247 | } 248 | .CodeMirror-wrap pre { 249 | word-wrap: break-word; 250 | white-space: pre-wrap; 251 | word-break: normal; 252 | } 253 | 254 | .CodeMirror-linebackground { 255 | position: absolute; 256 | left: 0; right: 0; top: 0; bottom: 0; 257 | z-index: 0; 258 | } 259 | 260 | .CodeMirror-linewidget { 261 | position: relative; 262 | z-index: 2; 263 | overflow: auto; 264 | } 265 | 266 | .CodeMirror-widget {} 267 | 268 | .CodeMirror-code { 269 | outline: none; 270 | } 271 | 272 | /* Force content-box sizing for the elements where we expect it */ 273 | .CodeMirror-scroll, 274 | .CodeMirror-sizer, 275 | .CodeMirror-gutter, 276 | .CodeMirror-gutters, 277 | .CodeMirror-linenumber { 278 | -moz-box-sizing: content-box; 279 | box-sizing: content-box; 280 | } 281 | 282 | .CodeMirror-measure { 283 | position: absolute; 284 | width: 100%; 285 | height: 0; 286 | overflow: hidden; 287 | visibility: hidden; 288 | } 289 | 290 | .CodeMirror-cursor { position: absolute; } 291 | .CodeMirror-measure pre { position: static; } 292 | 293 | div.CodeMirror-cursors { 294 | visibility: hidden; 295 | position: relative; 296 | z-index: 3; 297 | } 298 | div.CodeMirror-dragcursors { 299 | visibility: visible; 300 | } 301 | 302 | .CodeMirror-focused div.CodeMirror-cursors { 303 | visibility: visible; 304 | } 305 | 306 | .CodeMirror-selected { background: #d9d9d9; } 307 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 308 | .CodeMirror-crosshair { cursor: crosshair; } 309 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 310 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 311 | 312 | .cm-searching { 313 | background: #ffa; 314 | background: rgba(255, 255, 0, .4); 315 | } 316 | 317 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */ 318 | .CodeMirror span { *vertical-align: text-bottom; } 319 | 320 | /* Used to force a border model for a node */ 321 | .cm-force-border { padding-right: .1px; } 322 | 323 | @media print { 324 | /* Hide the cursor when printing */ 325 | .CodeMirror div.CodeMirror-cursors { 326 | visibility: hidden; 327 | } 328 | } 329 | 330 | /* See issue #2901 */ 331 | .cm-tab-wrap-hack:after { content: ''; } 332 | 333 | /* Help users use markselection to safely style text background */ 334 | span.CodeMirror-selectedtext { background: none; } 335 | -------------------------------------------------------------------------------- /src/sass/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /src/js/components/Settings.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | import React, { Component, PropTypes } from 'react'; 4 | import JSZip from 'jszip'; 5 | import Select from 'react-select'; 6 | 7 | import io from '../backend/io'; 8 | import log from '../backend/log'; 9 | import event from '../backend/event'; 10 | import Lang from '../backend/language'; 11 | import update from '../backend/update'; 12 | import Manifest from '../backend/manifest'; 13 | import { remote, dialog, ipcRenderer } from '../backend/nw.interface'; 14 | import { EVENT, 15 | APP_NAME, 16 | USER_HOME, 17 | APP_VERSION, 18 | APP_HOMEPAGE, 19 | APP_RELEASES_URL, 20 | HOSTS_COUNT_MATHER, 21 | SURGE_HOSTS_HEADER } from '../constants'; 22 | 23 | import Titlebar from './Titlebar'; 24 | 25 | const terminate = remote.getGlobal('terminate'); 26 | const updateStatus = remote.getGlobal('updateStatus'); 27 | 28 | class Settings extends Component { 29 | constructor(props) { 30 | super(props); 31 | this.state = { 32 | activeIndex: 0, 33 | locale: Lang.getCurrentLocale(), 34 | updateStatus: updateStatus.get(), 35 | }; 36 | } 37 | 38 | componentDidMount () { 39 | ipcRenderer.on('UPDATE_STATUS', (eventEmitter, value) => { 40 | this.setState({ updateStatus: value }); 41 | }); 42 | } 43 | 44 | __onLinkClick (index) { 45 | this.setState({ activeIndex: index }); 46 | } 47 | 48 | __onImportZipClick () { 49 | const openPath = dialog.showOpenDialog({ 50 | filters: [ 51 | { name: 'Zip', extensions: ['zip'] }, 52 | { name: Lang.get('common.all_files'), extensions: ['*'] }, 53 | ] 54 | }); 55 | if (!openPath || !openPath.length) { 56 | return; 57 | } 58 | const confirm = dialog.showMessageBox({ 59 | buttons: ['OK', 'Cancel'], 60 | type: 'warning', 61 | title: Lang.get('common.warning'), 62 | detail: Lang.get('settings.confirm_continue'), 63 | message: Lang.get('settings.overridden_warning'), 64 | }); 65 | if (confirm === 0) { 66 | io.readFile(openPath[0]).then((data) => { 67 | const zip = new JSZip(data); 68 | const manifest = new Manifest(JSON.parse(zip.file('manifest.json').asText())); 69 | const promises = []; 70 | manifest.getHostsList().forEach((hosts) => { 71 | hosts.setText(zip.file(hosts.uid).asText()); 72 | promises.push(hosts.save()); 73 | }); 74 | promises.push(manifest.commit()); 75 | return Promise.all(promises); 76 | }).then(() => { 77 | dialog.showMessageBox({ 78 | buttons: ['OK'], 79 | type: 'info', 80 | title: Lang.get('common.success'), 81 | message: Lang.get('settings.import_complete'), 82 | detail: Lang.get('settings.please_restart_app', APP_NAME), 83 | }); 84 | terminate(); 85 | }).catch((e) => { 86 | dialog.showErrorBox( 87 | Lang.get('settings.import_error'), 88 | Lang.get('settings.confirm_a_valid_file') 89 | ); 90 | log(e); 91 | }); 92 | } 93 | } 94 | 95 | __onExportZipClick () { 96 | const zip = new JSZip(); 97 | const { manifest } = this.props; 98 | zip.file('manifest.json', JSON.stringify(manifest.toSimpleObject())); 99 | manifest.getHostsList().forEach((hosts) => { 100 | zip.file(hosts.uid, hosts.text); 101 | }); 102 | Promise.resolve(zip.generate({ type: 'nodebuffer' })).then((buffer) => { 103 | const savePath = dialog.showSaveDialog({ 104 | defaultPath: path.join(USER_HOME, APP_NAME + '-export.zip'), 105 | filters: [ 106 | { name: 'Zip', extensions: ['zip'] }, 107 | ] 108 | }); 109 | if (savePath) { 110 | return io.writeFile(savePath, buffer); 111 | } else { 112 | return Promise.resolve(); 113 | } 114 | }).catch(log); 115 | } 116 | 117 | __onExportSurgeClick () { 118 | const { manifest } = this.props; 119 | let ret; 120 | let text = ''; 121 | const hosts = manifest.getMergedHosts(); 122 | while ((ret = HOSTS_COUNT_MATHER.exec(hosts.text)) !== null) { 123 | if (ret.index === HOSTS_COUNT_MATHER.lastIndex) { 124 | HOSTS_COUNT_MATHER.lastIndex++; 125 | } 126 | if (ret[2].indexOf('localhost') > -1 || 127 | ret[2].indexOf('broadcasthost') > -1) { 128 | continue; 129 | } 130 | text += `${ ret[2] } = ${ ret[1] }\n`; 131 | } 132 | text = SURGE_HOSTS_HEADER + text; 133 | const savePath = dialog.showSaveDialog({ 134 | defaultPath: path.join(USER_HOME, APP_NAME + '-surge.txt'), 135 | filters: [ 136 | { name: Lang.get('common.text'), extensions: ['txt'] }, 137 | ] 138 | }); 139 | if (savePath) { 140 | io.writeFile(savePath, text); 141 | } 142 | } 143 | 144 | __onLanguageChange (lang) { 145 | const { manifest } = this.props; 146 | const currentLocale = Lang.getCurrentLocale(); 147 | this.setState({ locale: lang }); 148 | if (lang.value !== currentLocale.value) { 149 | manifest.language = lang.value; 150 | manifest.commit().then(() => { 151 | dialog.showMessageBox({ 152 | buttons: ['OK'], 153 | type: 'info', 154 | title: Lang.get('common.success'), 155 | message: Lang.get('settings.language_changed'), 156 | detail: Lang.get('settings.please_restart_app', APP_NAME), 157 | }); 158 | terminate(); 159 | }); 160 | } 161 | } 162 | 163 | __onCheckUpdateClick () { 164 | update(true); 165 | } 166 | 167 | __onHomepageClick () { 168 | event.emit(EVENT.OPEN_EXTERNAL_URL, APP_HOMEPAGE); 169 | } 170 | 171 | render() { 172 | const { activeIndex, updateStatus, locale } = this.state; 173 | const items = [ 174 | { name: 'import', label: Lang.get('settings.import') }, 175 | { name: 'export', label: Lang.get('settings.export') }, 176 | { name: 'language', label: Lang.get('settings.language') }, 177 | { name: 'about', label: Lang.get('settings.about') }, 178 | ]; 179 | const links = items.map((item, index) => { 180 | return (
  • { item.label }
  • ); 183 | }); 184 | let updateText; 185 | if (updateStatus === 'checking') { 186 | updateText = Lang.get('settings.checking_update'); 187 | } else if (updateStatus === 'downloading') { 188 | updateText = Lang.get('settings.downloading_update'); 189 | } else if (updateStatus === 'applying') { 190 | updateText = Lang.get('settings.applying_update'); 191 | } else { 192 | updateText = Lang.get('settings.check_update'); 193 | } 194 | return (
    195 | 199 |
    200 |
      201 | { links } 202 |
    203 |
    204 |
    205 | { Lang.get('settings.import') } 206 | 209 |
    210 |
    211 | { Lang.get('settings.export') } 212 | 215 | 218 |
    219 |
    220 | { Lang.get('settings.language') } 221 |