Browse Source

开始 v3.3 版开发。

oldj 8 years ago
parent
commit
ac2ec0f698
43 changed files with 2980 additions and 18899 deletions
  1. 1 18781
      app/build/bundle.js
  2. 2 8
      app/index.html
  3. 107 108
      app/main.js
  4. 50 0
      app/renderer/Renderer.js
  5. 24 0
      app/server/Server.js
  6. 10 0
      app/server/actions.js
  7. 129 0
      app/ui2/components/app.js
  8. 20 0
      app/ui2/components/app.less
  9. 13 0
      app/ui2/components/cfg.less
  10. 50 0
      app/ui2/components/content/cm_hl.js
  11. 95 0
      app/ui2/components/content/content.js
  12. 61 0
      app/ui2/components/content/content.less
  13. 127 0
      app/ui2/components/content/editor.js
  14. 33 0
      app/ui2/components/content/editor.less
  15. 295 0
      app/ui2/components/frame/edit.js
  16. 55 0
      app/ui2/components/frame/edit.less
  17. 73 0
      app/ui2/components/frame/frame.js
  18. 108 0
      app/ui2/components/frame/frame.less
  19. 95 0
      app/ui2/components/frame/group.js
  20. 62 0
      app/ui2/components/frame/group.less
  21. 239 0
      app/ui2/components/frame/preferences.js
  22. 35 0
      app/ui2/components/frame/preferences.less
  23. 85 0
      app/ui2/components/frame/sudo.js
  24. 7 0
      app/ui2/components/frame/sudo.less
  25. 118 0
      app/ui2/components/panel/buttons.js
  26. 53 0
      app/ui2/components/panel/buttons.less
  27. 83 0
      app/ui2/components/panel/iconfont/iconfont.css
  28. BIN
      app/ui2/components/panel/iconfont/iconfont.eot
  29. 102 0
      app/ui2/components/panel/iconfont/iconfont.js
  30. 150 0
      app/ui2/components/panel/iconfont/iconfont.svg
  31. BIN
      app/ui2/components/panel/iconfont/iconfont.ttf
  32. BIN
      app/ui2/components/panel/iconfont/iconfont.woff
  33. 316 0
      app/ui2/components/panel/list.js
  34. 10 0
      app/ui2/components/panel/list.less
  35. 151 0
      app/ui2/components/panel/list_item.js
  36. 65 0
      app/ui2/components/panel/list_item.less
  37. 26 0
      app/ui2/components/panel/panel.js
  38. 10 0
      app/ui2/components/panel/panel.less
  39. 79 0
      app/ui2/components/panel/searchbar.js
  40. 19 0
      app/ui2/components/panel/searchbar.less
  41. 20 0
      app/ui2/ui.js
  42. 1 1
      app/version.js
  43. 1 1
      webpack.config.js

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


+ 2 - 8
app/index.html

@@ -7,14 +7,8 @@
 <body>
 <div id="app"></div>
 <script>
-  const IS_DEV = process.env.ENV === 'dev'
-  const SH_event = require('./ui/event').event
-  const SH_Agent = require('./ui/agent')
-  const {ipcRenderer} = require('electron')
-  const notifier = require('node-notifier')
-  const platform = process.platform
+  const SHRenderer = require('./renderer/Renderer')
+  require('./build/bundle')
 </script>
-<!--<script src="build/bundle.js"></script>-->
-<script>require('./build/bundle')</script>
 </body>
 </html>

+ 107 - 108
app/main.js

@@ -7,143 +7,142 @@
  * @source https://github.com/oldj/SwitchHosts
  */
 
-const electron = require('electron');
-const fs = require('fs');
+const electron = require('electron')
+const fs = require('fs')
 // Module to control application life.
-const app = electron.app;
+const app = electron.app
 // Module to create native browser window.
-const BrowserWindow = electron.BrowserWindow;
+const BrowserWindow = electron.BrowserWindow
+const pref = require('./ui/libs/pref')
 
-// const yargs = require('yargs');
-// console.log('argv', yargs.argv);
-
-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');
+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 SHServer = require('./server/Server')
 
 // Keep a global reference of the window object, if you don't, the window will
 // be closed automatically when the JavaScript object is garbage collected.
-let mainWindow;
-let contents;
-let willQuitApp = false;
-let is_tray_initialized;
-let renderer;
-
-function createWindow() {
-    // Create the browser window.
-    mainWindow = new BrowserWindow({
-        width: 800, height: 500,
-        minWidth: 400, minHeight: 250,
-        fullscreenable: true
-    });
-    contents = mainWindow.webContents;
-    app.mainWindow = mainWindow;
-
-    // and load the index.html of the app.
-    mainWindow.loadURL(`file://${__dirname}/index.html?lang=${user_language}`);
-
-    if (process.env && process.env.ENV === 'dev') {
-        // Open the DevTools.
-        mainWindow.webContents.openDevTools();
+let mainWindow
+let contents
+let willQuitApp = false
+let is_tray_initialized
+let renderer
+
+function createWindow () {
+  // Create the browser window.
+  mainWindow = new BrowserWindow({
+    width: 800, height: 500,
+    minWidth: 400, minHeight: 250,
+    fullscreenable: true
+  })
+  contents = mainWindow.webContents
+  app.mainWindow = mainWindow
+
+  // and load the index.html of the app.
+  mainWindow.loadURL(`file://${__dirname}/index.html?lang=${user_language}`)
+
+  if (process.env && process.env.ENV === 'dev') {
+    // Open the DevTools.
+    mainWindow.webContents.openDevTools()
+  }
+
+  if (pref.get('hide_at_launch')) {
+    // mainWindow.minimize();
+    mainWindow.hide()
+  }
+
+  mainWindow.on('close', (e) => {
+    if (willQuitApp) {
+      /* the user tried to quit the app */
+      mainWindow = null
+    } else {
+      /* the user only tried to close the window */
+      e.preventDefault()
+      mainWindow.hide()
     }
-
-    if (pref.get('hide_at_launch')) {
-        // mainWindow.minimize();
-        mainWindow.hide();
+  })
+
+  // Emitted when the window is closed.
+  mainWindow.on('closed', () => {
+    // Dereference the window object, usually you would store windows
+    // in an array if your app supports multi windows, this is the time
+    // when you should delete the corresponding element.
+    mainWindow = null
+    contents = null
+  })
+
+  contents.on('did-finish-load', () => {
+    if (!is_tray_initialized) {
+      tray.makeTray(app, contents, user_language)
+      is_tray_initialized = true
     }
+  })
 
-    mainWindow.on('close', (e) => {
-        if (willQuitApp) {
-            /* the user tried to quit the app */
-            mainWindow = null;
-        } else {
-            /* the user only tried to close the window */
-            e.preventDefault();
-            mainWindow.hide();
-        }
-    });
-
-    // Emitted when the window is closed.
-    mainWindow.on('closed', () => {
-        // Dereference the window object, usually you would store windows
-        // in an array if your app supports multi windows, this is the time
-        // when you should delete the corresponding element.
-        mainWindow = null;
-        contents = null;
-    });
-
-    contents.on('did-finish-load', () => {
-        if (!is_tray_initialized) {
-            tray.makeTray(app, contents, user_language);
-            is_tray_initialized = true;
-        }
-    });
-
-    require('./bg/events').init(app, contents);
+  require('./bg/events').init(app, contents)
 }
 
 const should_quit = app.makeSingleInstance((commandLine, workingDirectory) => {
-    // Someone tried to run a second instance, we should focus our window.
-    if (mainWindow) {
-        if (mainWindow.isMinimized()) {
-            mainWindow.restore();
-        }
-        mainWindow.show();
-        // mainWindow.focus();
+  // Someone tried to run a second instance, we should focus our window.
+  if (mainWindow) {
+    if (mainWindow.isMinimized()) {
+      mainWindow.restore()
     }
-});
+    mainWindow.show()
+    // mainWindow.focus();
+  }
+})
 
 if (should_quit) {
-    app.quit();
+  app.quit()
 }
 
 // This method will be called when Electron has finished
 // initialization and is ready to create browser windows.
 // Some APIs can only be used after this event occurs.
 app.on('ready', () => {
-    createWindow();
-    require('./ui/modules/mainMenu').init(app, user_language);
+  createWindow()
+  require('./ui/modules/mainMenu').init(app, user_language)
 
-    setTimeout(() => {
-        if (renderer) {
-            require('./bg/check_for_update').check(true, renderer);
-        }
-    }, 1000);
-});
+  setTimeout(() => {
+    if (renderer) {
+      require('./bg/check_for_update').check(true, renderer)
+    }
+  }, 1000)
+})
 
 electron.ipcMain.on('reg_renderer', (e) => {
-    renderer = e.sender;
-});
+  renderer = e.sender
+})
 
 // Quit when all windows are closed.
 app.on('window-all-closed', function () {
-    // if (process.platform !== 'darwin') {
-    //     app.quit();
-    // }
-});
+  // if (process.platform !== 'darwin') {
+  //     app.quit();
+  // }
+})
 
 app.on('show', function () {
-    if (mainWindow) {
-        if (mainWindow.isMinimized()) {
-            mainWindow.restore();
-        }
-        mainWindow.show();
-    } else {
-        createWindow();
+  if (mainWindow) {
+    if (mainWindow.isMinimized()) {
+      mainWindow.restore()
     }
-});
+    mainWindow.show()
+  } else {
+    createWindow()
+  }
+})
 
 app.on('activate', function () {
-    // On OS X it's common to re-create a window in the app when the
-    // dock icon is clicked and there are no other windows open.
-    if (mainWindow === null) {
-        createWindow();
-    } else if (mainWindow.isMinimized()) {
-        mainWindow.restore();
-    } else {
-        mainWindow.show();
-    }
-});
-
-app.on('before-quit', () => willQuitApp = true);
+  // On OS X it's common to re-create a window in the app when the
+  // dock icon is clicked and there are no other windows open.
+  if (mainWindow === null) {
+    createWindow()
+  } else if (mainWindow.isMinimized()) {
+    mainWindow.restore()
+  } else {
+    mainWindow.show()
+  }
+})
+
+app.on('before-quit', () => willQuitApp = true)

+ 50 - 0
app/renderer/Renderer.js

@@ -0,0 +1,50 @@
+/**
+ * @author oldj
+ * @blog https://oldj.net
+ */
+
+'use strict'
+
+const IS_DEV = process.env.ENV === 'dev'
+//const SH_event = require('./ui/event').event
+//const SH_Agent = require('./ui/agent')
+const {ipcRenderer} = require('electron')
+const notifier = require('node-notifier')
+const platform = process.platform
+
+ipcRenderer.setMaxListeners(20)
+
+let x_get_idx = 0
+
+function act (action, data, callback) {
+  let fn = ['_cb', (new Date()).getTime(), (x_get_idx++)].join('_')
+
+  if (!callback && typeof data === 'function') {
+    callback = data
+    data = null
+  }
+
+  if (typeof callback === 'function') {
+    ipcRenderer.once(fn, (e, d) => callback.apply(null, d))
+  }
+
+  console.log(fn)
+  ipcRenderer.send('x', {
+    action
+    , data
+    , callback: fn
+  })
+}
+
+function pact (action, data) {
+  return new Promise((resolve, reject) => act(action, data,
+    (err, result) => err ? reject(err) : resolve(result)))
+}
+
+module.exports = {
+  IS_DEV
+  , notifier
+  , platform
+  , act
+  , pact
+}

+ 24 - 0
app/server/Server.js

