├── 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 | 
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 ();
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 |
203 |
204 |
205 | { Lang.get('settings.import') }
206 |
207 | { Lang.get('settings.import_from_zip') }
208 |
209 |
210 |
211 | { Lang.get('settings.export') }
212 |
213 | { Lang.get('settings.export_to_zip') }
214 |
215 |
216 | { Lang.get('settings.export_to_surge') }
217 |
218 |
219 |
220 | { Lang.get('settings.language') }
221 |
229 |
230 |
231 | { Lang.get('settings.about') }
232 | { Lang.get('settings.current_version', APP_VERSION) }
233 |
234 | { Lang.get('settings.homepage') }
235 |
236 |
239 | { updateText }
240 |
241 |
242 |
243 |
244 |
);
245 | }
246 | }
247 |
248 | Settings.propTypes = {
249 | manifest: PropTypes.object,
250 | };
251 |
252 | export default Settings;
253 |
--------------------------------------------------------------------------------
/src/js/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import Dropzone from 'react-dropzone';
3 | import dragula from 'react-dragula';
4 |
5 | import { EVENT,
6 | APP_NAME,
7 | TOTAL_HOSTS_UID,
8 | NO_PERM_ERROR_TAG,
9 | NO_PERM_ERROR_TAG_WIN32 } from '../constants';
10 |
11 | import io from '../backend/io';
12 | import log from '../backend/log';
13 | import event from '../backend/event';
14 | import Hosts from '../backend/hosts';
15 | import Lang from '../backend/language';
16 | import nw from '../backend/nw.interface';
17 | import permission from '../backend/permission';
18 |
19 | import Editor from './Editor';
20 | import Sidebar from './Sidebar';
21 | import Titlebar from './Titlebar';
22 | import SnackBar from './SnackBar';
23 |
24 | const getPosition = (element) => {
25 | return Array.prototype.slice.call(element.parentElement.children).indexOf(element);
26 | };
27 |
28 | class App extends Component {
29 | constructor(props) {
30 | super(props);
31 | this.totalHosts = null;
32 | this.dragStartPosition = -1;
33 | this.state = {
34 | snack: null,
35 | manifest: null,
36 | activeUid: TOTAL_HOSTS_UID,
37 | editingUid: null,
38 | searchText: '',
39 | }
40 | }
41 |
42 | componentDidMount () {
43 | const { manifest } = this.props;
44 | const updateRemoteHosts = manifest.getHostsList().map((hosts) => {
45 | return hosts.updateFromUrl().then(() => {
46 | this.__updateManifest(manifest);
47 | });
48 | });
49 | this.__updateManifest(manifest);
50 | Promise.all(updateRemoteHosts).then(() => {
51 | event.emit(EVENT.INITIAL_CLOUD_HOSTS_UPDATED);
52 | });
53 |
54 | const drake = dragula([document.querySelector('.sidebar-list-dragable')]);
55 | drake.on('drag', (element) => {
56 | this.dragStartPosition = getPosition(element);
57 | });
58 | drake.on('drop', (element) => {
59 | const { manifest } = this.state;
60 | if (manifest && this.dragStartPosition > -1) {
61 | manifest.moveHostsIndex(this.dragStartPosition, getPosition(element));
62 | manifest.commit();
63 | }
64 | drake.cancel(true);
65 | this.__updateManifest(manifest, false);
66 | });
67 | drake.on('cancel', (element) => {
68 | this.dragStartPosition = -1;
69 | });
70 | }
71 |
72 | __updateManifest (manifest) {
73 | this.totalHosts = manifest.getMergedHosts();
74 | manifest.saveSysHosts(this.totalHosts).catch((error) => {
75 | if (error.message.indexOf(NO_PERM_ERROR_TAG) > -1 ||
76 | error.message.indexOf(NO_PERM_ERROR_TAG_WIN32) > -1) {
77 | this.__onPermissionError();
78 | }
79 | });
80 | this.__createHostsTrayMenu(manifest);
81 | this.setState({ manifest });
82 | }
83 |
84 | __createHostsTrayMenu (manifest) {
85 | const menus = [];
86 | menus.push({
87 | label: this.totalHosts.name,
88 | type: 'checkbox',
89 | checked: this.totalHosts.online,
90 | click: () => {
91 | manifest.online = !manifest.online;
92 | manifest.commit();
93 | this.__updateManifest(manifest);
94 | }
95 | });
96 | for (let hosts of manifest.getHostsList()) {
97 | menus.push({
98 | label: hosts.name,
99 | type: 'checkbox',
100 | checked: hosts.online,
101 | click: () => {
102 | if (manifest.online) {
103 | hosts.toggleStatus();
104 | manifest.commit();
105 | }
106 | this.__updateManifest(manifest);
107 | }
108 | });
109 | }
110 | event.emit(EVENT.SET_HOSTS_MENU, menus);
111 | }
112 |
113 | __updateHosts (uid, text) {
114 | const { manifest } = this.state;
115 | const hosts = manifest.getHostsByUid(uid);
116 | if (uid !== TOTAL_HOSTS_UID && hosts) {
117 | hosts.setText(text);
118 | hosts.save();
119 | manifest.commit();
120 | this.__updateManifest(manifest);
121 | }
122 | }
123 |
124 | __onHostsClick (item, e) {
125 | e && e.stopPropagation && e.stopPropagation();
126 | this.setState({ activeUid: item.uid });
127 | }
128 |
129 | __onHostsRemove (item, e) {
130 | e && e.stopPropagation && e.stopPropagation();
131 | const { manifest } = this.state;
132 | manifest.removeHosts(item).commit();
133 | item.remove().then(() => {
134 | this.__updateManifest(manifest);
135 | });
136 | }
137 |
138 | __onHostsStatusChange (item, e) {
139 | e && e.stopPropagation && e.stopPropagation();
140 | const { manifest } = this.state;
141 | if (item.uid !== TOTAL_HOSTS_UID) {
142 | if (manifest.online) {
143 | manifest.getHostsByUid(item.uid).toggleStatus();
144 | manifest.commit();
145 | this.__updateManifest(manifest);
146 | }
147 | } else {
148 | manifest.online = !manifest.online;
149 | manifest.commit();
150 | this.__updateManifest(manifest);
151 | }
152 | }
153 |
154 | __createNewHosts (options) {
155 | const { manifest } = this.state;
156 | if (options && options.name) {
157 | const hosts = new Hosts(options);
158 | if (hosts.url) {
159 | hosts.updateFromUrl().then(() => {
160 | this.__updateManifest(manifest);
161 | });
162 | } else {
163 | hosts.save();
164 | }
165 | manifest.addHosts(hosts).commit();
166 | this.__updateManifest(manifest);
167 | }
168 | }
169 |
170 | __onUpdateHostsClick (nextHosts) {
171 | const { manifest } = this.state;
172 | if (nextHosts && nextHosts.name) {
173 | if (nextHosts.url) {
174 | nextHosts.updateFromUrl().then(() => {
175 | this.__updateManifest(manifest);
176 | });
177 | } else {
178 | nextHosts.save();
179 | }
180 | manifest.setHostsByUid(nextHosts.uid, nextHosts);
181 | manifest.commit();
182 | this.__updateManifest(manifest);
183 | }
184 | this.setState({ editingUid: null });
185 | }
186 |
187 | __onHostsEdit (hosts, e) {
188 | e && e.stopPropagation && e.stopPropagation();
189 | this.setState({ editingUid: hosts.uid });
190 | }
191 |
192 | __onSearchChange (text) {
193 | this.setState({ searchText: text });
194 | }
195 |
196 | __onSnackDismiss () {
197 | this.setState({ snack: null });
198 | }
199 |
200 | __onLinuxPermissionSet() {
201 | this.setState({
202 | snack: {
203 | type: 'info',
204 | text: Lang.get('main.have_to_logout_for_permission'),
205 | actions: [
206 | {
207 | name: Lang.get('common.ok'),
208 | onClick: () => {
209 | this.__onSnackDismiss();
210 | }
211 | },
212 | ]
213 | }
214 | });
215 | }
216 |
217 | __onPermissionError () {
218 | this.setState({
219 | snack: {
220 | type: 'danger',
221 | text: Lang.get('main.dont_have_permission'),
222 | actions: [
223 | {
224 | name: Lang.get('main.grant_permission'),
225 | onClick: () => {
226 | permission.enableFullAccess().then(() => {
227 | if (process.platform !== 'linux') {
228 | this.__onSnackDismiss();
229 | } else {
230 | this.__onLinuxPermissionSet();
231 | }
232 | }).catch(log);
233 | }
234 | },
235 | ]
236 | }
237 | });
238 | }
239 |
240 | __onDrop (files) {
241 | const promises = io.readDropFiles(files);
242 | for (let promise of promises) {
243 | promise.then((result) => {
244 | this.__createNewHosts(result);
245 | });
246 | }
247 | }
248 |
249 | render() {
250 | const { snack, manifest, activeUid, editingUid, searchText } = this.state;
251 | let list = manifest ? manifest.getHostsList() : [];
252 | if (searchText) {
253 | list = list.filter((hosts) => {
254 | return hosts.name.indexOf(searchText) > -1 || hosts.text.indexOf(searchText) > -1;
255 | });
256 | }
257 | let activeHosts = null;
258 | if (activeUid !== null) {
259 | if (activeUid !== TOTAL_HOSTS_UID) {
260 | activeHosts = manifest.getHostsByUid(activeUid);
261 | } else {
262 | activeHosts = this.totalHosts;
263 | }
264 | }
265 | let editingHosts = null;
266 | if (editingUid !== null) {
267 | editingHosts = manifest.getHostsByUid(editingUid);
268 | }
269 | let readOnly = false;
270 | if (activeHosts && (TOTAL_HOSTS_UID === activeHosts.uid || activeHosts.url)) {
271 | readOnly = true;
272 | } else {
273 | readOnly = false;
274 | }
275 | return (
276 |
281 |
282 |
294 |
295 |
296 |
297 |
300 | { snack !== null ?
301 | :
306 | null }
307 | { activeHosts ?
308 | : null }
314 |
315 |
);
316 | }
317 | };
318 |
319 | App.propTypes = {
320 | manifest: PropTypes.object,
321 | };
322 |
323 | export default App;
324 |
--------------------------------------------------------------------------------
/src/sass/main.scss:
--------------------------------------------------------------------------------
1 | @import 'normalize';
2 | @import 'dragula';
3 | @import 'codemirror';
4 | @import 'react-select';
5 |
6 | $sidebar-width: 240px;
7 | $titlebar-height: 44px;
8 | $settings-links-width: 160px;
9 |
10 | $color-theme: rgb(2, 187, 0);
11 | $color-theme-red: rgb(253, 65, 33);
12 | $color-theme-blue: rgb(0, 169, 253);
13 | $background-dark: rgb(58, 58, 58);
14 | $background-light: rgb(242, 242, 242);
15 | $text-light: rgb(208, 208, 208);
16 | $text-grey: rgb(99, 104, 110);
17 | $text-dark: rgb(30, 30, 30);
18 | $border-light: rgb(221, 221, 221);
19 |
20 | * {
21 | outline: none;
22 | cursor: default;
23 | box-sizing: border-box;
24 | &[disabled]:hover {
25 | cursor: not-allowed;
26 | }
27 | }
28 |
29 | body {
30 | -webkit-user-select: none;
31 | font: 12px 'Myriad Set Pro', 'Helvetica Neue', 'Microsoft Yahei', Helvetica, Arial, sans-serif;
32 | .iconfont {
33 | -webkit-text-stroke-width: 0px;
34 | }
35 | }
36 |
37 | input[type="text"] {
38 | cursor: text;
39 | }
40 |
41 | button {
42 | padding: 8px 10px;
43 | border-radius: 3px;
44 | background-color: white;
45 | border: 1px solid $border-light;
46 | &:hover:not([disabled]) {
47 | color: white;
48 | background-color: $color-theme;
49 | box-shadow: 0 1px 0 rgba(0,0,0,0.06);
50 | border: 1px solid darken($color-theme, 10%);
51 | }
52 | }
53 |
54 | button {
55 | &[disabled] {
56 | color: $text-grey;
57 | background-color: $background-light;
58 | &:hover {
59 | color: $text-grey;
60 | background-color: $background-light;
61 | }
62 | }
63 | }
64 |
65 | #app {
66 | -webkit-app-region: no-drag;
67 | position: absolute;
68 | top: 15px;
69 | left: 15px;
70 | right: 15px;
71 | bottom: 15px;
72 | overflow: hidden;
73 | border-radius: 4px;
74 | box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.4),
75 | 0 0 15px rgba(0, 0, 0, 0.1);
76 | &.maximized {
77 | top: 0;
78 | left: 0;
79 | right: 0;
80 | bottom: 0;
81 | }
82 | }
83 |
84 | .searchbox {
85 | padding: 15px;
86 | input {
87 | width: 100%;
88 | border: none;
89 | border-radius: 2px;
90 | padding: 6px 25px;
91 | transition: background-color 0.2s ease;
92 | background-color: rgba(255, 255, 255, 0.1);
93 | &:focus {
94 | background-color: rgba(255, 255, 255, 0.05);
95 | }
96 | }
97 | .clear,
98 | .search {
99 | top: 20px;
100 | font-size: 14px;
101 | position: absolute;
102 | color: rgb(122, 126, 130);
103 | }
104 | .search {
105 | top: 22px;
106 | left: 21px;
107 | }
108 | .clear {
109 | top: 20px;
110 | right: 21px;
111 | cursor: pointer;
112 | }
113 | }
114 |
115 | .sidebar {
116 | top: 0;
117 | left: 0;
118 | bottom: 0;
119 | color: $text-light;
120 | position: absolute;
121 | width: $sidebar-width;
122 | background-color: $background-dark;
123 | .sidebar-list {
124 | left: 0;
125 | top: 58px;
126 | width: 100%;
127 | bottom: 53px;
128 | overflow-y: auto;
129 | overflow-x: hidden;
130 | position: absolute;
131 | }
132 | .sidebar-bottom {
133 | left: 0;
134 | bottom: 0;
135 | width: 100%;
136 | padding: 15px;
137 | position: absolute;
138 | .actions {
139 | float: right;
140 | .iconfont {
141 | cursor: pointer;
142 | font-size: 20px;
143 | margin-left: 8px;
144 | vertical-align: text-bottom;
145 | }
146 | .ok,
147 | .add,
148 | .settings {
149 | &:hover {
150 | color: $color-theme;
151 | }
152 | }
153 | }
154 | }
155 | }
156 | .sidebar-item {
157 | color: $text-light;
158 | padding: 10px 15px;
159 | position: relative;
160 | &:hover {
161 | background-color: rgba(255, 255, 255, 0.05);
162 | .edit,
163 | .delete {
164 | display: inline-block;
165 | }
166 | }
167 | &.active {
168 | background-color: rgba(255, 255, 255, 0.1);
169 | * {
170 | color: white;
171 | }
172 | }
173 | &.gu-mirror {
174 | background-color: $background-dark;
175 | }
176 | &.gu-transit {
177 | background-color: darken($background-dark, 10%);
178 | }
179 | .edit,
180 | .status,
181 | .delete {
182 | top: 50%;
183 | cursor: pointer;
184 | position: absolute;
185 | }
186 | .status {
187 | width: 12px;
188 | height: 12px;
189 | margin-top: -6px;
190 | border-radius: 6px;
191 | display: inline-block;
192 | border: 1px solid rgba(0, 0, 0, 0.6);
193 | background-color: rgba(0, 0, 0, 0.5);
194 | &.online {
195 | background-color: $color-theme;
196 | }
197 | }
198 | .content {
199 | margin-left: 24px;
200 | margin-right: 40px;
201 | p {
202 | margin: 0;
203 | }
204 | .name {
205 | overflow: hidden;
206 | margin-bottom: 3px;
207 | text-overflow: ellipsis;
208 | }
209 | .meta {
210 | color: $text-grey;
211 | * {
212 | line-height: 16px;
213 | vertical-align: text-top;
214 | }
215 | .iconfont {
216 | margin-right: 4px;
217 | }
218 | .syncing {
219 | animation: animSyncing 2s infinite;
220 | }
221 | }
222 | }
223 | .edit,
224 | .delete {
225 | width: 14px;
226 | height: 14px;
227 | display: none;
228 | font-size: 14px;
229 | margin-top: -7px;
230 | }
231 | .edit {
232 | right: 35px;
233 | }
234 | .delete {
235 | right: 15px;
236 | }
237 | }
238 |
239 | .main-container,
240 | .settings-container {
241 | top: 1px;
242 | left: 1px;
243 | right: 1px;
244 | bottom: 1px;
245 | overflow: hidden;
246 | position: absolute;
247 | background-color: $background-light;
248 | .titlebar {
249 | -webkit-app-region: drag;
250 | top: 0;
251 | left: 0;
252 | right: 0;
253 | position: absolute;
254 | height: $titlebar-height;
255 | .titlebar-title {
256 | text-align: center;
257 | line-height: $titlebar-height - 1px;
258 | }
259 | .window-controls {
260 | top: 12px;
261 | right: 12px;
262 | position: absolute;
263 | -webkit-app-region: no-drag;
264 | .iconfont {
265 | cursor: pointer;
266 | font-size: 20px;
267 | transition: color 0.2s ease;
268 | &:hover {
269 | color: $color-theme;
270 | }
271 | }
272 | .close:hover {
273 | color: rgb(242, 85, 97);
274 | }
275 | }
276 | }
277 | .snackbar {
278 | left: 0;
279 | right: 0;
280 | bottom: 0;
281 | color: white;
282 | z-index: 100;
283 | padding: 10px 15px;
284 | position: absolute;
285 | background: $color-theme;
286 | &.snackbar-info {
287 | background: $color-theme-blue;
288 | }
289 | &.snackbar-danger {
290 | background: $color-theme-red;
291 | }
292 | span {
293 | line-height: 16px;
294 | display: inline-block;
295 | vertical-align: text-bottom;
296 | }
297 | .snackbar-actions {
298 | float: right;
299 | span {
300 | color: white;
301 | cursor: pointer;
302 | margin-left: 8px;
303 | text-decoration: none;
304 | transition: 0.2s opacity ease;
305 | &:hover {
306 | opacity: 0.6;
307 | }
308 | }
309 | }
310 | }
311 | }
312 | .main-container {
313 | left: $sidebar-width;
314 | border-top-right-radius: 2px;
315 | border-bottom-right-radius: 2px;
316 | }
317 | .settings-container {
318 | border-radius: 2px;
319 | .settings {
320 | left: 0;
321 | right: 0;
322 | bottom: 0;
323 | top: 44px;
324 | position: absolute;
325 | border-top: 1px solid $border-light;
326 | .links,
327 | .contents {
328 | top: 0;
329 | height: 100%;
330 | overflow-y: auto;
331 | overflow-x: hidden;
332 | position: absolute;
333 | }
334 | .links {
335 | left: 0;
336 | margin: 0;
337 | padding: 0;
338 | list-style: none;
339 | text-align: right;
340 | padding-top: 20px;
341 | width: $settings-links-width;
342 | a {
343 | display: block;
344 | cursor: pointer;
345 | color: $text-grey;
346 | padding: 5px 20px;
347 | text-decoration: none;
348 | &:hover {
349 | color: $color-theme;
350 | }
351 | }
352 | li.active {
353 | a {
354 | color: $color-theme;
355 | }
356 | }
357 | }
358 | .contents {
359 | right: 0;
360 | padding-left: 20px;
361 | left: $settings-links-width;
362 | border-left: 1px solid $border-light;
363 | section {
364 | padding-top: 25px;
365 | button {
366 | margin-right: 10px;
367 | }
368 | .section-title {
369 | display: block;
370 | color: $text-grey;
371 | margin-bottom: 10px;
372 | }
373 | }
374 | }
375 | }
376 | .language-select {
377 | max-width: 200px;
378 | * {
379 | cursor: pointer;
380 | }
381 | .Select-control {
382 | height: auto;
383 | }
384 | .Select-placeholder,
385 | :not(.Select--multi)>.Select-control .Select-value {
386 | padding: 8px 10px;
387 | line-height: auto;
388 | }
389 | .Select-option.is-focused {
390 | color: white;
391 | background-color: $color-theme;
392 | }
393 | &.is-focused:not(.is-open)>.Select-control {
394 | box-shadow: none;
395 | border-color: darken($color-theme, 10%);
396 | }
397 | }
398 | }
399 | .ReactCodeMirror {
400 | left: 0;
401 | right: 0;
402 | bottom: 0;
403 | position: absolute;
404 | top: $titlebar-height;
405 | border-top: 1px solid $border-light;
406 | .CodeMirror {
407 | height: 100%;
408 | .CodeMirror-selected {
409 | background-color: rgb(185, 239, 255);
410 | }
411 | .CodeMirror-line {
412 | * {
413 | cursor: text;
414 | }
415 | }
416 | }
417 | }
418 |
419 | .popover {
420 | z-index: 100;
421 | color: $text-dark;
422 | position: absolute;
423 | .popover-content {
424 | padding: 15px;
425 | border-radius: 3px;
426 | background-color: white;
427 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
428 | .vertical-inputs {
429 | input[type="text"] {
430 | width: 100%;
431 | padding: 8px;
432 | display: block;
433 | margin-top: 10px;
434 | border-radius: 3px;
435 | border: 1px solid $border-light;
436 | &:focus {
437 | border-color: $color-theme;
438 | }
439 | }
440 | }
441 | }
442 | .popover-arrow {
443 | width: 0;
444 | height: 0;
445 | position: absolute;
446 | border-width: 10px;
447 | border-style: solid;
448 | border-color: transparent;
449 | }
450 | &.new-hosts-dialog {
451 | left: 15px;
452 | width: 400px;
453 | bottom: 50px;
454 | .popover-arrow {
455 | left: 161px;
456 | border-top-color: white;
457 | }
458 | .dialog-title {
459 | .close {
460 | float: right;
461 | cursor: pointer;
462 | line-height: 14px;
463 | &:hover {
464 | color: rgb(242, 85, 97);
465 | }
466 | }
467 | }
468 | }
469 | }
470 |
471 | .dropzone {
472 | height: 100%;
473 | position: absolute;
474 | width: $sidebar-width;
475 | transition: 0.2s ease;
476 | &.dropzone-active {
477 | -webkit-filter: blur(2px);
478 | }
479 | }
480 |
481 | .cm-s-default {
482 | .cm-comment {
483 | color: #085;
484 | }
485 | .cm-error {
486 | color: #FF2626;
487 | }
488 | }
489 |
490 | @keyframes animSyncing {
491 | 0% {
492 | color: white;
493 | }
494 | 50% {
495 | color: $color-theme;
496 | }
497 | 100% {
498 | color: white;
499 | }
500 | }
501 |
502 | ::-webkit-scrollbar {
503 | width: 8px;
504 | height: 8px;
505 | }
506 | ::-webkit-scrollbar-button {
507 | display: none;
508 | }
509 | ::-webkit-scrollbar-track {
510 | background: transparent;
511 | }
512 | ::-webkit-scrollbar-track-piece {
513 | background: transparent;
514 | }
515 | ::-webkit-scrollbar-thumb {
516 | border-radius: 5px;
517 | background-color: #8C8C8C;
518 | }
519 | ::selection {
520 | color: white;
521 | text-shadow: none;
522 | background: $color-theme;
523 | }
--------------------------------------------------------------------------------