oldj 8 years ago
parent
commit
893e0cf116

+ 84 - 84
app/bg/check_for_update.js

@@ -3,105 +3,105 @@
  * @blog http://oldj.net
  */
 
-'use strict';
+'use strict'
 
-const request = require('request');
-const cheerio = require('cheerio');
-const {shell, dialog} = require('electron');
-const current_version = require('../version').version;
-const m_lang = require('../ui/lang');
-const util = require('../ui/libs/util');
-const lang = m_lang.getLang(global.user_language);
+const request = require('request')
+const cheerio = require('cheerio')
+const {shell, dialog} = require('electron')
+const current_version = require('../version').version
+const m_lang = require('../server/lang')
+const util = require('../ui/libs/util')
+const lang = m_lang.getLang(global.user_language)
 
-function convertStrVersion(v) {
-    let a = v.match(/\d+/g);
-    return a.map(i => parseInt(i));
+function convertStrVersion (v) {
+  let a = v.match(/\d+/g)
+  return a.map(i => parseInt(i))
 }
 
-function compareVersion(a, b) {
-    if (typeof a === 'string') {
-        a = convertStrVersion(a);
-    }
-    if (typeof b === 'string') {
-        b = convertStrVersion(b);
-    }
+function compareVersion (a, b) {
+  if (typeof a === 'string') {
+    a = convertStrVersion(a)
+  }
+  if (typeof b === 'string') {
+    b = convertStrVersion(b)
+  }
 
-    let len = Math.max(a.length, b.length);
-    for (let i = 0; i < len; i++) {
-        let ai = a[i];
-        let bi = b[i];
+  let len = Math.max(a.length, b.length)
+  for (let i = 0; i < len; i++) {
+    let ai = a[i]
+    let bi = b[i]
 
-        if (typeof ai === 'number' && typeof bi === 'number') {
-            if (ai === bi) {
-                continue;
-            }
+    if (typeof ai === 'number' && typeof bi === 'number') {
+      if (ai === bi) {
+        continue
+      }
 
-            return ai - bi;
-        }
+      return ai - bi
+    }
 
-        if (typeof ai === 'number' && typeof bi !== 'number') {
-            return 1;
-        }
-        if (typeof ai !== 'number' && typeof bi === 'number') {
-            return -1;
-        }
-        return 0;
+    if (typeof ai === 'number' && typeof bi !== 'number') {
+      return 1
     }
+    if (typeof ai !== 'number' && typeof bi === 'number') {
+      return -1
+    }
+    return 0
+  }
 }
 
 exports.check = (is_silent = false, renderer = null) => {
-    let release_url = require('../ui/configs').url_download;
-    console.log('start check updates..');
-    request(release_url, (err, res, body) => {
-        let buttons = [lang.ok];
-        if (err) {
-            console.log(err);
+  let release_url = require('../ui/configs').url_download
+  console.log('start check updates..')
+  request(release_url, (err, res, body) => {
+    let buttons = [lang.ok]
+    if (err) {
+      console.log(err)
 
-            if (!is_silent) {
-                dialog.showMessageBox({
-                    type: 'error',
-                    message: lang.check_update_err,
-                    buttons
-                });
-            }
-            return;
-        }
+      if (!is_silent) {
+        dialog.showMessageBox({
+          type: 'error',
+          message: lang.check_update_err,
+          buttons
+        })
+      }
+      return
+    }
 
-        let $ = cheerio.load(body);
-        let a = $('.release-meta .css-truncate-target');
-        if (a.length <= 0) {
-            console.log('not found versios!');
-            return;
-        }
-        let last_v = $(a[0]).text();
-        // Array.from(a).map(i => {
-        //     console.log($(i).text());
-        // });
+    let $ = cheerio.load(body)
+    let a = $('.release-meta .css-truncate-target')
+    if (a.length <= 0) {
+      console.log('not found versios!')
+      return
+    }
+    let last_v = $(a[0]).text()
+    // Array.from(a).map(i => {
+    //     console.log($(i).text());
+    // });
 
-        let cmp = compareVersion(current_version, last_v);
-        console.log('cmp', cmp);
-        let message;
-        if (cmp >= 0) {
-            // 没有发现新版本
-            message = m_lang.fill(lang.check_update_nofound, util.formatVersion(current_version));
+    let cmp = compareVersion(current_version, last_v)
+    console.log('cmp', cmp)
+    let message
+    if (cmp >= 0) {
+      // 没有发现新版本
+      message = m_lang.fill(lang.check_update_nofound, util.formatVersion(current_version))
 
-        } else {
-            // 发现新版本
-            message = m_lang.fill(lang.check_update_found, last_v);
-            buttons.unshift(lang.cancel);
-            renderer.send('update_found', last_v);
-        }
+    } else {
+      // 发现新版本
+      message = m_lang.fill(lang.check_update_found, last_v)
+      buttons.unshift(lang.cancel)
+      renderer.send('update_found', last_v)
+    }
 
-        if (!is_silent) {
-            dialog.showMessageBox({
-                type: 'info',
-                message,
-                buttons
-            }, (res) => {
-                if (cmp < 0 && res === 1) {
-                    shell.openExternal(release_url);
-                }
-            });
+    if (!is_silent) {
+      dialog.showMessageBox({
+        type: 'info',
+        message,
+        buttons
+      }, (res) => {
+        if (cmp < 0 && res === 1) {
+          shell.openExternal(release_url)
         }
-    });
-};
+      })
+    }
+  })
+}

File diff suppressed because it is too large
+ 453 - 549
app/build/bundle.js


+ 2 - 2
app/main.js

@@ -18,7 +18,7 @@ const pref = require('./ui/libs/pref')
 let user_language = pref.get('user_language') ||
   (app.getLocale() || '').split('-')[0].toLowerCase() || 'en'
 global.user_language = user_language
-const tray = require('./ui/modules/tray')
+//const tray = require('./ui/modules/tray')
 const SHServer = require('./server/Server')
 
 // Keep a global reference of the window object, if you don't, the window will
@@ -74,7 +74,7 @@ function createWindow () {
 
   contents.on('did-finish-load', () => {
     if (!is_tray_initialized) {
-      tray.makeTray(app, contents, user_language)
+      //tray.makeTray(app, contents, user_language)
       is_tray_initialized = true
     }
   })

+ 6 - 0
app/renderer/Agent.js

@@ -20,6 +20,12 @@ const evt = new MyEmitter();
 
 let x_get_idx = 0
 
+/**
+ * act
+ * @param action {String}
+ * @param [data] {Any}
+ * @param callback {Function}
+ */
 function act (action, data, callback) {
   let fn = ['_cb', (new Date()).getTime(), (x_get_idx++)].join('_')
 

+ 8 - 9
app/server/Server.js

@@ -7,19 +7,18 @@
 
 const {ipcMain} = require('electron')
 const actions = require('./actions')
-console.log(actions)
 
 ipcMain.on('x', (e, d) => {
   let sender = e.sender
   let action = d.action
   if (typeof actions[action] === 'function') {
-    actions[action](d.args, (e, v) => {
-      try {
-        sender.send(d.callback, [e, v])
-      } catch (e2) {
-        console.log(e2)
-        sender.send(d.callback, [e2])
-      }
-    })
+    actions[action](...(d.args || []))
+      .then(v => {
+        sender.send(d.callback, [null, v])
+      })
+      .catch(e => {
+        console.log(e)
+        sender.send(d.callback, [e])
+      })
   }
 })

+ 4 - 4
app/server/actions/getHosts.js

@@ -10,13 +10,13 @@
 const getSysHosts = require('./getSysHosts')
 const getUserHosts = require('./getUserHosts')
 
-module.exports = (args, callback) => {
-  Promise
+module.exports = () => {
+  return Promise
     .all([getSysHosts(), getUserHosts()])
     .then(([sys_hosts, user_hosts]) => {
-      callback(null, {
+      return {
         sys: sys_hosts,
         list: user_hosts
-      })
+      }
     })
 }

+ 14 - 0
app/server/actions/getLang.js

@@ -0,0 +1,14 @@
+/**
+ * @author oldj
+ * @blog https://oldj.net
+ */
+
+'use strict'
+
+const m_lang = require('../lang')
+
+exports.getLang = (user_lang = 'en') => {
+  let lang = m_lang.getLang(user_lang)
+
+  return Promise.resolve().then(() => lang)
+}

+ 25 - 0
app/server/actions/getPref.js

@@ -0,0 +1,25 @@
+/**
+ * @author oldj
+ * @blog https://oldj.net
+ */
+
+'use strict'
+
+const paths = require('../paths')
+const io = require('../io')
+
+module.exports = () => {
+  let fn = paths.preference_path
+  return io
+    .pReadFile(fn)
+    .then(cnt => {
+      let data
+      try {
+        data = JSON.parse(cnt)
+      } catch (e) {
+        console.log(e)
+        data = {}
+      }
+      return data
+    })
+}

+ 2 - 2
app/server/actions/test.js

@@ -5,7 +5,7 @@
 
 'use strict'
 
-module.exports = (args, callback) => {
-  setTimeout(() => callback(null, 'ttt22'), 100)
+module.exports = () => {
+  return Promise.resolve().then(() => 'ttt33')
 }
 

+ 43 - 0
app/server/lang.js

@@ -0,0 +1,43 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict'
+
+const languages = {
+  'en': require('../common/lang/en').content,
+  'cn': require('../common/lang/cn').content
+}
+
+module.exports = {
+  languages: languages,
+  lang_list: (() => {
+    let list = []
+    for (let k in languages) {
+      if (languages.hasOwnProperty(k)) {
+        list.push({
+          key: k,
+          name: languages[k]._lang_name
+        })
+      }
+    }
+    return list
+  })(),
+  getLang: (lang) => {
+    lang = lang.toLowerCase()
+    if (lang === 'cn' || lang === 'zh-cn') {
+      lang = 'cn'
+    } else {
+      lang = 'en'
+    }
+    return languages[lang] || languages['en']
+  },
+  fill: (tpl, ...vals) => {
+    vals.map((v, idx) => {
+      let r = new RegExp('\\$\\{' + idx + '\\}', 'g')
+      tpl = tpl.replace(r, v)
+    })
+    return tpl
+  }
+}

+ 0 - 43
app/ui/lang.js

@@ -1,43 +0,0 @@
-/**
- * @author oldj
- * @blog http://oldj.net
- */
-
-"use strict";
-
-const languages = {
-    'en': require('../common/lang/en').content,
-    'cn': require('../common/lang/cn').content
-};
-
-module.exports = {
-    languages: languages,
-    lang_list: (() => {
-        let list = [];
-        for (let k in languages) {
-            if (languages.hasOwnProperty(k)) {
-                list.push({
-                    key: k,
-                    name: languages[k]._lang_name
-                });
-            }
-        }
-        return list;
-    })(),
-    getLang: (lang) => {
-        lang = lang.toLowerCase();
-        if (lang == 'cn' || lang == 'zh-cn') {
-            lang = 'cn';
-        } else {
-            lang = 'en';
-        }
-        return languages[lang] || languages['en'];
-    },
-    fill: (tpl, ...vals) => {
-        vals.map((v, idx) => {
-            let r = new RegExp('\\$\\{' + idx + '\\}', 'g');
-            tpl = tpl.replace(r, v);
-        });
-        return tpl;
-    }
-};

+ 33 - 15
app/ui2/components/app.js

@@ -19,31 +19,40 @@ class App extends React.Component {
     super(props)
 
     this.state = {
-      hosts: [],
-      current: {}
+      list: [],
+      sys: {},
+      current: {},
+      lang: {}
     }
 
     Agent.act('getUserHosts', (e, data) => {
       this.setState({
-        hosts: data.list
+        list: data.list,
+        sys: data.sys
       })
     })
-  }
 
-  setCurrent (host) {
-    if (host.is_sys) {
-      Agent.act('getSysHosts', (e, cnt) => {
+    Agent.act('getLang', (e, lang) => {
+      this.setState({lang})
+    })
+  }
 
+  setCurrent (hosts) {
+    if (hosts.is_sys) {
+      Agent.act('getSysHosts', (e, _hosts) => {
+        this.setState({
+          current: _hosts
+        })
       })
     } else {
+      this.setState({
+        current: hosts
+      })
     }
-    this.setState({
-      current: host.is_sys ? SH_Agent.getSysHosts() : host
-    })
   }
 
   static isReadOnly (host) {
-    return host.is_sys || host.where === 'remote'
+    return !host || host.is_sys || host.where === 'remote'
   }
 
   toSave () {
@@ -73,10 +82,19 @@ class App extends React.Component {
     let current = this.state.current
     return (
       <div id="app" className={'platform-' + Agent.platform}>
-        <Panel hosts={this.state.hosts} current={current}
-               setCurrent={this.setCurrent.bind(this)}/>
-        <Content current={current} readonly={App.isReadOnly(current)}
-                 setHostContent={this.setHostContent.bind(this)}/>
+        <Panel
+          list={this.state.list}
+          sys={this.state.sys}
+          current={current}
+          setCurrent={this.setCurrent.bind(this)}
+          lang={this.state.lang}
+        />
+        <Content
+          current={current}
+          readonly={App.isReadOnly(current)}
+          setHostContent={this.setHostContent.bind(this)}
+          lang={this.state.lang}
+        />
         <div className="frames">
           <SudoPrompt/>
           <EditPrompt hosts={this.state.hosts}/>

+ 76 - 76
app/ui2/components/content/content.js

@@ -3,93 +3,93 @@
  * @blog http://oldj.net
  */
 
-'use strict';
+'use strict'
 
-import React from 'react';
-import Editor from './editor';
-import classnames from 'classnames';
-import './content.less';
+import React from 'react'
+import Editor from './editor'
+import Agent from '../../../renderer/Agent'
+import classnames from 'classnames'
+import './content.less'
 
 export default class Content extends React.Component {
 
-    constructor(props) {
-        super(props);
+  constructor (props) {
+    super(props)
 
-        this.codemirror = null;
-        this.state = {
-            is_loading: this.props.current.is_loading,
-            code: this.props.current.content || ''
-        };
-        this._t = null;
-
-        SH_event.on('loading', (host) => {
-            if (host === this.props.current) {
-                this.setState({
-                    is_loading: true
-                });
-            }
-        });
-
-        SH_event.on('loading_done', (host, data) => {
-            if (host === this.props.current) {
-                this.setState({
-                    is_loading: false,
-                    code: data.content || ''
-                });
-            }
-        });
+    this.codemirror = null
+    this.state = {
+      is_loading: this.props.current.is_loading,
+      code: this.props.current.content || ''
     }
+    this._t = null
 
+    Agent.on('loading', (host) => {
+      if (host === this.props.current) {
+        this.setState({
+          is_loading: true
+        })
+      }
+    })
 
-    setValue(v) {
-        this.props.setHostContent(v);
-    }
-
-    componentWillReceiveProps(next_props) {
+    Agent.on('loading_done', (host, data) => {
+      if (host === this.props.current) {
         this.setState({
-            is_loading: next_props.current.is_loading,
-            code: next_props.current.content || ''
-        });
-    }
+          is_loading: false,
+          code: data.content || ''
+        })
+      }
+    })
+  }
+
+  setValue (v) {
+    this.props.setHostContent(v)
+  }
 
-    render() {
-        let {current} = this.props;
+  componentWillReceiveProps (next_props) {
+    this.setState({
+      is_loading: next_props.current.is_loading,
+      code: next_props.current.content || ''
+    })
+  }
 
-        return (
-            <div id="sh-content">
-                <div className="inform">
+  render () {
+    let {current} = this.props
+
+    return (
+      <div id="sh-content">
+        <div className="inform">
                     <span
-                        className={classnames({
-                            loading: 1,
-                            show: this.state.is_loading
-                        })}
+                      className={classnames({
+                        loading: 1,
+                        show: this.state.is_loading
+                      })}
                     >loading...</span>
-                    <i
-                        className={classnames({
-                            show: current.where === 'remote',
-                            iconfont: 1,
-                            'icon-earth': 1
-                        })}
-                        title={SH_Agent.lang.remote_hosts}
-                    />
-                    <i
-                        className={classnames({
-                            show: this.props.readonly,
-                            iconfont: 1,
-                            'icon-lock2': 1
-                        })}
-                        title={SH_Agent.lang.readonly}
-                    />
-                </div>
-                <div className={classnames({
-                    errorMessage: 1,
-                    show: !!this.props.current.error
-                })}>{this.props.current.error}</div>
-                <Editor
-                    code={this.state.code}
-                    readonly={this.props.readonly}
-                    setValue={this.setValue.bind(this)}/>
-            </div>
-        );
-    }
+          <i
+            className={classnames({
+              show: current.where === 'remote',
+              iconfont: 1,
+              'icon-earth': 1
+            })}
+            title={Agent.lang.remote_hosts}
+          />
+          <i
+            className={classnames({
+              show: this.props.readonly,
+              iconfont: 1,
+              'icon-lock2': 1
+            })}
+            title={Agent.lang.readonly}
+          />
+        </div>
+        <div className={classnames({
+          errorMessage: 1,
+          show: !!this.props.current.error
+        })}>{this.props.current.error}</div>
+        <Editor
+          code={this.state.code}
+          readonly={this.props.readonly}
+          setValue={this.setValue.bind(this)}/>
+      </div>
+    )
+  }
 }

+ 102 - 101
app/ui2/components/panel/buttons.js

@@ -3,116 +3,117 @@
  * @blog http://oldj.net
  */
 
-'use strict';
+'use strict'
 
-import React from 'react';
-import classnames from 'classnames';
-import './buttons.less';
+import React from 'react'
+import classnames from 'classnames'
+import Agent from '../../../renderer/Agent'
+import './buttons.less'
 
 export default class Buttons extends React.Component {
 
-    constructor(props) {
-        super(props);
+  constructor (props) {
+    super(props)
 
-        this.state = {
-            top_toggle_on: true,
-            search_on: false
-        };
-
-        this.on_items = null;
-
-        SH_event.on('toggle_host', (on) => {
-            if (on && !this.state.top_toggle_on) {
-                this.setState({
-                    top_toggle_on: true
-                });
-                this.on_items = null;
-            }
-        });
-
-        SH_event.on('cancel_search', () => {
-            this.calcelSearch();
-        });
-
-        ipcRenderer.on('to_add_host', () => {
-            SH_event.emit('add_host');
-        });
-
-    }
-
-    static btnAdd() {
-        SH_event.emit('add_host');
-    }
-
-    btnToggle() {
-        if (this.state.top_toggle_on) {
-            SH_event.emit('get_on_hosts', (items) => {
-                this.on_items = items;
-            });
-        }
-
-        this.setState({
-            top_toggle_on: !this.state.top_toggle_on
-        }, () => {
-            SH_event.emit('top_toggle', this.state.top_toggle_on, this.on_items);
-            if (this.state.top_toggle_on) {
-                this.on_items = null;
-            }
-        });
+    this.state = {
+      top_toggle_on: true,
+      search_on: false
     }
 
-    btnSearch() {
-        this.setState({
-            search_on: !this.state.search_on
-        }, () => {
-            SH_event.emit(this.state.search_on ? 'search_on' : 'search_off');
-        });
-    }
+    this.on_items = null
 
-    calcelSearch() {
+    Agent.on('toggle_host', (on) => {
+      if (on && !this.state.top_toggle_on) {
         this.setState({
-            search_on: false
-        }, () => {
-            SH_event.emit('search_off');
-        });
-    }
-
-    componentDidMount() {
-        ipcRenderer.on('to_search', () => {
-            this.btnSearch();
-        });
+          top_toggle_on: true
+        })
+        this.on_items = null
+      }
+    })
+
+    Agent.on('cancel_search', () => {
+      this.calcelSearch()
+    })
+
+    Agent.on('to_add_host', () => {
+      Agent.emit('add_host')
+    })
+
+  }
+
+  static btnAdd () {
+    Agent.emit('add_host')
+  }
+
+  btnToggle () {
+    if (this.state.top_toggle_on) {
+      Agent.emit('get_on_hosts', (items) => {
+        this.on_items = items
+      })
     }
 
-    render() {
-        return (
-            <div id="sh-buttons">
-                <div className="left">
-                    <a
-                        className="btn-add"
-                        href="#"
-                        onClick={() => Buttons.btnAdd()}
-                    >+</a>
-                </div>
-
-                <div className="right">
-                    <i
-                        className={classnames({
-                            iconfont: 1,
-                            'icon-search': 1,
-                            'on': this.state.search_on
-                        })}
-                        onClick={() => this.btnSearch()}
-                    />
-                    <i
-                        className={classnames({
-                            iconfont: 1,
-                            'icon-switchon': this.state.top_toggle_on,
-                            'icon-switchoff': !this.state.top_toggle_on
-                        })}
-                        onClick={() => this.btnToggle()}
-                    />
-                </div>
-            </div>
-        );
-    }
+    this.setState({
+      top_toggle_on: !this.state.top_toggle_on
+    }, () => {
+      Agent.emit('top_toggle', this.state.top_toggle_on, this.on_items)
+      if (this.state.top_toggle_on) {
+        this.on_items = null
+      }
+    })
+  }
+
+  btnSearch () {
+    this.setState({
+      search_on: !this.state.search_on
+    }, () => {
+      Agent.emit(this.state.search_on ? 'search_on' : 'search_off')
+    })
+  }
+
+  calcelSearch () {
+    this.setState({
+      search_on: false
+    }, () => {
+      Agent.emit('search_off')
+    })
+  }
+
+  componentDidMount () {
+    Agent.on('to_search', () => {
+      this.btnSearch()
+    })
+  }
+
+  render () {
+    return (
+      <div id="sh-buttons">
+        <div className="left">
+          <a
+            className="btn-add"
+            href="#"
+            onClick={() => Buttons.btnAdd()}
+          >+</a>
+        </div>
+
+        <div className="right">
+          <i
+            className={classnames({
+              iconfont: 1,
+              'icon-search': 1,
+              'on': this.state.search_on
+            })}
+            onClick={() => this.btnSearch()}
+          />
+          <i
+            className={classnames({
+              iconfont: 1,
+              'icon-switchon': this.state.top_toggle_on,
+              'icon-switchoff': !this.state.top_toggle_on
+            })}
+            onClick={() => this.btnToggle()}
+          />
+        </div>
+      </div>
+    )
+  }
 }

+ 168 - 300
app/ui2/components/panel/list.js

@@ -3,314 +3,182 @@
  * @blog http://oldj.net
  */
 
-'use strict';
-
-import React from 'react';
-import ListItem from './list_item';
-import update from 'react-addons-update';
-import './list.less';
+'use strict'
 
+import React from 'react'
+import ListItem from './list_item'
+import Agent from '../../../renderer/Agent'
+import update from 'react-addons-update'
+import './list.less'
 
 class List extends React.Component {
 
-    constructor(props) {
-        super(props);
-
-        this.state = {
-            current: this.props.current,
-            list: this.props.hosts.list
-        };
-        this.last_content = this.props.hosts.sys.content;
-
-        SH_event.on('imported', () => {
-            this.setState({
-                current: this.props.current,
-                list: this.props.hosts.list
-            }, () => {
-                SH_event.emit('change');
-            });
-        });
-
-        SH_event.on('change', () => {
-            SH_event.emit('save_data', this.state.list);
-            let content = this.getOnContent();
-            if (content !== this.last_content) {
-                SH_event.emit('apply', content, () => {
-                    this.last_content = content;
-                });
-            }
-        });
-
-        SH_event.on('host_added', (data) => {
-            this.setState({
-                list: update(this.state.list, {$push: [data]})
-            }, () => {
-                this.selectOne(data);
-
-                setTimeout(() => {
-                    SH_event.emit('change', true);
-                    let el = this.refs.items;
-                    el.scrollTop = document.querySelector('.list-item.selected').offsetTop - el.offsetHeight + 50;
-                    this.checkUpdateHost(data);
-                }, 100);
-            });
-
-        });
-
-        SH_event.on('host_edited', (data, host) => {
-            let idx = this.state.list.findIndex((item) => item == host);
-            if (idx == -1) return;
-
-            this.setState({
-                list: update(this.state.list, {$splice: [[idx, 1, data]]})
-            }, () => {
-                this.selectOne(data);
-
-                setTimeout(() => {
-                    SH_event.emit('change', true);
-                    this.checkUpdateHost(data, true);
-                }, 100);
-            });
-        });
-
-        SH_event.on('host_refreshed', (data, host) => {
-            let idx = this.state.list.findIndex((item) => item == host);
-            if (idx == -1) return;
-
-            this.setState({
-                list: update(this.state.list, {$splice: [[idx, 1, data]]})
-            }, () => {
-                setTimeout(() => {
-                    if (host === this.state.current) {
-                        this.selectOne(data);
-                    }
-                    SH_event.emit('change', true);
-                }, 100);
-            });
-        });
-
-        SH_event.on('del_host', (host) => {
-            let list = this.state.list;
-            let idx_to_del = list.findIndex((item) => {
-                return host === item;
-            });
-            if (idx_to_del == -1) return;
-            // list.splice(idx_to_del, 1);
-            this.setState({
-                list: update(this.state.list, {$splice: [[idx_to_del, 1]]})
-                // list: this.state.list.filter((item, idx) => idx != idx_to_del)
-            }, () => {
-                setTimeout(() => {
-                    let list = this.state.list;
-                    let next_host = list[idx_to_del] || list[list.length - 1] || this.props.hosts.sys;
-                    if (next_host) {
-                        this.selectOne(next_host);
-                    }
-                    SH_event.emit('change');
-                }, 100);
-            });
-        });
-
-        SH_event.on('get_on_hosts', (callback) => {
-            callback(this.getOnItems());
-        });
-
-        ipcRenderer.on('get_host_list', () => {
-            ipcRenderer.send('send_host_list', this.state.list);
-        });
-
-        ipcRenderer.on('get_export_data', (e, fn) => {
-            let data = Object.assign({}, {
-                version: require('../../configs').version,
-                list: this.state.list.map(item => {
-                    let new_item = Object.assign({}, item);
-                    new_item.on = false;
-                    return new_item;
-                })
-            });
-            ipcRenderer.send('export_data', fn, JSON.stringify(data));
-        });
-
-        SH_event.on('top_toggle', (on, items) => {
-            this.setState({
-                list: this.state.list.map((item) => {
-                    if (items.findIndex((i) => i == item) > -1) {
-                        item.on = on;
-                    }
-                    return item;
-                })
-            }, () => {
-                SH_event.emit('change');
-            });
-        });
-
-        SH_event.on('loading_done', (host, data) => {
-            SH_event.emit('host_refreshed', data, host);
-            // if (host == this.state.current || host._ == this.state.current) {
-            //     setTimeout(() => {
-            //         this.selectOne(this.state.current);
-            //     }, 100);
-            // }
-            if (data.error) {
-                console.log(data.error);
-            }
-        });
-
-        // auto check refresh
-        setTimeout(() => {
-            this.autoCheckRefresh();
-        }, 1000 * 5);
-    }
-
-    /**
-     * 检查当前 host 是否需要从网络下载更新
-     * @param host
-     * @param force {Boolean} 如果为 true,则只要是 remote 且 refresh_interval != 0,则强制更新
-     */
-    checkUpdateHost(host, force = false) {
-        SH_event.emit('check_host_refresh', host, force);
-    }
-
-    autoCheckRefresh() {
-        let remote_idx = -1;
-        this.state.list.map((host, idx) => {
-            if (host.where === 'remote') {
-                remote_idx++;
-            }
-            setTimeout(() => {
-                SH_event.emit('check_host_refresh', host);
-            }, 1000 * 5 * remote_idx + idx);
-        });
-
-        // let wait = 1000 * 60 * 10;
-        let wait = 1000 * 30; // test only
-        setTimeout(() => {
-            this.autoCheckRefresh();
-        }, wait);
-    }
+  constructor (props) {
+    super(props)
 
-    apply(content, success) {
-        SH_event.emit('apply', content, () => {
-            this.last_content = content;
-            success();
-            SH_event.emit('save_data', this.state.list);
-            SH_Agent.notify({
-                message: 'host updated.'
-            });
-        });
+    this.state = {
+      current: this.props.current,
+      list: this.props.list,
+      sys: this.props.sys
     }
-
-    selectOne(host) {
+    this.last_content = this.props.sys.content
+
+    // auto check refresh
+    setTimeout(() => {
+      this.autoCheckRefresh()
+    }, 1000 * 5)
+  }
+
+  /**
+   * 检查当前 host 是否需要从网络下载更新
+   * @param host
+   * @param force {Boolean} 如果为 true,则只要是 remote 且 refresh_interval != 0,则强制更新
+   */
+  checkUpdateHost (host, force = false) {
+    Agent.emit('check_host_refresh', host, force)
+  }
+
+  autoCheckRefresh () {
+    let remote_idx = -1
+    this.state.list.map((host, idx) => {
+      if (host.where === 'remote') {
+        remote_idx++
+      }
+      setTimeout(() => {
+        Agent.emit('check_host_refresh', host)
+      }, 1000 * 5 * remote_idx + idx)
+    })
+
+    // let wait = 1000 * 60 * 10;
+    let wait = 1000 * 30 // test only
+    setTimeout(() => {
+      this.autoCheckRefresh()
+    }, wait)
+  }
+
+  apply (content, success) {
+    Agent.emit('apply', content, () => {
+      this.last_content = content
+      success()
+      Agent.emit('save_data', this.state.list)
+      Agent.notify({
+        message: 'host updated.'
+      })
+    })
+  }
+
+  selectOne (host) {
+    this.setState({
+      current: host
+    })
+
+    this.props.setCurrent(host)
+  }
+
+  toggleOne (idx, success) {
+
+    let content = this.getOnContent(idx)
+    this.apply(content, () => {
+      let choice_mode = Agent.pref.get('choice_mode')
+      if (choice_mode === 'single') {
+        // 单选模式
         this.setState({
-            current: host
-        });
-
-        this.props.setCurrent(host);
-    }
-
-    toggleOne(idx, success) {
-
-        let content = this.getOnContent(idx);
-        this.apply(content, () => {
-            let choice_mode = SH_Agent.pref.get('choice_mode');
-            if (choice_mode === 'single') {
-                // 单选模式
-                this.setState({
-                    list: this.state.list.map((item, _idx) => {
-                        if (idx != _idx) {
-                            item.on = false;
-                        }
-                        return item;
-                    })
-                });
-            }
-
-            if (typeof success === 'function') {
-                success();
+          list: this.state.list.map((item, _idx) => {
+            if (idx !== _idx) {
+              item.on = false
             }
-        });
-    }
-
-    getOnItems(idx = -1) {
-        let choice_mode = SH_Agent.pref.get('choice_mode');
-        return this.state.list.filter((item, _idx) => {
-            if (choice_mode === 'single') {
-                return !item.on && _idx == idx;
-            } else {
-                return (item.on && _idx != idx) || (!item.on && _idx == idx);
-            }
-        });
-    }
-
-    getOnContent(idx = -1) {
-        let contents = this.getOnItems(idx).map((item) => {
-            return item.content || '';
-        });
-
-        contents.unshift('# SwitchHosts!');
-
-        return contents.join(`\n\n`);
-    }
-
-    customItems() {
-        return this.state.list.map((item, idx) => {
-            return (
-                <ListItem
-                    data={item}
-                    idx={idx}
-                    selectOne={this.selectOne.bind(this)}
-                    current={this.state.current}
-                    onToggle={(success)=> this.toggleOne(idx, success)}
-                    key={'host-' + idx}
-                    dragOrder={(sidx, tidx) => this.dragOrder(sidx, tidx)}
-                />
-            )
-        });
-    }
-
-    dragOrder(source_idx, target_idx) {
-        let source = this.state.list[source_idx];
-        let target = this.state.list[target_idx];
-
-        let list = this.state.list.filter((item, idx) => idx != source_idx);
-        let new_target_idx = list.findIndex((item) => item == target);
-
-        let to_idx;
-        if (source_idx < target_idx) {
-            // append
-            to_idx = new_target_idx + 1;
-        } else {
-            // insert before
-            to_idx = new_target_idx;
-        }
-        list.splice(to_idx, 0, source);
-
-        this.setState({
-            list: list
-        });
-
-        setTimeout(() => {
-            SH_event.emit('change');
-        }, 100);
-    }
-
-    componentDidMount() {
-    }
-
-    render() {
-        return (
-            <div id="sh-list">
-                <ListItem
-                    data={this.props.hosts.sys}
-                    selectOne={this.selectOne.bind(this)}
-                    current={this.state.current}
-                    sys="1"/>
-                <div ref="items" className="custom-items">
-                    {this.customItems()}
-                </div>
-            </div>
-        );
+            return item
+          })
+        })
+      }
+
+      if (typeof success === 'function') {
+        success()
+      }
+    })
+  }
+
+  getOnItems (idx = -1) {
+    let choice_mode = Agent.pref.get('choice_mode')
+    return this.state.list.filter((item, _idx) => {
+      if (choice_mode === 'single') {
+        return !item.on && _idx === idx
+      } else {
+        return (item.on && _idx !== idx) || (!item.on && _idx === idx)
+      }
+    })
+  }
+
+  getOnContent (idx = -1) {
+    let contents = this.getOnItems(idx).map((item) => {
+      return item.content || ''
+    })
+
+    contents.unshift('# SwitchHosts!')
+
+    return contents.join(`\n\n`)
+  }
+
+  customItems () {
+    return this.state.list.map((item, idx) => {
+      return (
+        <ListItem
+          data={item}
+          idx={idx}
+          selectOne={this.selectOne.bind(this)}
+          current={this.state.current}
+          onToggle={(success) => this.toggleOne(idx, success)}
+          key={'host-' + idx}
+          dragOrder={(sidx, tidx) => this.dragOrder(sidx, tidx)}
+        />
+      )
+    })
+  }
+
+  dragOrder (source_idx, target_idx) {
+    let source = this.state.list[source_idx]
+    let target = this.state.list[target_idx]
+
+    let list = this.state.list.filter((item, idx) => idx !== source_idx)
+    let new_target_idx = list.findIndex((item) => item === target)
+
+    let to_idx
+    if (source_idx < target_idx) {
+      // append
+      to_idx = new_target_idx + 1
+    } else {
+      // insert before
+      to_idx = new_target_idx
     }
+    list.splice(to_idx, 0, source)
+
+    this.setState({
+      list: list
+    })
+
+    setTimeout(() => {
+      Agent.emit('change')
+    }, 100)
+  }
+
+  componentDidMount () {
+  }
+
+  render () {
+    return (
+      <div id="sh-list">
+        <ListItem
+          data={this.props.sys}
+          lang={this.props.lang}
+          selectOne={this.selectOne.bind(this)}
+          current={this.state.current}
+          sys="1"/>
+        <div ref="items" className="custom-items">
+          {this.customItems()}
+        </div>
+      </div>
+    )
+  }
 }
 
-export default List;
+export default List

+ 125 - 132
app/ui2/components/panel/list_item.js

@@ -3,149 +3,142 @@
  * @blog http://oldj.net
  */
 
-'use strict';
+'use strict'
 
-import React from 'react';
-import classnames from 'classnames';
-import {kw2re} from '../../libs/kw';
-import './list_item.less';
+import React from 'react'
+import classnames from 'classnames'
+import Agent from '../../../renderer/Agent'
+import { kw2re } from '../../libs/kw'
+import './list_item.less'
 
 export default class ListItem extends React.Component {
-    constructor(props) {
-        super(props);
-
-        this.is_sys = !!this.props.sys;
-        this.state = {
-            is_selected: false,
-            search_kw: '',
-            search_re: null,
-            // on: this.props.data.on,
-        };
-
-        SH_event.on('search', (kw) => {
-            this.setState({
-                search_kw: kw,
-                search_re: kw ? kw2re(kw) : null
-            });
-        });
-
-        ipcRenderer.on('tray_toggle_host', (e, idx) => {
-            // ipcRenderer.send('send_host_list', this.state.list);
-            // this.toggleOne(idx);
-            if (idx === this.props.idx) {
-                this.toggle();
-            }
-        });
-
-    }
-
-    getTitle() {
-        return this.is_sys ? SH_Agent.lang.sys_host_title : this.props.data.title || SH_Agent.lang.untitled;
+  constructor (props) {
+    super(props)
+
+    this.is_sys = !!this.props.sys
+    this.state = {
+      is_selected: false,
+      search_kw: '',
+      search_re: null
+      // on: this.props.data.on,
     }
 
-    beSelected() {
-        // this.setState({
-        //     is_selected: true
-        // });
-
-        this.props.selectOne(this.props.data);
+    Agent.on('search', (kw) => {
+      this.setState({
+        search_kw: kw,
+        search_re: kw ? kw2re(kw) : null
+      })
+    })
+  }
+
+  getTitle () {
+    let {lang} = this.props
+    return this.is_sys ? lang.sys_host_title : this.props.data.title ||
+      lang.untitled
+  }
+
+  beSelected () {
+    // this.setState({
+    //     is_selected: true
+    // });
+
+    this.props.selectOne(this.props.data)
+  }
+
+  toEdit () {
+    Agent.emit('edit_host', this.props.data)
+  }
+
+  toggle () {
+    let on = !this.props.data.on
+
+    this.props.onToggle(() => {
+      this.props.data.on = on
+      this.forceUpdate()
+    })
+
+    Agent.emit('toggle_host', on)
+  }
+
+  allowedDrop (e) {
+    e.preventDefault()
+  }
+
+  onDrop (e) {
+    if (this.props.sys) {
+      e.preventDefault()
+      return false
     }
+    let source_idx = parseInt(e.dataTransfer.getData('text'))
 
-    toEdit() {
-        SH_event.emit('edit_host', this.props.data);
-    }
+    this.props.dragOrder(source_idx, this.props.idx)
+  }
 
-    toggle() {
-        let on = !this.props.data.on;
+  onDrag (e) {
+    e.dataTransfer.setData('text', this.props.idx)
+  }
 
-        this.props.onToggle(() => {
-            this.props.data.on = on;
-            this.forceUpdate();
-        });
+  isMatched () {
+    if (this.props.sys) return true
+    let kw = this.state.search_kw
+    let re = this.state.search_re
+    if (!kw || kw === '/') return true
 
-        SH_event.emit('toggle_host', on);
-    }
+    let {title, content} = this.props.data
 
-    allowedDrop(e) {
-        e.preventDefault();
+    if (re) {
+      return re.test(title) || re.test(content)
+    } else {
+      return title.indexOf(kw) > -1 || content.indexOf(kw) > -1
     }
-
-    onDrop(e) {
-        if (this.props.sys) {
-            e.preventDefault();
-            return false;
-        }
-        let source_idx = parseInt(e.dataTransfer.getData('text'));
-
-        this.props.dragOrder(source_idx, this.props.idx);
-    }
-
-    onDrag(e) {
-        e.dataTransfer.setData('text', this.props.idx);
-    }
-
-    isMatched() {
-        if (this.props.sys) return true;
-        let kw = this.state.search_kw;
-        let re = this.state.search_re;
-        if (!kw || kw === '/') return true;
-
-        let {title, content} = this.props.data;
-
-        if (re) {
-            return re.test(title) || re.test(content);
-        } else {
-            return title.indexOf(kw) > -1 || content.indexOf(kw) > -1;
-        }
-    }
-
-    render() {
-        let {data, sys, current} = this.props;
-        let is_selected = data == current;
-
-        return (
-            <div className={classnames({
-                'list-item': 1
-                , 'hidden': !this.isMatched()
-                , 'sys-host': sys
-                , 'selected': is_selected
+  }
+
+  render () {
+    let {data, sys, current} = this.props
+    let is_selected = data === current
+
+    return (
+      <div className={classnames({
+        'list-item': 1
+        , 'hidden': !this.isMatched()
+        , 'sys-host': sys
+        , 'selected': is_selected
+      })}
+           onClick={this.beSelected.bind(this)}
+           draggable={!sys}
+           onDragStart={(e) => this.onDrag(e)}
+           onDragOver={(e) => this.allowedDrop(e)}
+           onDrop={(e) => this.onDrop(e)}
+      >
+        { sys ? null : (
+          <div>
+            <i className={classnames({
+              'switch': 1
+              , 'iconfont': 1
+              , 'icon-on': data.on
+              , 'icon-off': !data.on
             })}
-                 onClick={this.beSelected.bind(this)}
-                 draggable={!sys}
-                 onDragStart={(e) => this.onDrag(e)}
-                 onDragOver={(e) => this.allowedDrop(e)}
-                 onDrop={(e) => this.onDrop(e)}
-            >
-                { sys ? null :
-                    (
-                        <div>
-                            <i className={classnames({
-                                'switch': 1
-                                , 'iconfont': 1
-                                , 'icon-on': data.on
-                                , 'icon-off': !data.on
-                            })}
-                               onClick={this.toggle.bind(this)}
-                            />
-                            <i
-                                className="iconfont icon-edit"
-                                onClick={this.toEdit.bind(this)}
-                            />
-                        </div>
-                    )
-                }
-                <i className={classnames({
-                    'iconfont': 1
-                    , 'item-icon': 1
-                    , 'icon-warn': !!data.error
-                    , 'icon-file': !sys && !data.error && data.where !== 'group'
-                    , 'icon-files': data.where === 'group'
-                    , 'icon-sysserver': sys && !data.error
-                })}
-                   title={data.error || ''}
-                />
-                <span>{this.getTitle()}</span>
-            </div>
-        );
-    }
+               onClick={this.toggle.bind(this)}
+            />
+            <i
+              className="iconfont icon-edit"
+              onClick={this.toEdit.bind(this)}
+            />
+          </div>
+        )
+        }
+        <i className={classnames({
+          'iconfont': 1
+          , 'item-icon': 1
+          , 'icon-warn': !!data.error
+          , 'icon-file': !sys && !data.error && data.where !== 'group'
+          , 'icon-files': data.where === 'group'
+          , 'icon-sysserver': sys && !data.error
+        })}
+           title={data.error || ''}
+        />
+        <span>{this.getTitle()}</span>
+      </div>
+    )
+  }
 }

+ 22 - 16
app/ui2/components/panel/panel.js

@@ -3,24 +3,30 @@
  * @blog http://oldj.net
  */
 
-'use strict';
+'use strict'
 
-import React from 'react';
-import Buttons from './buttons';
-import SearchBar from './searchbar';
-import List from './list';
-import './panel.less';
+import React from 'react'
+import Buttons from './buttons'
+import SearchBar from './searchbar'
+import List from './list'
+import './panel.less'
 
 export default class Panel extends React.Component {
-    render() {
-        let {current, hosts} = this.props;
+  render () {
+    let {current, list, sys, setCurrent, lang} = this.props
 
-        return (
-            <div id="panel">
-                <List hosts={hosts} current={current} setCurrent={this.props.setCurrent}/>
-                <SearchBar/>
-                <Buttons/>
-            </div>
-        );
-    }
+    return (
+      <div id="panel">
+        <List
+          list={list}
+          sys={sys}
+          current={current}
+          setCurrent={setCurrent}
+          lang={lang}
+        />
+        <SearchBar/>
+        <Buttons/>
+      </div>
+    )
+  }
 }

+ 60 - 59
app/ui2/components/panel/searchbar.js

@@ -3,77 +3,78 @@
  * @blog http://oldj.net
  */
 
-'use strict';
+'use strict'
 
-import React from 'react';
-import classnames from 'classnames';
-import './searchbar.less';
+import React from 'react'
+import Agent from '../../../renderer/Agent'
+import classnames from 'classnames'
+import './searchbar.less'
 
 export default class SearchBar extends React.Component {
 
-    constructor(props) {
-        super(props);
+  constructor (props) {
+    super(props)
 
-        this.state = {
-            show: false,
-            keyword: ''
-        };
+    this.state = {
+      show: false,
+      keyword: ''
+    }
 
-        this._t = null;
+    this._t = null
 
-        SH_event.on('search_on', () => {
-            this.setState({
-                show: true
-            }, () => {
-                setTimeout(() => {
-                    this.refs.keyword.focus();
-                }, 100);
-            });
-        });
+    Agent.on('search_on', () => {
+      this.setState({
+        show: true
+      }, () => {
+        setTimeout(() => {
+          this.refs.keyword.focus()
+        }, 100)
+      })
+    })
 
-        SH_event.on('search_off', () => {
-            this.clearSearch();
-        });
-    }
+    Agent.on('search_off', () => {
+      this.clearSearch()
+    })
+  }
 
-    clearSearch() {
-        this.setState({
-            show: false,
-            keyword: ''
-        });
-        SH_event.emit('search', '')
-    }
+  clearSearch () {
+    this.setState({
+      show: false,
+      keyword: ''
+    })
+    Agent.emit('search', '')
+  }
 
-    doSearch(kw) {
-        this.setState({
-            keyword: kw
-        });
+  doSearch (kw) {
+    this.setState({
+      keyword: kw
+    })
 
-        clearTimeout(this._t);
-        this._t = setTimeout(() => {
-            SH_event.emit('search', kw)
-        }, 300);
-    }
+    clearTimeout(this._t)
+    this._t = setTimeout(() => {
+      Agent.emit('search', kw)
+    }, 300)
+  }
 
-    onCancel() {
-        SH_event.emit('cancel_search');
-    }
+  onCancel () {
+    Agent.emit('cancel_search')
+  }
 
-    render() {
-        if (!this.state.show) {
-            return null;
-        }
-        return (
-            <div id="sh-searchbar">
-                <input
-                    ref="keyword"
-                    type="text"
-                    placeholder="keyword"
-                    value={this.state.keyword}
-                    onChange={(e) => this.doSearch(e.target.value)}
-                    onKeyDown={(e)=>(e.keyCode===27 && this.onCancel())}
-                />
-            </div>
-        );
+  render () {
+    if (!this.state.show) {
+      return null
     }
+    return (
+      <div id="sh-searchbar">
+        <input
+          ref="keyword"
+          type="text"
+          placeholder="keyword"
+          value={this.state.keyword}
+          onChange={(e) => this.doSearch(e.target.value)}
+          onKeyDown={(e) => (e.keyCode === 27 && this.onCancel())}
+        />
+      </div>
+    )
+  }
 }

+ 33 - 33
app/ui2/lang.js

@@ -3,41 +3,41 @@
  * @blog http://oldj.net
  */
 
-"use strict";
+'use strict'
 
 const languages = {
-    'en': require('../common/lang/en').content,
-    'cn': require('../common/lang/cn').content
-};
+  'en': require('../common/lang/en').content,
+  'cn': require('../common/lang/cn').content
+}
 
 module.exports = {
-    languages: languages,
-    lang_list: (() => {
-        let list = [];
-        for (let k in languages) {
-            if (languages.hasOwnProperty(k)) {
-                list.push({
-                    key: k,
-                    name: languages[k]._lang_name
-                });
-            }
-        }
-        return list;
-    })(),
-    getLang: (lang) => {
-        lang = lang.toLowerCase();
-        if (lang == 'cn' || lang == 'zh-cn') {
-            lang = 'cn';
-        } else {
-            lang = 'en';
-        }
-        return languages[lang] || languages['en'];
-    },
-    fill: (tpl, ...vals) => {
-        vals.map((v, idx) => {
-            let r = new RegExp('\\$\\{' + idx + '\\}', 'g');
-            tpl = tpl.replace(r, v);
-        });
-        return tpl;
+  languages: languages,
+  lang_list: (() => {
+    let list = []
+    for (let k in languages) {
+      if (languages.hasOwnProperty(k)) {
+        list.push({
+          key: k,
+          name: languages[k]._lang_name
+        })
+      }
     }
-};
+    return list
+  })(),
+  getLang: (lang) => {
+    lang = lang.toLowerCase()
+    if (lang === 'cn' || lang === 'zh-cn') {
+      lang = 'cn'
+    } else {
+      lang = 'en'
+    }
+    return languages[lang] || languages['en']
+  },
+  fill: (tpl, ...vals) => {
+    vals.map((v, idx) => {
+      let r = new RegExp('\\$\\{' + idx + '\\}', 'g')
+      tpl = tpl.replace(r, v)
+    })
+    return tpl
+  }
+}

+ 305 - 0
app/ui2/menu/mainMenu.js

@@ -0,0 +1,305 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+const path = require('path');
+const paths = require('../libs/paths');
+const {Menu, shell, ipcMain, dialog} = require('electron');
+const m_chk_update = require('../../bg/check_for_update');
+const m_lang = require('../lang');
+const pref = require('./../libs/pref');
+const version = require('../../version').version;
+
+exports.init = function (app, sys_lang = 'en') {
+    let lang = m_lang.getLang(pref.get('user_language', sys_lang));
+    let last_path = null;
+
+    const template = [
+        {
+            label: lang.file,
+            submenu: [
+                {
+                    label: lang.new,
+                    accelerator: 'CommandOrControl+N',
+                    click: () => {
+                        ipcMain.emit('to_add_host');
+                    }
+                }, {
+                    type: 'separator'
+                }, {
+                    label: lang.import,
+                    accelerator: 'Alt+CommandOrControl+I',
+                    click: () => {
+                        dialog.showOpenDialog({
+                            title: lang.import,
+                            defaultPath: path.join(last_path || paths.home_path, 'sh.json'),
+                            filters: [
+                                {name: 'JSON', extensions: ['json']},
+                                {name: 'All Files', extensions: ['*']}
+                            ]
+                        }, (fns) => {
+                            if (fns && fns.length > 0) {
+                                ipcMain.emit('to_import', fns[0]);
+                                last_path = path.dirname(fns[0]);
+                            }
+                        });
+                    }
+                }, {
+                    label: lang.export,
+                    accelerator: 'Alt+CommandOrControl+E',
+                    click: () => {
+                        dialog.showSaveDialog({
+                            title: lang.export,
+                            defaultPath: path.join(last_path || paths.home_path, 'sh.json'),
+                            filters: [
+                                {name: 'JSON', extensions: ['json']},
+                                {name: 'All Files', extensions: ['*']}
+                            ]
+                        }, (fn) => {
+                            if (fn) {
+                                ipcMain.emit('to_export', fn);
+                                last_path = path.dirname(fn);
+                            }
+                        });
+                    }
+                }, {
+                    type: 'separator'
+                }, {
+                    label: lang.preferences,
+                    accelerator: 'CommandOrControl+,',
+                    click: () => {
+                        app.mainWindow.webContents.send('show_preferences');
+                    }
+                }
+            ]
+        },
+        {
+            label: lang.edit,
+            submenu: [{
+                role: 'undo'
+            }, {
+                role: 'redo'
+            }, {
+                type: 'separator'
+            }, {
+                role: 'cut'
+            }, {
+                role: 'copy'
+            }, {
+                role: 'paste'
+            }, {
+                role: 'pasteandmatchstyle'
+            }, {
+                role: 'delete'
+            }, {
+                role: 'selectall'
+            }, {
+                type: 'separator'
+            }, {
+                label: lang.search,
+                accelerator: 'CommandOrControl+F',
+                click () {
+                    // ipcMain.emit('to_search');
+                    app.mainWindow.webContents.send('to_search');
+                }
+            }, {
+                label: lang.comment,
+                accelerator: 'CommandOrControl+/',
+                click () {
+                    // ipcMain.emit('to_search');
+                    app.mainWindow.webContents.send('to_comment');
+                }
+            }]
+        }, {
+            label: lang.view,
+            submenu: [
+                // {
+                //     label: 'Reload',
+                //     accelerator: 'CmdOrCtrl+R',
+                //     click (item, focusedWindow) {
+                //         if (focusedWindow) focusedWindow.reload()
+                //     }
+                // },
+                // {
+                //     label: 'Toggle Developer Tools',
+                //     accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
+                //     click (item, focusedWindow) {
+                //         if (focusedWindow) focusedWindow.webContents.toggleDevTools()
+                //     }
+                // },
+                // {
+                //     type: 'separator'
+                // },
+                {
+                    role: 'resetzoom'
+                }, {
+                    role: 'zoomin'
+                }, {
+                    role: 'zoomout'
+                }, {
+                    type: 'separator'
+                }, {
+                    role: 'togglefullscreen'
+                }
+            ]
+        }, {
+            label: lang.window,
+            role: 'window',
+            submenu: [{
+                role: 'minimize'
+            }, {
+                role: 'close'
+            }]
+        }, {
+            label: lang.help,
+            role: 'help',
+            submenu: [{
+                label: lang.check_update,
+                click () {
+                    m_chk_update.check();
+                }
+            }, {
+                type: 'separator'
+            }, {
+                label: lang.feedback,
+                click () {
+                    shell.openExternal('https://github.com/oldj/SwitchHosts/issues');
+                }
+            }, {
+                label: lang.homepage,
+                click () {
+                    shell.openExternal('http://oldj.github.io/SwitchHosts/');
+                }
+            }]
+        }
+    ];
+
+    const name = require('electron').app.getName();
+    const os = process.platform;
+    if (os === 'darwin') {
+        template.unshift({
+            label: name,
+            submenu: [{
+                role: 'about'
+            }, {
+                type: 'separator'
+            },
+                // {
+                //     role: 'services',
+                //     submenu: []
+                // },
+                // {
+                //     type: 'separator'
+                // },
+                {
+                    role: 'hide'
+                }, {
+                    role: 'hideothers'
+                }, {
+                    role: 'unhide'
+                }, {
+                    type: 'separator'
+                }, {
+                    role: 'quit'
+                }]
+        });
+        // Edit menu.
+        /*template[2].submenu.push(
+         {
+         type: 'separator'
+         },
+         {
+         label: 'Speech',
+         submenu: [
+         {
+         role: 'startspeaking'
+         },
+         {
+         role: 'stopspeaking'
+         }
+         ]
+         }
+         );*/
+        // Window menu.
+        template[4].submenu = [
+            {
+                label: 'Close',
+                accelerator: 'CmdOrCtrl+W',
+                role: 'close'
+            },
+            {
+                label: 'Minimize',
+                accelerator: 'CmdOrCtrl+M',
+                role: 'minimize'
+            },
+            {
+                label: 'Zoom',
+                role: 'zoom'
+            },
+            {
+                type: 'separator'
+            },
+            {
+                label: 'Bring All to Front',
+                role: 'front'
+            }
+        ]
+    } else if (os == 'win32') {
+        template[0].submenu.unshift({
+            type: 'separator'
+        });
+        template[0].submenu.unshift({
+            role: 'about',
+            click: () => {
+                dialog.showMessageBox({
+                    type: 'info',
+                    buttons: [],
+                    title: 'About',
+                    message: `${name} v${version.slice(0, 3).join('.')} (${version[3]})`
+                });
+            }
+        });
+
+        template[0].submenu.push({
+            type: 'separator'
+        });
+        template[0].submenu.push({
+            label: 'Quit',
+            role: 'quit',
+            accelerator: 'CmdOrCtrl+Q',
+        });
+
+        // VIEW
+        template[2].submenu.splice(0, 4);
+    }
+
+    if (process.env.ENV == 'dev') {
+        // VIEW
+        template[3].submenu = [
+            {
+                label: 'Reload',
+                accelerator: 'CmdOrCtrl+R',
+                click (item, focusedWindow) {
+                    if (focusedWindow) focusedWindow.reload()
+                }
+            },
+            {
+                label: 'Toggle Developer Tools',
+                accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
+                click (item, focusedWindow) {
+                    if (focusedWindow) focusedWindow.webContents.toggleDevTools()
+                }
+            },
+            {
+                type: 'separator'
+            }
+        ].concat(template[3].submenu);
+    }
+
+    const menu = Menu.buildFromTemplate(template);
+    Menu.setApplicationMenu(menu);
+}
+;

+ 116 - 0
app/ui2/menu/tray.js

@@ -0,0 +1,116 @@
+/**
+ * tray
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const {Menu, Tray, ipcMain, shell} = require('electron');
+const m_lang = require('../lang');
+const m_chk_update = require('../../bg/check_for_update');
+const pref = require('./../libs/pref');
+const os = process.platform;
+const util = require('../libs/util');
+const current_version = require('../../version').version;
+
+let tray = null;
+
+function makeMenu(app, list, contents, sys_lang) {
+    let menu = [];
+    let lang = m_lang.getLang(pref.get('user_language', sys_lang));
+
+    menu.push({
+        label: 'SwitchHosts!',
+        type: 'normal',
+        // sublabel: util.formatVersion(current_version), // does not work on Mac
+        click: () => {
+            app.emit('show');
+        }
+    });
+    menu.push({label: util.formatVersion(current_version), type: 'normal', enabled: false});
+    menu.push({label: '-', type: 'separator'});
+
+    let ac = '1234567890abcdefghijklmnopqrstuvwxyz'.split('');
+    list.map((item, idx) => {
+        menu.push({
+            label: item.title || 'untitled',
+            type: 'checkbox',
+            checked: item.on,
+            accelerator: ac[idx],
+            click: () => {
+                contents.send('tray_toggle_host', idx);
+                contents.send('get_host_list');
+            }
+        });
+    });
+
+    menu.push({type: 'separator'});
+    menu.push({
+        label: lang.feedback, type: 'normal', click: () => {
+            shell.openExternal('https://github.com/oldj/SwitchHosts/issues');
+        }
+    });
+
+    menu.push({
+        label: lang.check_update, type: 'normal', click: () => {
+            m_chk_update.check();
+        }
+    });
+
+    if (os === 'darwin') {
+        menu.push({
+            label: lang.toggle_dock_icon, type: 'normal', click: () => {
+                let is_dock_visible = app.dock.isVisible();
+                if (is_dock_visible) {
+                    app.dock.hide();
+                } else {
+                    app.dock.show();
+                }
+                pref.set('is_dock_icon_hidden', is_dock_visible);
+            }
+        });
+    }
+
+    menu.push({type: 'separator'});
+    menu.push({
+        label: lang.quit, type: 'normal', accelerator: 'CommandOrControl+Q', click: () => {
+            app.quit();
+        }
+    });
+
+    return menu;
+}
+
+function makeTray(app, contents, sys_lang = 'en') {
+    let icon = 'logo.png';
+    if (process.platform === 'darwin') {
+        icon = 'ilogoTemplate.png';
+    }
+
+    tray = new Tray(path.join(__dirname, '..', 'assets', icon));
+    tray.setToolTip('SwitchHosts!');
+
+    contents.send('get_host_list');
+
+    ipcMain.on('send_host_list', (e, d) => {
+        const contextMenu = Menu.buildFromTemplate(makeMenu(app, d, contents, sys_lang));
+        tray.setContextMenu(contextMenu);
+    });
+
+    let is_dock_icon_hidden = pref.get('is_dock_icon_hidden', false);
+    if (is_dock_icon_hidden) {
+        app.dock.hide();
+    }
+
+    // windows only
+    if (process.platform === 'win32') {
+        tray.on('click', () => {
+            app.emit('show');
+        });
+    }
+}
+
+exports.makeTray = makeTray;

Some files were not shown because too many files changed in this diff