@@ -0,0 +1,24 @@
+/**
+ * @author oldj
+ * @blog https://oldj.net
+ */
+
+'use strict'
+
+const {ipcMain} = require('electron')
+const actions = require('./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])
+      }
+    })
+  }
+})

+ 10 - 0
app/server/actions.js

@@ -0,0 +1,10 @@
+/**
+ * @author oldj
+ * @blog https://oldj.net
+ */
+
+'use strict'
+
+exports.test = (data, callback) => {
+  setTimeout(() => callback(null, 'ttt'), 100)
+}

+ 129 - 0
app/ui2/components/app.js

@@ -0,0 +1,129 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict'
+
+import React from 'react'
+import Panel from './panel/panel'
+import Content from './content/content'
+import SudoPrompt from './frame/sudo'
+import EditPrompt from './frame/edit'
+import PreferencesPrompt from './frame/preferences'
+import util from '../libs/util'
+import './app.less'
+
+class App extends React.Component {
+  constructor (props) {
+    super(props)
+
+    let _data = SH_Agent.getHosts()
+
+    this.state = {
+      hosts: _data,
+      current: _data.sys
+    }
+
+    SH_event.on('after_apply', () => {
+      if (this.state.current.is_sys) {
+        // 重新读取
+        this.setState({
+          current: SH_Agent.getSysHosts()
+        })
+      }
+    })
+
+    ipcRenderer.on('to_import', (e, fn) => {
+      if (!confirm(SH_Agent.lang.confirm_import)) return
+
+      SH_Agent.readFile(fn, (err, cnt) => {
+        if (err) {
+          alert(err.message || 'Import Error!')
+          return
+        }
+        let data
+        try {
+          data = JSON.parse(cnt)
+        } catch (e) {
+          console.log(e)
+          alert(
+            e.message || 'Bad format, the import file should be a JSON file.')
+          return
+        }
+
+        if (!data.list || !Array.isArray(data.list)) {
+          alert('Bad format, the data JSON should have a [list] field.')
+          return
+        }
+
+        data.list.map(item => {
+          if (!item.id) {
+            item.id = util.makeId()
+          }
+        })
+
+        this.setState({
+          hosts: Object.assign({}, this.state.hosts, {list: data.list})
+        }, () => {
+          SH_event.emit('imported')
+        })
+        console.log('imported.')
+      })
+    })
+
+    ipcRenderer.send('reg_renderer')
+  }
+
+  setCurrent (host) {
+    this.setState({
+      current: host.is_sys ? SH_Agent.getSysHosts() : host
+    })
+  }
+
+  static isReadOnly (host) {
+    return host.is_sys || host.where === 'remote'
+  }
+
+  toSave () {
+    clearTimeout(this._t)
+
+    this._t = setTimeout(() => {
+      SH_event.emit('change')
+    }, 1000)
+  }
+
+  setHostContent (v) {
+    if (this.state.current.content === v) return // not changed
+
+    this.state.current.content = v || ''
+    this.toSave()
+  }
+
+  componentDidMount () {
+    window.addEventListener('keydown', (e) => {
+      if (e.keyCode === 27) {
+        SH_event.emit('esc')
+      }
+    }, false)
+  }
+
+  render () {
+    let current = this.state.current
+    return (
+      <div id="app" className={'platform-' + 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)}/>
+        <div className="frames">
+          <SudoPrompt/>
+          <EditPrompt hosts={this.state.hosts}/>
+          <PreferencesPrompt/>
+        </div>
+      </div>
+    )
+  }
+}
+
+export default App

+ 20 - 0
app/ui2/components/app.less

@@ -0,0 +1,20 @@
+@import "./cfg.less";
+
+html, body {
+  margin: 0;
+  padding: 0;
+  height: 100%;
+  font-size: 12px;
+  font-family: Arial, Helvetica, sans-serif;
+  color: @font_color;
+  line-height: 20px;
+  background: #fff;
+}
+
+a {
+  text-decoration: none;
+}
+
+#app {
+  height: 100%;
+}

+ 13 - 0
app/ui2/components/cfg.less

@@ -0,0 +1,13 @@
+@font_color: #212121;
+@bd_color: bg_left;
+@bg_prompt: #f5f5f5;
+@color_hover: #09f;
+@color_on: #af9;
+@color_off: font_color_left;
+@color_danger: #f03;
+
+// left
+@bg_left: #373d47;
+@bg_left_search: @bg_left * 1.1;
+@left_width: 240px;
+@font_color_left: #979da7;

+ 50 - 0
app/ui2/components/content/cm_hl.js

@@ -0,0 +1,50 @@
+// custom mode
+
+'use strict';
+
+import CodeMirror from 'codemirror';
+
+export default function () {
+
+    CodeMirror.defineMode('host', function () {
+        function tokenBase(stream) {
+            if (stream.eatSpace()) return null;
+
+            var sol = stream.sol();
+            var ch = stream.next();
+
+            var s = stream.string;
+
+            if (ch === '#') {
+                stream.skipToEnd();
+                return 'comment';
+            }
+            if (!s.match(/^\s*([\d\.]+|[\da-f:\.%lo]+)\s+\w/i)) {
+                return 'error';
+            }
+
+            if (sol && ch.match(/[\w\.:%]/)) {
+                stream.eatWhile(/[\w\.:%]/);
+                return 'ip';
+            }
+
+            return null;
+        }
+
+        function tokenize(stream, state) {
+            return (state.tokens[0] || tokenBase)(stream, state);
+        }
+
+        return {
+            startState: function () {
+                return {tokens: []};
+            },
+            token: function (stream, state) {
+                return tokenize(stream, state);
+            },
+            lineComment: '#'
+        };
+    });
+
+    //CodeMirror.defineMIME('text/x-host', 'host');
+}

+ 95 - 0
app/ui2/components/content/content.js

@@ -0,0 +1,95 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+import React from 'react';
+import Editor from './editor';
+import classnames from 'classnames';
+import './content.less';
+
+export default class Content extends React.Component {
+
+    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 || ''
+                });
+            }
+        });
+    }
+
+
+    setValue(v) {
+        this.props.setHostContent(v);
+    }
+
+    componentWillReceiveProps(next_props) {
+        this.setState({
+            is_loading: next_props.current.is_loading,
+            code: next_props.current.content || ''
+        });
+    }
+
+    render() {
+        let {current} = this.props;
+
+        return (
+            <div id="sh-content">
+                <div className="inform">
+                    <span
+                        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>
+        );
+    }
+}

+ 61 - 0
app/ui2/components/content/content.less

@@ -0,0 +1,61 @@
+#sh-content {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 240px;
+  height: 100%;
+
+  .inform {
+    position: absolute;
+    z-index: 100;
+    top: 5px;
+    right: 10px;
+    opacity: 0.5;
+    background: #fff;
+
+    i {
+      display: none;
+      color: #666;
+      margin-left: 5px;
+
+      &.show {
+        display: inline-block;
+      }
+    }
+
+    span {
+      display: none;
+
+      &.show {
+        display: inline-block;
+      }
+    }
+  }
+
+  .errorMessage {
+    display: none;
+    position: absolute;
+    z-index: 101;
+    top: 0;
+    left: 0;
+    right: 0;
+    padding: 4px 40px;
+    text-align: center;
+    background: rgba(153, 0, 0, 0.5);
+    color: #fff;
+    transition: 0.5s;
+
+    &.show {
+      display: block;
+    }
+  }
+}
+
+.platform-win32 {
+  #sh-content {
+    .inform {
+      right: 20px;
+    }
+  }
+}

+ 127 - 0
app/ui2/components/content/editor.js

@@ -0,0 +1,127 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+import React from 'react';
+import CodeMirror from 'codemirror';
+// import '../../../node_modules/codemirror/addon/comment/comment';
+import 'codemirror/addon/comment/comment';
+import classnames from 'classnames';
+import modeHost from './cm_hl';
+import m_kw from '../../libs/kw';
+import 'codemirror/lib/codemirror.css';
+import './editor.less'
+
+export default class Editor extends React.Component {
+
+    constructor(props) {
+        super(props);
+
+        this.codemirror = null;
+
+        modeHost();
+
+        this.marks = [];
+        this.kw = '';
+
+        SH_event.on('search', (kw) => {
+            this.kw = kw;
+            this.highlightKeyword();
+        });
+    }
+
+    highlightKeyword() {
+        while (this.marks.length > 0) {
+            this.marks.shift().clear();
+        }
+
+        let code = this.props.code;
+        let pos = m_kw.findPositions(this.kw, code) || [];
+        // this.codemirror.markText({line: 6, ch: 16}, {line: 6, ch: 22}, {className: 'cm-hl'});
+
+        pos.map((p) => {
+            this.marks.push(this.codemirror.markText(p[0], p[1], {className: 'cm-hl'}));
+        });
+    }
+
+    setValue(v) {
+        this.props.setValue(v);
+    }
+
+    toComment() {
+        let doc = this.codemirror.getDoc();
+        let cur = doc.getCursor();
+        let line = cur.line;
+        let info = doc.lineInfo(line);
+        this.codemirror.toggleComment({
+            line: line,
+            cur: 0
+        }, {
+            line: line,
+            cur: info.text.length
+        });
+    }
+
+    componentDidMount() {
+        // console.log(this.cnt_node, this.cnt_node.value);
+        this.codemirror = CodeMirror.fromTextArea(this.cnt_node, {
+            lineNumbers: true,
+            readOnly: true,
+            mode: 'host'
+        });
+
+        this.codemirror.setSize('100%', '100%');
+
+        this.codemirror.on('change', (a) => {
+            let v = a.getDoc().getValue();
+            this.setValue(v);
+        });
+
+        this.codemirror.on('gutterClick', (cm, n) => {
+            if (this.props.readonly === true) return;
+
+            let info = cm.lineInfo(n);
+            //cm.setGutterMarker(n, "breakpoints", info.gutterMarkers ? null : makeMarker());
+            let ln = info.text;
+            if (/^\s*$/.test(ln)) return;
+
+            let new_ln;
+            if (/^#/.test(ln)) {
+                new_ln = ln.replace(/^#\s*/, '');
+            } else {
+                new_ln = '# ' + ln;
+            }
+            this.codemirror.getDoc().replaceRange(new_ln, {line: info.line, ch: 0}, {line: info.line, ch: ln.length});
+            //app.caculateHosts();
+        });
+
+        ipcRenderer.on('to_comment', () => {
+            this.toComment();
+        });
+    }
+
+    componentWillReceiveProps(next_props) {
+        // console.log(next_props);
+        this.codemirror.getDoc().setValue(next_props.code);
+        this.codemirror.setOption('readOnly', next_props.readonly);
+        setTimeout(() => {
+            this.highlightKeyword();
+        }, 100);
+    }
+
+    render() {
+        return (
+            <div id="sh-editor" className={classnames({
+                readonly: this.props.readonly
+            })}>
+                <textarea
+                    ref={(c) => this.cnt_node = c}
+                    defaultValue={this.props.code || ''}
+                />
+            </div>
+        );
+    }
+}

+ 33 - 0
app/ui2/components/content/editor.less

@@ -0,0 +1,33 @@
+#sh-editor {
+  height: 100%;
+  font-family: Menlo, "Source Code Pro", Monaco, "Courier New", sans-serif;
+
+  // CodeMirror
+  .cm-s-default .cm-comment {
+    color: #090;
+  }
+  .cm-s-default .cm-ip {
+    color: #00a;
+    font-weight: bold;
+  }
+  .cm-s-default .cm-hl {
+    background: #ff0;
+  }
+  .CodeMirror-gutters {
+    border-right: none;
+    padding-right: 6px;
+  }
+
+  .CodeMirror-linenumber {
+    cursor: pointer;
+  }
+
+  &.readonly .CodeMirror {
+    .CodeMirror-linenumber {
+      cursor: default;
+    }
+    .CodeMirror-cursors {
+      display: none;
+    }
+  }
+}

+ 295 - 0
app/ui2/components/frame/edit.js

@@ -0,0 +1,295 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict'
+
+import React from 'react'
+import MyFrame from './frame'
+import classnames from 'classnames'
+import Group from './group'
+import util from '../../libs/util'
+import './edit.less'
+
+export default class EditPrompt extends React.Component {
+  constructor (props) {
+    super(props)
+
+    this.state = {
+      show: false,
+      add: true,
+      where: 'local',
+      title: '',
+      url: '',
+      last_refresh: null,
+      refresh_interval: 0,
+      is_loading: false,
+    }
+
+    this.current_host = null
+  }
+
+  tryToFocus () {
+    let el = this.refs.body && this.refs.body.querySelector('input[type=text]')
+    el && el.focus()
+  }
+
+  clear () {
+    this.setState({
+      where: 'local',
+      title: '',
+      url: '',
+      last_refresh: null,
+      refresh_interval: 0,
+    })
+  }
+
+  componentDidMount () {
+    SH_event.on('add_host', () => {
+      this.setState({
+        show: true,
+        add: true,
+      })
+      setTimeout(() => {
+        this.tryToFocus()
+      }, 100)
+    })
+
+    SH_event.on('edit_host', (host) => {
+      this.current_host = host
+      this.setState({
+        show: true,
+        add: false,
+        where: host.where || 'local',
+        title: host.title || '',
+        url: host.url || '',
+        last_refresh: host.last_refresh || null,
+        refresh_interval: host.refresh_interval || 0,
+      })
+      setTimeout(() => {
+        this.tryToFocus()
+      }, 100)
+    })
+
+    SH_event.on('loading_done', (old_host, data) => {
+      if (old_host === this.current_host) {
+        this.setState({
+          last_refresh: data.last_refresh,
+          is_loading: false,
+        })
+        SH_event.emit('host_refreshed', data, this.current_host)
+      }
+    })
+  }
+
+  onOK () {
+    this.setState({
+      title: (this.state.title || '').replace(/^\s+|\s+$/g, ''),
+      url: (this.state.url || '').replace(/^\s+|\s+$/g, ''),
+    })
+
+    if (this.state.title === '') {
+      this.refs.title.focus()
+      return false
+    }
+    if (this.state.where === 'remote' && this.state.url === '') {
+      this.refs.url.focus()
+      return false
+    }
+
+    let data = Object.assign({}, this.current_host, this.state,
+      this.state.add ? {
+        content: `# ${this.state.title}`,
+        on: false,
+      } : {})
+
+    if (!data.id) data.id = util.makeId()
+
+    delete data['add']
+    SH_event.emit('host_' + (this.state.add ? 'add' : 'edit') + 'ed', data,
+      this.current_host)
+
+    this.setState({
+      show: false,
+    })
+    this.clear()
+  }
+
+  onCancel () {
+    this.setState({
+      show: false,
+    })
+    this.clear()
+  }
+
+  confirmDel () {
+    if (!confirm(SH_Agent.lang.confirm_del)) return
+    SH_event.emit('del_host', this.current_host)
+    this.setState({
+      show: false,
+    })
+    this.clear()
+  }
+
+  static getRefreshOptions () {
+    let k = [
+      [0, `${SH_Agent.lang.never}`],
+      [1, `1 ${SH_Agent.lang.hour}`],
+      [24, `24 ${SH_Agent.lang.hours}`],
+      [168, `7 ${SH_Agent.lang.days}`],
+    ]
+    if (IS_DEV) {
+      k.splice(1, 0, [0.002778, `10s (for DEV)`]) // dev test only
+    }
+    return k.map(([v, n], idx) => {
+      return (
+        <option value={v} key={idx}>{n}</option>
+      )
+    })
+  }
+
+  getEditOperations () {
+    if (this.state.add) return null
+
+    return (
+      <div>
+        <div className="ln">
+          <a href="#" className="del"
+             onClick={this.confirmDel.bind(this)}
+          >
+            <i className="iconfont icon-delete"/>
+            <span>{SH_Agent.lang.del_host}</span>
+          </a>
+        </div>
+      </div>
+    )
+  }
+
+  refresh () {
+    if (this.state.is_loading) return
+
+    SH_event.emit('check_host_refresh', this.current_host, true)
+    this.setState({
+      is_loading: true,
+    }, () => {
+      setTimeout(() => {
+        this.setState({
+          is_loading: false,
+        })
+      }, 1000)
+    })
+
+  }
+
+  renderGroup () {
+    if (this.state.where !== 'group') return null
+
+    return <Group hosts={this.props.hosts}/>
+  }
+
+  renderRemoteInputs () {
+    if (this.state.where !== 'remote') return null
+
+    return (
+      <div className="remote-ipts">
+        <div className="ln">
+          <div className="title">{SH_Agent.lang.url}</div>
+          <div className="cnt">
+            <input
+              type="text"
+              ref="url"
+              value={this.state.url}
+              placeholder="http://"
+              onChange={(e) => this.setState({url: e.target.value})}
+              onKeyDown={(e) => (e.keyCode === 13 && this.onOK()) ||
+              (e.keyCode === 27 && this.onCancel())}
+            />
+          </div>
+        </div>
+        <div className="ln">
+          <div className="title">{SH_Agent.lang.auto_refresh}</div>
+          <div className="cnt">
+            <select
+              value={this.state.refresh_interval}
+              onChange={(e) => this.setState(
+                {refresh_interval: parseFloat(e.target.value) || 0})}
+            >
+              {EditPrompt.getRefreshOptions()}
+            </select>
+
+            <i
+              className={classnames({
+                iconfont: 1,
+                'icon-refresh': 1,
+                'invisible': !this.current_host ||
+                this.state.url != this.current_host.url,
+                'loading': this.state.is_loading,
+              })}
+              title={SH_Agent.lang.refresh}
+              onClick={() => this.refresh()}
+            />
+
+            <span className="last-refresh">
+                            {SH_Agent.lang.last_refresh}
+              {this.state.last_refresh || 'N/A'}
+                        </span>
+          </div>
+        </div>
+      </div>
+    )
+  }
+
+  body () {
+    return (
+      <div ref="body">
+        <div className="ln">
+          <input id="ipt-local" type="radio" name="where" value="local"
+                 checked={this.state.where === 'local'}
+                 onChange={(e) => this.setState({where: e.target.value})}
+          />
+          <label htmlFor="ipt-local">{SH_Agent.lang.where_local}</label>
+          <input id="ipt-remote" type="radio" name="where" value="remote"
+                 checked={this.state.where === 'remote'}
+                 onChange={(e) => this.setState({where: e.target.value})}
+          />
+          <label htmlFor="ipt-remote">{SH_Agent.lang.where_remote}</label>
+          <input id="ipt-remote" type="radio" name="where" value="group"
+                 checked={this.state.where === 'group'}
+                 onChange={(e) => this.setState({where: e.target.value})}
+          />
+          <label htmlFor="ipt-remote">{SH_Agent.lang.where_group}</label>
+        </div>
+        <div className="ln">
+          <div className="title">{SH_Agent.lang.host_title}</div>
+          <div className="cnt">
+            <input
+              type="text"
+              ref="title"
+              name="text"
+              value={this.state.title}
+              onChange={(e) => this.setState({title: e.target.value})}
+              onKeyDown={(e) => (e.keyCode === 13 && this.onOK() ||
+              e.keyCode === 27 && this.onCancel())}
+            />
+          </div>
+        </div>
+        {this.renderRemoteInputs()}
+        {this.renderGroup()}
+        {this.getEditOperations()}
+      </div>
+    )
+  }
+
+  render () {
+    return (
+      <MyFrame
+        show={this.state.show}
+        head={SH_Agent.lang[this.state.add ? 'add_host' : 'edit_host']}
+        body={this.body()}
+        onOK={() => this.onOK()}
+        onCancel={() => this.onCancel()}
+      />
+    )
+  }
+}

+ 55 - 0
app/ui2/components/frame/edit.less

@@ -0,0 +1,55 @@
+@keyframes loading {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+.frame {
+  label {
+    padding: 0 4em 0 0.5em;
+  }
+
+  .spiner {
+    transform-origin: right center;
+    animation: spin 1s;
+    animation-iteration-count: 1;
+    -webkit-animation-iteration-count: 1;
+  }
+
+  .ln {
+    i.icon-refresh {
+      display: inline-block;
+      color: #999;
+      margin: 0 0.6em;
+      cursor: pointer;
+
+      &:hover {
+        color: #333;
+      }
+
+      &.loading {
+        animation: loading 1s infinite linear;
+      }
+
+      &.invisible {
+        visibility: hidden;
+      }
+    }
+  }
+
+  .last-refresh {
+    //padding-left: 1em;
+    color: #999;
+  }
+
+  a.del {
+    color: red;
+
+    span {
+      padding-left: 0.5em;
+    }
+  }
+}

+ 73 - 0
app/ui2/components/frame/frame.js

@@ -0,0 +1,73 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+import React from 'react';
+import './frame.less';
+
+export default class MyFrame extends React.Component {
+    constructor(props) {
+        super(props);
+    }
+
+    componentDidMount() {
+        SH_event.on('esc', () => {
+            this.onCancel();
+        });
+    }
+
+    onOK() {
+        this.props.onOK();
+    }
+
+    onCancel() {
+        this.props.onCancel();
+    }
+
+    renderFootButtons() {
+        let html = [];
+
+        html.push(
+            <div
+                className="button btn-cancel"
+                key="btn-cancel"
+                onClick={this.onCancel.bind(this)}
+            >
+                {this.props.cancel_title || SH_Agent.lang.cancel}
+            </div>
+        );
+
+        html.push(
+            <div
+                className="button btn-ok btn-default"
+                key="btn-ok"
+                onClick={this.onOK.bind(this)}
+            >
+                {this.props.ok_title || SH_Agent.lang.ok}
+            </div>
+        );
+
+        return html;
+    }
+
+
+    render() {
+        if (!this.props.show) {
+            return null;
+        }
+
+        return (
+            <div className="frame" ref="frame">
+                <div className="overlay"></div>
+                <div className="prompt">
+                    <div className="head">{this.props.head}</div>
+                    <div className="body">{this.props.body}</div>
+                    <div className="foot">{this.renderFootButtons()}</div>
+                </div>
+            </div>
+        );
+    }
+}

+ 108 - 0
app/ui2/components/frame/frame.less

@@ -0,0 +1,108 @@
+.frame {
+  @bg: #f5f5f5;
+  @btn_default: #05a;
+  @lh: 24px;
+
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+
+  .overlay {
+    position: absolute;
+    z-index: -1;
+    width: 100%;
+    height: 100%;
+    background: #000;
+    opacity: 0.5;
+  }
+
+  .prompt {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    min-width: 300px;
+    max-width: 600px;
+    background: #fff;
+    box-shadow: 0 0 4px 4px rgba(0, 0, 0, 0.1);
+
+    .head {
+      padding: 20px;
+      font-size: 16px;
+      background: @bg;
+      //border-bottom: solid 1px #ccc;
+    }
+
+    .body {
+      padding: 20px 20px;
+
+      .ln {
+        line-height: 30px;
+        padding: 2px 0;
+
+        .title {
+          float: left;
+          width: 100px;
+          line-height: @lh;
+        }
+        .cnt {
+          margin-left: 100px;
+          line-height: @lh;
+          input[type=text] {
+            width: 240px;
+            outline: none;
+            padding: 6px 10px;
+          }
+          textarea {
+            font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
+          }
+        }
+
+        .inform {
+          color: #999;
+          line-height: @lh;
+        }
+      }
+
+      input {
+        padding: 6px 4px;
+      }
+
+      input[type=password] {
+        letter-spacing: 8px;
+        width: 240px;
+        outline: none;
+        padding: 6px 10px;
+      }
+    }
+
+    .foot {
+      padding: 20px;
+      background: @bg;
+      text-align: right;
+
+      .button {
+        display: inline-block;
+        background: #ccc;
+        padding: 8px 20px;
+        margin-left: 1em;
+        cursor: pointer;
+
+        &:hover {
+          background: #ddd;
+        }
+
+        &.btn-default {
+          background: @btn_default;
+          color: #fff;
+
+          &:hover {
+            background: @btn_default * 1.4;
+          }
+        }
+      }
+    }
+  }
+}

+ 95 - 0
app/ui2/components/frame/group.js

@@ -0,0 +1,95 @@
+/**
+ * @author oldj
+ * @blog https://oldj.net
+ */
+
+'use strict'
+
+import React from 'react'
+import classnames from 'classnames'
+import Sortable from 'sortablejs'
+import listToArray from 'wheel-js/src/common/list-to-array'
+import './group.less'
+
+export default class Group extends React.Component {
+  constructor (props) {
+    super(props)
+
+    this.state = {
+      list: this.props.hosts.list
+    }
+
+    this.current_host = null
+  }
+
+  makeItem (item) {
+    let attrs = {
+      'data-id': 'id:' + (item.id || '')
+    }
+    return (
+      <div className="hosts-item" {...attrs}>
+        <i className={classnames({
+          'iconfont': 1
+          , 'item-icon': 1
+          , 'icon-file': item.where !== 'group'
+          , 'icon-files': item.where === 'group'
+        })}
+        />
+        <span>{item.title}</span>
+      </div>
+    )
+  }
+
+  makeList () {
+    let items = this.state.list
+      .filter(item => item.where !== 'group')
+      .map(item => this.makeItem(item))
+
+    return (
+      <div id="hosts-group-valid">
+        <div ref="group_valid" className="hosts-group-list">
+          {items}
+        </div>
+      </div>
+    )
+  }
+
+  currentList () {
+    return (
+      <div id="hosts-group-current">
+        <div ref="group_current" className="hosts-group-list"></div>
+      </div>
+    )
+  }
+
+  getCurrentListFromDOM () {
+    let nodes = this.refs.group_current.getElementsByClassName('hosts-item')
+    nodes = listToArray(nodes)
+    console.log(nodes)
+  }
+
+  componentDidMount () {
+    Sortable.create(this.refs.group_valid, {
+      group: 'sorting'
+      , sort: false
+    })
+
+    Sortable.create(this.refs.group_current, {
+      group: 'sorting'
+      , sort: true
+      , onSort: evt => {
+        this.getCurrentListFromDOM()
+      }
+    })
+  }
+
+  render () {
+    return (
+      <div id="hosts-group">
+        {this.makeList()}
+        <div className="arrow"/>
+        {this.currentList()}
+      </div>
+    )
+  }
+}

+ 62 - 0
app/ui2/components/frame/group.less

@@ -0,0 +1,62 @@
+#hosts-group {
+  @h: 160px;
+
+  position: relative;
+  margin-top: 20px;
+  height: @h + 2px;
+  overflow: hidden;
+
+  &::after {
+    content: '';
+    clear: both;
+  }
+
+  .arrow {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    width: 20px;
+    height: 20px;
+    line-height: 20px;
+    text-align: center;
+    transform: translate(-50%, -50%);
+
+    &::after {
+      content: '→';
+    }
+  }
+
+  .hosts-group-list {
+    height: @h;
+    overflow: auto;
+    border: solid 1px #ccc;
+
+    .hosts-item {
+      cursor: move;
+      padding: 2px 6px;
+
+      &:hover {
+        background: #f5f5f5;
+      }
+
+      i {
+        font-size: 12px;
+        color: #999;
+      }
+
+      & > span {
+        padding-left: 6px;
+      }
+    }
+  }
+}
+
+#hosts-group-valid {
+  width: 45%;
+  float: left;
+}
+
+#hosts-group-current {
+  width: 45%;
+  float: right;
+}

+ 239 - 0
app/ui2/components/frame/preferences.js

@@ -0,0 +1,239 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+import React from 'react';
+import Frame from './frame';
+import classnames from 'classnames';
+import './preferences.less';
+import lang from '../../lang';
+import util from '../../libs/util';
+const current_version = require('../../../version').version;
+
+const AUTO_LAUNCH = 'auto_launch';
+
+export default class PreferencesPrompt extends React.Component {
+    constructor(props) {
+        super(props);
+
+        let choice_mode = SH_Agent.pref.get('choice_mode');
+        if (!choice_mode || (choice_mode != 'multiple' && choice_mode != 'single')) {
+            choice_mode = 'multiple';
+        }
+
+        this.state = {
+            show: false,
+            lang_key: SH_Agent.lang_key,
+            after_cmd: SH_Agent.pref.get('after_cmd') || '',
+            choice_mode: choice_mode,
+            auto_launch: !!SH_Agent.pref.get(AUTO_LAUNCH),
+            hide_at_launch: !!SH_Agent.pref.get('hide_at_launch'),
+            update_found: false // 发现新版本
+        };
+
+    }
+
+    componentDidMount() {
+        SH_event.on('show_preferences', () => {
+            this.setState({
+                show: true
+            });
+        });
+        ipcRenderer.on('show_preferences', () => {
+            this.setState({
+                show: true
+            });
+        });
+
+        ipcRenderer.on('update_found', (v) => {
+            console.log(v);
+            this.setState({
+                update_found: true
+            });
+        });
+    }
+
+    onOK() {
+        this.setState({
+            show: false
+        }, () => {
+            setTimeout(() => {
+                SH_Agent.relaunch();
+            }, 200);
+        });
+    }
+
+    onCancel() {
+        this.setState({
+            show: false
+        });
+    }
+
+    static getLanguageOptions() {
+        return lang.lang_list.map(({key, name}, idx) => {
+            return (
+                <option value={key} key={idx}>{name}</option>
+            );
+        });
+    }
+
+    updateLangKey(v) {
+        SH_Agent.lang_key = v;
+        SH_Agent.pref.set('user_language', v);
+        this.setState({
+            lange_key: v
+        });
+    }
+
+    updateChoiceMode(v) {
+        SH_Agent.pref.set('choice_mode', v);
+        this.setState({
+            choice_mode: v
+        });
+    }
+
+    updateAfterCmd(v) {
+        SH_Agent.pref.set('after_cmd', v);
+        this.setState({
+            after_cmd: v
+        });
+    }
+
+    updateAutoLaunch(v) {
+        SH_Agent.pref.set(AUTO_LAUNCH, v);
+        this.setState({
+            auto_launch: v
+        });
+
+        // todo set auto launch
+    }
+
+    updateMinimizeAtLaunch(v) {
+        SH_Agent.pref.set('hide_at_launch', v);
+        this.setState({
+            hide_at_launch: v
+        });
+    }
+
+    prefLanguage() {
+        return (
+            <div className="ln">
+                <div className="title">{SH_Agent.lang.language}</div>
+                <div className="cnt">
+                    <select
+                        value={SH_Agent.lang_key}
+                        onChange={(e) => this.updateLangKey(e.target.value)}
+                    >
+                        {PreferencesPrompt.getLanguageOptions()}
+                    </select>
+
+                    <div className="inform">{SH_Agent.lang.should_restart_after_change_language}</div>
+                </div>
+            </div>
+        )
+    }
+
+    prefChoiceMode() {
+        return (
+            <div className="ln">
+                <div className="title">{SH_Agent.lang.pref_choice_mode}</div>
+                <div className="cnt">
+                    <input type="radio" id="pref-choice-mode-single" name="choice_mode" value="single"
+                           defaultChecked={this.state.choice_mode === 'single'}
+                           onChange={(e) => this.updateChoiceMode(e.target.value)}
+                    />
+                    <label htmlFor="pref-choice-mode-single">{SH_Agent.lang.pref_choice_mode_single}</label>
+                    <input type="radio" id="pref-choice-mode-multiple" name="choice_mode" value="multiple"
+                           defaultChecked={this.state.choice_mode === 'multiple'}
+                           onChange={(e) => this.updateChoiceMode(e.target.value)}
+                    />
+                    <label htmlFor="pref-choice-mode-multiple">{SH_Agent.lang.pref_choice_mode_multiple}</label>
+                </div>
+            </div>
+        )
+    }
+
+    prefAfterCmd() {
+        return (
+            <div className="ln">
+                <div className="title">{SH_Agent.lang.pref_after_cmd}</div>
+                <div className="cnt">
+                    <div className="inform">{SH_Agent.lang.pref_after_cmd_info}</div>
+                    <textarea
+                        name=""
+                        defaultValue={this.state.after_cmd}
+                        placeholder={SH_Agent.lang.pref_after_cmd_placeholder}
+                        onChange={(e) => this.updateAfterCmd(e.target.value)}
+                    />
+                </div>
+            </div>
+        )
+    }
+
+    prefAutoLaunch() {
+        return (
+            <div className="ln">
+                <div className="title">{SH_Agent.lang.auto_launch}</div>
+                <div className="cnt">
+                    <input type="checkbox" name=""
+                           defaultChecked={this.state.auto_launch}
+                           onChange={(e) => this.updateAutoLaunch(e.target.checked)}
+                    />
+                </div>
+            </div>
+        )
+    }
+
+    prefMinimizeAtLaunch() {
+        return (
+            <div className="ln">
+                <div className="title">{SH_Agent.lang.hide_at_launch}</div>
+                <div className="cnt">
+                    <input type="checkbox" name=""
+                           defaultChecked={this.state.hide_at_launch}
+                           onChange={(e) => this.updateMinimizeAtLaunch(e.target.checked)}
+                    />
+                </div>
+            </div>
+        )
+    }
+
+    openDownloadPage() {
+        ipcRenderer.send('open_url', require('../../configs').url_download);
+    }
+
+    body() {
+        return (
+            <div ref="body">
+                {/*<div className="title">{SH_Agent.lang.host_title}</div>*/}
+                {/*<div className="cnt">*/}
+                {/*</div>*/}
+                <div className={classnames("current-version", {"update-found": this.state.update_found})}>
+                    <a href="#" onClick={this.openDownloadPage}>{util.formatVersion(current_version)}</a>
+                </div>
+                {this.prefLanguage()}
+                {this.prefChoiceMode()}
+                {this.prefAfterCmd()}
+                {/*{this.prefAutoLaunch()}*/}
+                {this.prefMinimizeAtLaunch()}
+            </div>
+        )
+    }
+
+    render() {
+        return (
+            <Frame
+                show={this.state.show}
+                head={SH_Agent.lang.preferences}
+                body={this.body()}
+                onOK={() => this.onOK()}
+                onCancel={() => this.onCancel()}
+                cancel_title={SH_Agent.lang.set_and_back}
+                ok_title={SH_Agent.lang.set_and_relaunch_app}
+            />
+        );
+    }
+}

+ 35 - 0
app/ui2/components/frame/preferences.less

@@ -0,0 +1,35 @@
+.frame {
+  textarea {
+    width: 300px;
+    height: 80px;
+    padding: 2px 4px;
+    outline: none;
+    border: solid 1px #ccc;
+  }
+
+  .current-version {
+    float: right;
+    margin-top: -60px;
+    color: #999;
+
+    a {
+      color: #999;
+
+      &:hover {
+        color: #000;
+      }
+    }
+
+    &.update-found {
+      &:after {
+        content: '';
+        display: block;
+        float: right;
+        width: 6px;
+        height: 6px;
+        background: #f00;
+        border-radius: 3px;
+      }
+    }
+  }
+}

+ 85 - 0
app/ui2/components/frame/sudo.js

@@ -0,0 +1,85 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+import React from 'react';
+import Frame from './frame';
+import './sudo.less';
+
+export default class SudoPrompt extends React.Component {
+    constructor(props) {
+        super(props);
+
+        this.onSuccess = null;
+        this.state = {
+            show: false,
+            pswd: ''
+        }
+    }
+
+    componentDidMount() {
+        SH_event.on('sudo_prompt', (success) => {
+            this.setState({show: true});
+            this.onSuccess = success;
+            setTimeout(() => {
+                let el = this.refs.body;
+                el && el.querySelector('input').focus();
+            }, 100);
+        });
+    }
+
+    onOK() {
+        let pswd = this.refs.pswd.value;
+        if (!pswd) return;
+
+        this.setState({
+            show: false,
+            pswd: pswd
+        });
+
+        SH_event.emit('sudo_pswd', pswd);
+        if (typeof this.onSuccess === 'function') {
+            this.onSuccess(pswd);
+        }
+        this.onSuccess = null;
+    }
+
+    onCancel() {
+        this.setState({
+            show: false
+        });
+        this.onSuccess = null;
+    }
+
+    body() {
+        return (
+            <div ref="body">
+                <div className="ln">
+                    <div className="title">{SH_Agent.lang.sudo_pswd}</div>
+                    <div className="cnt">
+                        <input
+                            type="password"
+                            ref="pswd"
+                            onKeyDown={(e)=>(e.keyCode === 13 && this.onOK()||e.keyCode===27 && this.onCancel())}
+                        />
+                    </div>
+                </div>
+            </div>
+        )
+    }
+
+    render() {
+        return (
+            <Frame
+                show={this.state.show}
+                head={SH_Agent.lang.input_sudo_pswd}
+                body={this.body()}
+                onOK={() => this.onOK()}
+                onCancel={() => this.onCancel()}
+            />
+        );
+    }
+}

+ 7 - 0
app/ui2/components/frame/sudo.less

@@ -0,0 +1,7 @@
+@import "frame";
+
+.frame {
+  .prompt {
+    width: 480px;
+  }
+}

+ 118 - 0
app/ui2/components/panel/buttons.js

@@ -0,0 +1,118 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+import React from 'react';
+import classnames from 'classnames';
+import './buttons.less';
+
+export default class Buttons extends React.Component {
+
+    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;
+            }
+        });
+    }
+
+    btnSearch() {
+        this.setState({
+            search_on: !this.state.search_on
+        }, () => {
+            SH_event.emit(this.state.search_on ? 'search_on' : 'search_off');
+        });
+    }
+
+    calcelSearch() {
+        this.setState({
+            search_on: false
+        }, () => {
+            SH_event.emit('search_off');
+        });
+    }
+
+    componentDidMount() {
+        ipcRenderer.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>
+        );
+    }
+}

+ 53 - 0
app/ui2/components/panel/buttons.less

@@ -0,0 +1,53 @@
+@import '../cfg.less';
+
+#sh-buttons {
+  @bg_left: #373d47;
+
+  position: absolute;
+  bottom: 0;
+  width: 240px;
+  height: 30px;
+  line-height: 30px;
+  background: @bg_left;
+
+  .left {
+    float: left;
+    padding-left: 10px;
+
+    .btn-add {
+      display: inline-block;
+      text-align: center;
+      width: 20px;
+      color: @font_color_left;
+      text-decoration: none;
+
+      &:hover {
+        background: rgba(255, 255, 255, 0.1);
+      }
+    }
+  }
+
+  .right {
+    float: right;
+    padding-right: 17px;
+    height: 30px;
+
+    i {
+      display: inline-block;
+      margin-left: 10px;
+      cursor: pointer;
+
+      &.icon-switchoff {
+        position: relative;
+        top: -1px;
+      }
+
+      &.icon-search {
+        padding: 0 8px;
+        &.on {
+          background: @bg_left_search;
+        }
+      }
+    }
+  }
+}

+ 83 - 0
app/ui2/components/panel/iconfont/iconfont.css

@@ -0,0 +1,83 @@
+
+@font-face {font-family: "iconfont";
+  src: url('iconfont.eot?t=1490237956135'); /* IE9*/
+  src: url('iconfont.eot?t=1490237956135#iefix') format('embedded-opentype'), /* IE6-IE8 */
+  url('iconfont.woff?t=1490237956135') format('woff'), /* chrome, firefox */
+  url('iconfont.ttf?t=1490237956135') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
+  url('iconfont.svg?t=1490237956135#iconfont') format('svg'); /* iOS 4.1- */
+}
+
+.iconfont {
+  font-family:"iconfont" !important;
+  font-size:16px;
+  font-style:normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-switchoff:before { content: "\e60d"; }
+
+.icon-icon:before { content: "\e600"; }
+
+.icon-warnfill:before { content: "\e607"; }
+
+.icon-warn:before { content: "\e608"; }
+
+.icon-refresh:before { content: "\e616"; }
+
+.icon-ok:before { content: "\e604"; }
+
+.icon-h:before { content: "\e617"; }
+
+.icon-lock:before { content: "\e61d"; }
+
+.icon-off:before { content: "\e613"; }
+
+.icon-on:before { content: "\e614"; }
+
+.icon-search:before { content: "\e61c"; }
+
+.icon-edit:before { content: "\e609"; }
+
+.icon-info:before { content: "\e601"; }
+
+.icon-add-s:before { content: "\e612"; }
+
+.icon-more:before { content: "\e602"; }
+
+.icon-grid:before { content: "\e603"; }
+
+.icon-movedown:before { content: "\e60a"; }
+
+.icon-moveup:before { content: "\e60b"; }
+
+.icon-add:before { content: "\e60c"; }
+
+.icon-folder:before { content: "\e618"; }
+
+.icon-group:before { content: "\e619"; }
+
+.icon-lock2:before { content: "\e61e"; }
+
+.icon-files:before { content: "\e61f"; }
+
+.icon-timescircle:before { content: "\e60e"; }
+
+.icon-earth:before { content: "\e61a"; }
+
+.icon-move:before { content: "\e60f"; }
+
+.icon-delete:before { content: "\e610"; }
+
+.icon-doc:before { content: "\e606"; }
+
+.icon-line:before { content: "\e611"; }
+
+.icon-file-box:before { content: "\e61b"; }
+
+.icon-switchon:before { content: "\e615"; }
+
+.icon-sysserver:before { content: "\e605"; }
+
+.icon-file:before { content: "\e77d"; }
+

BIN
app/ui2/components/panel/iconfont/iconfont.eot


File diff suppressed because it is too large
+ 102 - 0
app/ui2/components/panel/iconfont/iconfont.js


+ 150 - 0
app/ui2/components/panel/iconfont/iconfont.svg

@@ -0,0 +1,150 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>
+Created by FontForge 20120731 at Thu Mar 23 10:59:16 2017
+ By admin
+</metadata>
+<defs>
+<font id="iconfont" horiz-adv-x="1024" >
+  <font-face 
+    font-family="iconfont"
+    font-weight="500"
+    font-stretch="normal"
+    units-per-em="1024"
+    panose-1="2 0 6 3 0 0 0 0 0 0"
+    ascent="896"
+    descent="-128"
+    x-height="792"
+    bbox="0 -212 1024 896"
+    underline-thickness="0"
+    underline-position="0"
+    unicode-range="U+0078-E77D"
+  />
+<missing-glyph 
+ />
+    <glyph glyph-name=".notdef" 
+ />
+    <glyph glyph-name=".notdef" 
+ />
+    <glyph glyph-name=".null" horiz-adv-x="0" 
+ />
+    <glyph glyph-name="nonmarkingreturn" horiz-adv-x="341" 
+ />
+    <glyph glyph-name="x" unicode="x" horiz-adv-x="1001" 
+d="M281 543q-27 -1 -53 -1h-83q-18 0 -36.5 -6t-32.5 -18.5t-23 -32t-9 -45.5v-76h912v41q0 16 -0.5 30t-0.5 18q0 13 -5 29t-17 29.5t-31.5 22.5t-49.5 9h-133v-97h-438v97zM955 310v-52q0 -23 0.5 -52t0.5 -58t-10.5 -47.5t-26 -30t-33 -16t-31.5 -4.5q-14 -1 -29.5 -0.5
+t-29.5 0.5h-32l-45 128h-439l-44 -128h-29h-34q-20 0 -45 1q-25 0 -41 9.5t-25.5 23t-13.5 29.5t-4 30v167h911zM163 247q-12 0 -21 -8.5t-9 -21.5t9 -21.5t21 -8.5q13 0 22 8.5t9 21.5t-9 21.5t-22 8.5zM316 123q-8 -26 -14 -48q-5 -19 -10.5 -37t-7.5 -25t-3 -15t1 -14.5
+t9.5 -10.5t21.5 -4h37h67h81h80h64h36q23 0 34 12t2 38q-5 13 -9.5 30.5t-9.5 34.5q-5 19 -11 39h-368zM336 498v228q0 11 2.5 23t10 21.5t20.5 15.5t34 6h188q31 0 51.5 -14.5t20.5 -52.5v-227h-327z" />
+    <glyph glyph-name="switchoff" unicode="&#xe60d;" 
+d="M832 812h-640q-80 0 -136 -56t-56 -136v-640q0 -79 56 -135.5t136 -56.5h640q79 0 135.5 56.5t56.5 135.5v640q0 80 -56.5 136t-135.5 56zM160 -84q-13 0 -22.5 9.5t-9.5 22.5t9.5 22.5t22.5 9.5t22.5 -9.5t9.5 -22.5t-9.5 -22.5t-22.5 -9.5zM160 620q-13 0 -22.5 9.5
+t-9.5 22.5t9.5 22.5t22.5 9.5t22.5 -9.5t9.5 -22.5t-9.5 -22.5t-22.5 -9.5zM704 -20q0 -27 -18.5 -45.5t-45.5 -18.5h-256q-27 0 -45.5 18.5t-18.5 45.5v640q0 26 19 45t45 19h256q27 0 45.5 -18.5t18.5 -45.5v-640zM864 -84q-13 0 -22.5 9.5t-9.5 22.5t9.5 22.5t22.5 9.5
+t22.5 -9.5t9.5 -22.5t-9.5 -22.5t-22.5 -9.5zM864 620q-13 0 -22.5 9.5t-9.5 22.5t9.5 22.5t22.5 9.5t22.5 -9.5t9.5 -22.5t-9.5 -22.5t-22.5 -9.5zM576 300h-128q-26 0 -45 -19t-19 -45v-192q0 -27 18.5 -45.5t45.5 -18.5h128q27 0 45.5 18.5t18.5 45.5v192
+q0 27 -18.5 45.5t-45.5 18.5z" />
+    <glyph glyph-name="icon" unicode="&#xe600;" 
+d="M488 745l132 -269l297 -43l-215 -209l51 -296l-265 140l-266 -140l51 296l-215 209l297 43z" />
+    <glyph glyph-name="warnfill" unicode="&#xe607;" 
+d="M943 127l-341 609q-35 63 -90.5 63t-90.5 -63l-340 -609q-34 -62 -6 -111q29 -48 100 -48h674q71 0 99.5 48.5t-5.5 110.5zM480 576q0 13 9.5 22.5t22.5 9.5t22.5 -9.5t9.5 -22.5v-288q0 -13 -9.5 -22.5t-22.5 -9.5t-22.5 9.5t-9.5 22.5v288zM512 64q-20 0 -34 14t-14 34
+t14 34t34 14t34 -14t14 -34t-14 -34t-34 -14z" />
+    <glyph glyph-name="warn" unicode="&#xe608;" 
+d="M849 -33h-674q-71 0 -99.5 49t5.5 111l340 609q35 63 90.5 63t90.5 -63l341 -609q34 -63 5.5 -111.5t-99.5 -48.5zM512 735q-18 0 -35 -30l-340 -610q-17 -30 -7 -47t45 -17h674q35 0 45 17t-7 47l-341 610q-16 30 -34 30zM512 256q-13 0 -22.5 9.5t-9.5 22.5v288
+q0 13 9.5 22.5t22.5 9.5t22.5 -9.5t9.5 -22.5v-288q0 -13 -9.5 -22.5t-22.5 -9.5zM512 144zM464 144q0 20 14 34t34 14t34 -14t14 -34t-14 -34t-34 -14t-34 14t-14 34z" />
+    <glyph glyph-name="refresh" unicode="&#xe616;" 
+d="M504 17q-97 0 -179.5 48t-130.5 130.5t-48 179.5q0 110 62 201l93 -93v245h-246l92 -92q-85 -116 -85 -261q0 -90 35 -171.5t94.5 -141t141 -94.5t171.5 -35q85 0 163 31l-50 71q-55 -18 -113 -18v0zM720 277v-246h246l-96 96q76 112 76 248q0 90 -35 172t-94 141
+t-141 94t-172 35q-70 0 -136 -21l52 -73q41 10 84 10q98 0 180.5 -47.5t130 -130t47.5 -180.5q0 -101 -52 -187z" />
+    <glyph glyph-name="ok" unicode="&#xe604;" 
+d="M512 709q-111 0 -205.5 -54.5t-149 -149t-54.5 -205.5t54.5 -205.5t149 -149t205.5 -54.5t205.5 54.5t149 149t54.5 205.5t-54.5 205.5t-149 149t-205.5 54.5zM376 253q0 -1 5 -6l9 -9t10 -7.5t10 -3.5q16 0 109 104l138 158q7 8 15 9q13 0 21 -7l45 -44q7 -8 7.5 -18.5
+t-6.5 -18.5l-250 -264v0l-53 -57q-7 -7 -18 -7.5t-18 6.5l-151 142q-8 7 -8.5 18t7.5 19l35 37q7 8 18 8.5t19 -7.5z" />
+    <glyph glyph-name="h" unicode="&#xe617;" 
+d="M512 802q-104 0 -192.5 -51.5t-140 -140t-51.5 -193t51.5 -193t140 -140t192.5 -51.5t192.5 51.5t140 140t51.5 193t-51.5 193t-140 140t-192.5 51.5zM660 214h-50v175h-196v-175h-50v395h50v-179h196v179h50v-395z" />
+    <glyph glyph-name="lock" unicode="&#xe61d;" 
+d="M427 151q0 35 25 60t60 25t60 -25t25 -60q0 -47 -39 -72q9 -50 18 -110q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5l18 110q-39 25 -39 72zM341 385v150q0 70 50 120t121 50t121 -50t50 -120v-150h-342zM235 535v-150h-64q-36 0 -61 -25t-25 -60v-427
+q0 -35 25 -60t61 -25h682q36 0 61 25t25 60v427q0 35 -25 60t-61 25h-64v150q0 115 -81 196t-196 81t-196 -81t-81 -196v0z" />
+    <glyph glyph-name="off" unicode="&#xe613;" 
+d="M729 90h-434q-116 0 -197.5 81.5t-81.5 196.5v32q0 115 81.5 196.5t197.5 81.5h434q116 0 197.5 -81.5t81.5 -196.5v-32q0 -115 -81.5 -196.5t-197.5 -81.5zM946 400q0 90 -63.5 153t-153.5 63h-434q-90 0 -153.5 -63t-63.5 -153v-32q0 -90 63.5 -153.5t153.5 -63.5h434
+q90 0 153.5 63.5t63.5 153.5v32zM322 223q-68 0 -116 48.5t-48 116.5t48 116t116 48t116 -48t48 -116t-48 -116.5t-116 -48.5z" />
+    <glyph glyph-name="on" unicode="&#xe614;" 
+d="M729 90h-434q-116 0 -197.5 81.5t-81.5 196.5v32q0 115 81.5 196.5t197.5 81.5h434q116 0 197.5 -81.5t81.5 -196.5v-32q0 -115 -81.5 -196.5t-197.5 -81.5zM947 399q0 90 -63.5 153.5t-153.5 63.5h-436q-89 0 -152.5 -63.5t-63.5 -153.5v-31q0 -90 63.5 -153.5
+t152.5 -63.5h436q90 0 153.5 63.5t63.5 153.5v31v0zM702 223q-68 0 -116 48.5t-48 116.5t48 116t116 48t116.5 -48t48.5 -116t-48.5 -116.5t-116.5 -48.5z" />
+    <glyph glyph-name="search" unicode="&#xe61c;" 
+d="M995 3l-272 227q-2 2 -6 4q37 79 37 160q0 82 -34.5 158t-99.5 131q-105 89 -243 89q-82 0 -158 -35t-131 -100q-88 -105 -88 -243q0 -82 34.5 -158.5t100.5 -131.5q105 -88 241 -88h1q121 0 221 72q1 0 2 -1.5l1 -1.5l272 -227q27 -23 63 -18.5t59 33.5l6 7
+q24 28 22 64.5t-28 58.5zM585 219q-40 -47 -94.5 -72t-113.5 -25q-98 1 -174 64q-47 39 -72 94t-25 114q0 99 64 174q39 48 93.5 72.5t113.5 24.5q99 0 174 -63q47 -40 72 -94.5t25 -113.5q0 -99 -63 -175z" />
+    <glyph glyph-name="edit" unicode="&#xe609;" 
+d="M384 3l-169 169q-6 5 -6 13.5t6 14.5l380 380q6 6 14 6t14 -6l169 -169q6 -6 6 -14t-6 -14l-380 -380q-6 -6 -14.5 -6t-13.5 6zM693 678q18 18 42.5 18t42.5 -18l112 -112q18 -18 18 -42.5t-18 -42.5l-56 -56l-197 197zM116 -96l56 254l198 -198z" />
+    <glyph glyph-name="info" unicode="&#xe601;" 
+d="M512 -20q-110 0 -203 54t-147 147t-54 203t54 203t147 147t203 54t203 -54t147 -147t54 -203t-54 -203t-147 -147t-203 -54zM567 605q0 7 -5.5 12.5t-12.5 5.5h-74q-7 0 -12.5 -5.5t-5.5 -12.5v-74q0 -8 5.5 -13t12.5 -5h74q7 0 12.5 5t5.5 13v74zM567 402q0 8 -5.5 13.5
+t-12.5 5.5h-74q-7 0 -12.5 -5.5t-5.5 -13.5v-239q0 -7 5.5 -12.5t12.5 -5.5h74q7 0 12.5 5.5t5.5 12.5v239z" />
+    <glyph glyph-name="add-s" unicode="&#xe612;" 
+d="M699 358h-157v-158q0 -10 -7.5 -18t-18.5 -8t-19 8t-8 18v158h-157q-11 0 -18.5 7.5t-7.5 18.5t7.5 18.5t18.5 7.5h157v157q0 11 8 18.5t19 7.5t18.5 -7.5t7.5 -18.5v-157h157q11 0 18.5 -7.5t7.5 -18.5t-7.5 -18.5t-18.5 -7.5v0z" />
+    <glyph glyph-name="more" unicode="&#xe602;" 
+d="M174.5 501q-41.5 0 -71 -29.5t-29.5 -71.5t29.5 -71.5t71 -29.5t71.5 29.5t30 71.5t-30 71.5t-71.5 29.5zM511 501q-42 0 -71.5 -29.5t-29.5 -71.5t29.5 -71.5t71.5 -29.5t71.5 29.5t29.5 71.5t-29.5 71.5t-71.5 29.5zM848 501q-42 0 -71.5 -29.5t-29.5 -71.5t29.5 -71.5
+t71.5 -29.5t71.5 29.5t29.5 71.5t-29.5 71.5t-71.5 29.5z" />
+    <glyph glyph-name="grid" unicode="&#xe603;" 
+d="M601 830h-179q-12 0 -21 -9t-9 -21v-179q0 -12 9 -21t21 -9h179q12 0 21 9t9 21v179q0 12 -9 21t-21 9zM929 830h-179q-12 0 -21 -9t-9 -21v-179q0 -12 9 -21t21 -9h179q12 0 21 9t9 21v179q0 12 -9 21t-21 9zM273 830h-179q-12 0 -21 -9t-9 -21v-179q0 -12 9 -21t21 -9
+h179q12 0 21 9t9 21v179q0 12 -9 21t-21 9zM601 502h-179q-12 0 -21 -9t-9 -21v-179q0 -13 9 -21.5t21 -8.5h179q12 0 21 8.5t9 21.5v179q0 12 -9 21t-21 9zM929 502h-179q-12 0 -21 -9t-9 -21v-179q0 -13 9 -21.5t21 -8.5h179q12 0 21 8.5t9 21.5v179q0 12 -9 21t-21 9z
+M273 502h-179q-12 0 -21 -9t-9 -21v-179q0 -13 9 -21.5t21 -8.5h179q12 0 21 8.5t9 21.5v179q0 12 -9 21t-21 9zM601 174h-179q-12 0 -21 -9t-9 -21v-179q0 -13 9 -21.5t21 -8.5h179q12 0 21 8.5t9 21.5v179q0 12 -9 21t-21 9zM929 174h-179q-12 0 -21 -9t-9 -21v-179
+q0 -13 9 -21.5t21 -8.5h179q12 0 21 8.5t9 21.5v179q0 12 -9 21t-21 9zM273 174h-179q-12 0 -21 -9t-9 -21v-179q0 -13 9 -21.5t21 -8.5h179q12 0 21 8.5t9 21.5v179q0 12 -9 21t-21 9z" />
+    <glyph glyph-name="movedown" unicode="&#xe60a;" 
+d="M768 108v384h-64v-384h-160l192 -192l192 192h-160zM320 556v-192h-192v192h192zM384 620h-320v-320h320v320zM64 172h96v-64h-96v64zM192 172h96v-64h-96v64zM320 172h64v-96h-64v96zM64 -52h64v-96h-64v96zM160 -84h96v-64h-96v64zM288 -84h96v-64h-96v64zM64 76h64
+v-96h-64v96zM320 44h64v-96h-64v96z" />
+    <glyph glyph-name="moveup" unicode="&#xe60b;" 
+d="M704 300v-384h64v384h160l-192 192l-192 -192h160zM64 620h96v-64h-96v64zM192 620h96v-64h-96v64zM320 620h64v-96h-64v96zM64 396h64v-96h-64v96zM160 364h96v-64h-96v64zM288 364h96v-64h-96v64zM64 524h64v-96h-64v96zM320 492h64v-96h-64v96zM320 108v-192h-192v192
+h192zM384 172h-320v-320h320v320z" />
+    <glyph glyph-name="add" unicode="&#xe60c;" 
+d="M512 -41q-115 0 -213 57t-155 155t-57 213t57 213t155 155t213 57t213 -57t155 -155t57 -213t-57 -213t-155 -155t-213 -57zM724 426h-170v170h-84v-170h-170v-84h170v-170h84v170h170v84z" />
+    <glyph glyph-name="folder" unicode="&#xe618;" 
+d="M961 640q0 40 -28.5 68t-67.5 28h-186h-1h-1q-6 -1 -10 -2l-1 -1h-1q-5 -3 -8 -6q-18 -17 -35 -88q-3 -12 -4 -14q-4 -17 -27 -17h-7.5h-7.5h-416q-40 0 -68 -28.5t-28 -69.5l32 -414q0 -40 28 -68t68 -28h640q40 0 68 28t28 66zM864 96q0 -13 -9.5 -22.5t-22.5 -9.5
+h-640q-13 0 -22.5 10t-9.5 24l-32 414q0 13 9.5 22.5t22.5 9.5h416h7.5h7.5q35 0 58.5 17t30.5 48q2 4 4 15q8 34 13 48h168q13 0 22.5 -9.5t9.5 -22.5v-2v-1zM128 672h416q13 0 22.5 9.5t9.5 22.5t-9.5 22.5t-22.5 9.5h-416q-13 0 -22.5 -9.5t-9.5 -22.5t9.5 -22.5
+t22.5 -9.5z" />
+    <glyph glyph-name="group" unicode="&#xe619;" 
+d="M521 194l-376 247l-50 -52l426 -291l426 291l-51 53zM947 550l-426 291l-426 -291l426 -291zM521 756l302 -206l-302 -207l-302 207zM521 33l-376 248l-50 -52l426 -291l426 291l-51 53z" />
+    <glyph glyph-name="lock2" unicode="&#xe61e;" 
+d="M769 448v135v0q-3 104 -77.5 176.5t-178.5 72.5t-178.5 -72.5t-77.5 -175.5v0v-136h-1q-27 0 -45.5 -18.5t-18.5 -45.5v-384q0 -27 18.5 -45.5t45.5 -18.5h512q27 0 45.5 18.5t18.5 45.5v384q0 26 -18.5 45t-44.5 19zM321 581v0q2 78 58 132.5t134 54.5t134 -54.5
+t58 -132.5v0v-133h-384v133zM768 32q0 -13 -9.5 -22.5t-22.5 -9.5h-448q-13 0 -22.5 9.5t-9.5 22.5v320q0 13 9.5 22.5t22.5 9.5h448q13 0 22.5 -9.5t9.5 -22.5v-320zM544 206v52q0 13 -9.5 22.5t-22.5 9.5t-22.5 -9.5t-9.5 -22.5v-52q-28 -18 -28 -51q0 -25 17.5 -42.5
+t42.5 -17.5t42.5 17.5t17.5 42.5q0 33 -28 51z" />
+    <glyph glyph-name="files" unicode="&#xe61f;" 
+d="M744 434h-260q-12 0 -20.5 -8.5t-8.5 -20.5t8.5 -20.5t20.5 -8.5h260q12 0 20.5 8.5t8.5 20.5t-8.5 20.5t-20.5 8.5zM744 290h-260q-12 0 -20.5 -8.5t-8.5 -20.5t8.5 -20.5t20.5 -8.5h260q12 0 20.5 8.5t8.5 20.5t-8.5 20.5t-20.5 8.5zM744 839h-347q-45 0 -80 -35.5
+t-35 -80.5h-34q-45 0 -78 -35t-33 -80v-578q0 -45 35 -80.5t81 -35.5h433q46 0 81 35.5t35 80.5h29q46 0 81 35.5t35 79.5v405v57zM686 -28h-433q-23 0 -40.5 18t-17.5 40v578q0 22 16.5 39.5t38.5 17.5l32 1v-521q0 -44 35 -79.5t80 -35.5h347q0 -22 -17.5 -40t-40.5 -18v0
+zM889 145q0 -22 -17.5 -39.5t-40.5 -17.5h-434q-22 0 -39.5 17.5t-17.5 39.5v578q0 23 17.5 40.5t39.5 17.5h289v-116q0 -45 35 -80t81 -35h87v-405zM802 608q-21 0 -39.5 30.5t-18.5 55.5v86v0l145 -172h-87z" />
+    <glyph glyph-name="timescircle" unicode="&#xe60e;" 
+d="M657 171q0 15 -11 26l-104 103l104 103q11 11 11 26q0 16 -11 26l-52 52q-11 11 -26 11t-26 -11l-103 -104l-104 104q-10 11 -25 11q-16 0 -27 -11l-51 -52q-11 -10 -11 -26q0 -15 11 -26l103 -103l-103 -103q-11 -11 -11 -26q0 -16 11 -26l51 -52q11 -11 27 -11
+q15 0 25 11l104 104l103 -104q11 -11 26 -11t26 11l52 52q11 10 11 26zM878 300q0 -119 -59 -220t-160 -160t-220.5 -59t-220 59t-159.5 160t-59 220t59 220t159.5 160t220 59t220.5 -59t160 -160t59 -220z" />
+    <glyph glyph-name="earth" unicode="&#xe61a;" 
+d="M874 662q-73 73 -166 111.5t-196 38.5t-196 -38.5t-166 -111.5t-111.5 -166t-38.5 -196t38.5 -196t111.5 -166t166 -111.5t196 -38.5t196 38.5t166 111.5t111.5 166t38.5 196t-38.5 196t-111.5 166zM942 466q-7 12 -22 19.5t-46 18.5q-16 5 -28.5 20t-18.5 28.5t-15 41.5
+q-8 24 -13.5 37t-17 31t-25.5 29q130 -82 186 -225zM798 290q2 -19 2.5 -35.5t-8.5 -38.5t-29 -45q-12 -13 -26 -50q-8 -22 -13.5 -33.5t-16.5 -26t-28 -21t-41 -6.5q-7 8 -12 37q-4 24 -10 90q-8 111 -21 160q-25 92 -84 112q-26 9 -55 9q-14 0 -39 -3q-17 -2 -24 -2v0
+q-8 0 -13 1.5t-13 10.5t-16 26q-11 30 -10 63.5t20.5 69t53.5 57.5q55 37 89 37q27 0 69 -23q39 -20 81 -20q8 0 22 1h16q18 0 31 -9t21.5 -26t19.5 -48q8 -24 13.5 -37.5t16.5 -33t27 -32t37 -19.5q22 -8 31 -11q-9 -9 -36 -34q-21 -18 -30 -27q-21 -19 -25.5 -46t0.5 -47
+v0zM51 304q8 -1 18 -3q34 -8 48 -15q-3 -6 -12 -18q-28 -41 -22 -65q5 -22 -6 -56q-26 74 -26 153v2v2v0zM512 -161q-129 0 -238 66.5t-168 176.5q40 76 27 132q0 5 14 24q6 8 9 12.5t7 13t5.5 14.5t1 14.5t-4.5 15.5q-12 26 -80 41q-13 3 -31 6q14 113 78 206t164 146.5
+t216 53.5q119 0 223 -58q-19 8 -43 8h-19q-13 -1 -19 -1q-29 0 -57 15q-53 28 -93 28q-49 0 -117 -45q-64 -42 -88 -117q-23 -73 5 -136q30 -69 89 -69v0q10 0 30 2q22 3 33 3q20 0 38 -6q34 -11 51 -77q12 -44 20 -151q6 -80 13 -110q6 -26 17 -41q16 -24 41 -24
+q68 0 108 44q21 24 41 77q11 28 16 34q27 30 39.5 61.5t11 53.5t-2.5 44q-3 21 -1.5 31.5t9.5 18.5q13 11 30 26q32 29 41 39q22 24 18 44v1q27 -75 27 -156q0 -94 -36.5 -179t-98.5 -147t-147.5 -98.5t-178.5 -36.5v0z" />
+    <glyph glyph-name="move" unicode="&#xe60f;" 
+d="M1008 330l-156 155q-14 14 -33.5 14t-33 -13.5t-13.5 -33t14 -33.5l76 -76h-303v309l76 -75q14 -14 33.5 -14t33 13.5t13.5 33t-14 33.5l-155 155q-14 14 -33.5 14t-32.5 -14l-156 -155q-14 -14 -14 -33.5t13.5 -33t33 -13.5t33.5 13l76 76v-309h-304l76 76
+q14 14 14 33.5t-13.5 33t-33 13.5t-33.5 -14l-156 -155q-13 -14 -13 -33.5t13 -32.5l156 -156q14 -14 33.5 -14t33 14t13.5 33t-14 33l-76 76h304v-303l-76 76q-14 14 -33.5 14t-33 -13.5t-13.5 -33t14 -33.5l155 -155q14 -14 33.5 -14t33.5 14l155 155q14 14 14 33.5
+t-13.5 33t-33 13.5t-33.5 -14l-76 -75v302h303l-76 -76q-14 -14 -14 -33t13.5 -33t33 -14t33.5 14l156 156q13 13 13 32.5t-13 33.5z" />
+    <glyph glyph-name="delete" unicode="&#xe610;" 
+d="M924 573h-154v105q0 29 -20.5 49.5t-49.5 20.5h-376q-29 0 -49.5 -20.5t-20.5 -49.5v-105h-155q-14 0 -24 -10t-10 -24.5t10 -24.5t24 -10h79v-581q0 -29 20.5 -49.5t49.5 -20.5h528q29 0 49.5 20.5t20.5 49.5v558v0v23h78q15 0 25 10t10 24.5t-10 24.5t-25 10z
+M412.5 -10q-14.5 0 -24.5 10t-10 25l-1 372q0 15 10.5 25t25 10t24.5 -10t10 -25v-372q0 -15 -10 -25t-24.5 -10zM611.5 -10q-14.5 0 -24.5 10t-10 25v372q0 15 10 25t24.5 10t24.5 -10t10 -25l1 -372q0 -15 -10.5 -25t-25 -10zM323 635q0 17 13 30t31 13h290q18 0 31 -13
+t13 -30v-62h-378v62z" />
+    <glyph glyph-name="doc" unicode="&#xe606;" horiz-adv-x="1281" 
+d="M1280 422zM492 -7q2 -1 3.5 -2t5.5 -5.5t5.5 -9.5t-1.5 -11.5t-12 -13.5h-151q-4 0 -10.5 1.5t-24.5 9.5t-32 20.5t-26.5 38t-15.5 58.5v397v0q0 4 1 12t7.5 28.5t17 37t32 32t49.5 19.5h341q4 0 10.5 -1.5t24 -10.5t31.5 -22t26.5 -40t14.5 -62v-102v-160t0 -75
+q-1 -4 -2.5 -11.5t-11.5 -29.5t-24.5 -43t-43 -48t-65.5 -49h-2h-5h-7h-8.5h-7.5h-5h-2q-15 4 -15.5 24.5t0.5 86.5v0q0 3 1 8t6 18.5t12.5 24.5t23.5 21.5t36 14.5h81v326q0 3 -1 8.5t-5.5 20t-11.5 25.5t-21 21.5t-32 12.5h-332q-3 0 -7.5 -1t-16.5 -6t-21 -13.5
+t-17.5 -27.5t-10.5 -44q-1 -203 -1 -386q1 -3 1.5 -9t5 -21t11.5 -26.5t21.5 -22.5t34.5 -13h146zM736 106h-70q-4 -1 -9.5 -3.5t-16.5 -15.5t-12 -30v-16v-31.5v-15.5l8 -1q5 0 34 29q37 37 66 84zM651 430q0 -10 -7.5 -17.5t-17.5 -7.5h-221q-10 0 -17.5 7.5t-7.5 17.5v0
+q0 11 7.5 18.5t17.5 7.5h221q10 0 17.5 -7.5t7.5 -18.5v0zM652 282q0 -11 -7.5 -18.5t-18.5 -7.5h-220q-11 0 -18.5 7.5t-7.5 18.5v0q0 11 7.5 18.5t18.5 7.5h220q11 0 18.5 -7.5t7.5 -18.5v0zM524 133q0 -11 -7.5 -19t-18.5 -8h-91q-11 0 -19 8t-8 19v0q0 11 8 19t19 8h91
+q11 0 18.5 -8t7.5 -19v0z" />
+    <glyph glyph-name="line" unicode="&#xe611;" 
+d="M960 552q0 -17 -12 -29.5t-30 -12.5h-812q-18 0 -30 12.5t-12 29.5v0q0 18 12 30t30 12h812q18 0 30 -12t12 -30v0zM960 300q0 -17 -12 -29.5t-30 -12.5h-812q-18 0 -30 12.5t-12 29.5v0q0 17 12 29.5t30 12.5h812q18 0 30 -12.5t12 -29.5v0zM960 48q0 -18 -12 -30
+t-30 -12h-812q-18 0 -30 12t-12 30v0q0 17 12 29.5t30 12.5h812q18 0 30 -12.5t12 -29.5v0z" />
+    <glyph glyph-name="file-box" unicode="&#xe61b;" 
+d="M804 770l50 -431l-57 -7l-45 386h-480l-45 -386l-57 7l50 431h584zM319 674h386v-48h-386v48zM319 577h386v-48h-386v48zM319 481h386v-49h-386v49zM319 384h386v-48h-386v48zM874 287h-724q-10 0 -15 -6.5t-2 -15.5l82 -244q3 -10 12 -16.5t19 -6.5h532q10 0 19 6.5
+t12 16.5l82 244q3 9 -2 15.5t-15 6.5zM609 191h-194v48h194v-48z" />
+    <glyph glyph-name="switchon" unicode="&#xe615;" 
+d="M832 896h-640q-80 0 -136 -56t-56 -136v-640q0 -79 56 -135.5t136 -56.5h640q79 0 135.5 56.5t56.5 135.5v640q0 80 -56.5 136t-135.5 56zM160 0q-13 0 -22.5 9.5t-9.5 22.5t9.5 22.5t22.5 9.5t22.5 -9.5t9.5 -22.5t-9.5 -22.5t-22.5 -9.5zM160 704q-13 0 -22.5 9.5
+t-9.5 22.5t9.5 22.5t22.5 9.5t22.5 -9.5t9.5 -22.5t-9.5 -22.5t-22.5 -9.5zM704 64q0 -27 -18.5 -45.5t-45.5 -18.5h-256q-27 0 -45.5 18.5t-18.5 45.5v640q0 26 19 45t45 19h256q27 0 45.5 -18.5t18.5 -45.5v-640zM864 0q-13 0 -22.5 9.5t-9.5 22.5t9.5 22.5t22.5 9.5
+t22.5 -9.5t9.5 -22.5t-9.5 -22.5t-22.5 -9.5zM864 704q-13 0 -22.5 9.5t-9.5 22.5t9.5 22.5t22.5 9.5t22.5 -9.5t9.5 -22.5t-9.5 -22.5t-22.5 -9.5zM576 704h-128q-26 0 -45 -19t-19 -45v-192q0 -26 19 -45t45 -19h128q27 0 45.5 18.5t18.5 45.5v192q0 27 -18.5 45.5
+t-45.5 18.5z" />
+    <glyph glyph-name="sysserver" unicode="&#xe605;" 
+d="M515 145q-36 0 -71 -12t-64 -41t-36 -69h338q-5 53 -57 87.5t-110 34.5zM955 157q-3 265 0 528q0 34 -12.5 47t-46.5 13q-45 -1 -384 -1v0q-339 0 -390 1q-28 0 -40.5 -11t-12.5 -39q1 -231 0 -544q0 -28 11.5 -40.5t39.5 -11.5q35 1 107 0.5t107 0.5q25 1 48 17
+q56 40 115.5 45t116.5 -28q63 -37 150 -35q94 2 132 0q33 -2 46 11t13 47zM892 213h-760v456h760v-456z" />
+    <glyph glyph-name="file" unicode="&#xe77d;" 
+d="M645 809h-372q-44 0 -75 -31t-31 -75v-638q0 -44 31 -75t75 -31h478q44 0 75 31t31 75v532zM643 726l127 -127h-104q-9 0 -16 6.5t-7 16.5v104zM751 19h-478q-19 0 -32.5 13.5t-13.5 32.5v638q0 19 13.5 32.5t32.5 13.5h310v-127q0 -34 24.5 -58.5t58.5 -24.5h131v-474
+q0 -19 -13.5 -32.5t-32.5 -13.5zM682 388h-340q-12 0 -21 -9t-9 -21.5t9 -21t21 -8.5h340q12 0 21 8.5t9 21t-9 21.5t-21 9zM682 232h-340q-12 0 -21 -9t-9 -21.5t9 -21t21 -8.5h340q12 0 21 8.5t9 21t-9 21.5t-21 9z" />
+  </font>
+</defs></svg>

BIN
app/ui2/components/panel/iconfont/iconfont.ttf


BIN
app/ui2/components/panel/iconfont/iconfont.woff


+ 316 - 0
app/ui2/components/panel/list.js

@@ -0,0 +1,316 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+import React from 'react';
+import ListItem from './list_item';
+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);
+    }
+
+    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.'
+            });
+        });
+    }
+
+    selectOne(host) {
+        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();
+            }
+        });
+    }
+
+    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>
+        );
+    }
+}
+
+export default List;

+ 10 - 0
app/ui2/components/panel/list.less

@@ -0,0 +1,10 @@
+#sh-list {
+
+  .custom-items {
+    position: fixed;
+    width: 240px;
+    top: 36px;
+    bottom: 30px;
+    overflow: auto;
+  }
+}

+ 151 - 0
app/ui2/components/panel/list_item.js

@@ -0,0 +1,151 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+import React from 'react';
+import classnames from 'classnames';
+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;
+    }
+
+    beSelected() {
+        // this.setState({
+        //     is_selected: true
+        // });
+
+        this.props.selectOne(this.props.data);
+    }
+
+    toEdit() {
+        SH_event.emit('edit_host', this.props.data);
+    }
+
+    toggle() {
+        let on = !this.props.data.on;
+
+        this.props.onToggle(() => {
+            this.props.data.on = on;
+            this.forceUpdate();
+        });
+
+        SH_event.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'));
+
+        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
+            })}
+                 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>
+        );
+    }
+}

+ 65 - 0
app/ui2/components/panel/list_item.less

@@ -0,0 +1,65 @@
+#sh-list {
+  .list-item {
+    padding: 7px 10px 7px 15px;
+    cursor: pointer;
+
+    &.hidden {
+      display: none;
+    }
+
+    &.sys-host {
+      font-size: 14px;
+      padding: 8px 10px 8px 12px;
+
+      i {
+        width: 23px;
+      }
+    }
+
+    &.selected {
+      background: #2d3138;
+
+      span {
+        color: #fff;
+      }
+
+      i.item-icon {
+        color: #fff;
+      }
+
+      &:hover {
+        i.icon-edit {
+          display: block;
+          color: #fff;
+        }
+      }
+    }
+
+    i {
+      display: inline-block;
+      width: 20px;
+      text-align: center;
+      margin-right: 5px;
+
+      &.item-icon {
+        font-size: 12px;
+      }
+
+      &.switch {
+        float: right;
+        cursor: pointer;
+        line-height: 23px;
+
+        &.icon-on {
+          color: #af9;
+        }
+      }
+
+      &.icon-edit {
+        float: right;
+        display: none;
+      }
+    }
+
+  }
+}

+ 26 - 0
app/ui2/components/panel/panel.js

@@ -0,0 +1,26 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+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;
+
+        return (
+            <div id="panel">
+                <List hosts={hosts} current={current} setCurrent={this.props.setCurrent}/>
+                <SearchBar/>
+                <Buttons/>
+            </div>
+        );
+    }
+}

+ 10 - 0
app/ui2/components/panel/panel.less

@@ -0,0 +1,10 @@
+@import '../cfg.less';
+@import "iconfont/iconfont.css";
+
+#panel {
+
+  width: 240px;
+  height: 100%;
+  background: @bg_left;
+  color: @font_color_left;
+}

+ 79 - 0
app/ui2/components/panel/searchbar.js

@@ -0,0 +1,79 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+import React from 'react';
+import classnames from 'classnames';
+import './searchbar.less';
+
+export default class SearchBar extends React.Component {
+
+    constructor(props) {
+        super(props);
+
+        this.state = {
+            show: false,
+            keyword: ''
+        };
+
+        this._t = null;
+
+        SH_event.on('search_on', () => {
+            this.setState({
+                show: true
+            }, () => {
+                setTimeout(() => {
+                    this.refs.keyword.focus();
+                }, 100);
+            });
+        });
+
+        SH_event.on('search_off', () => {
+            this.clearSearch();
+        });
+    }
+
+    clearSearch() {
+        this.setState({
+            show: false,
+            keyword: ''
+        });
+        SH_event.emit('search', '')
+    }
+
+    doSearch(kw) {
+        this.setState({
+            keyword: kw
+        });
+
+        clearTimeout(this._t);
+        this._t = setTimeout(() => {
+            SH_event.emit('search', kw)
+        }, 300);
+    }
+
+    onCancel() {
+        SH_event.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>
+        );
+    }
+}

+ 19 - 0
app/ui2/components/panel/searchbar.less

@@ -0,0 +1,19 @@
+@import '../cfg.less';
+
+#sh-searchbar {
+  position: fixed;
+  width: 240px;
+  left: 0;
+  bottom: 30px;
+  background: @bg_left_search;
+  padding: 6px 20px 5px 18px;
+  border-top: solid 1px @bg_left * 0.9;
+
+  input {
+    background: transparent;
+    border: 0;
+    outline: 0;
+    color: #fff;
+    font-size: 13px;
+  }
+}

+ 20 - 0
app/ui2/ui.js

@@ -0,0 +1,20 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+//import React from 'react';
+//import ReactDom from 'react-dom';
+//import App from './components/app';
+//
+//ReactDom.render(
+//    <App/>
+//    , document.getElementById('app')
+//);
+
+console.log(SHRenderer.act)
+SHRenderer.act('test', (e, r) => {
+  console.log(e, r)
+})

+ 1 - 1
app/version.js

@@ -1 +1 @@
-exports.version = [3,2,3,4318];
+exports.version = [3,3,0,4318];

+ 1 - 1
webpack.config.js

@@ -9,7 +9,7 @@ const path = require('path');
 const webpack = require('webpack');
 
 module.exports = {
-    entry: './app/ui/ui.js',
+    entry: './app/ui2/ui.js',
     devtool: 'source-map',
     output: {
         path: path.join(__dirname, 'app', 'build'),

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