├── dist ├── web │ └── .gitkeep └── electron │ └── .gitkeep ├── static └── .gitkeep ├── src ├── renderer │ ├── assets │ │ ├── .gitkeep │ │ └── logo.png │ ├── views │ │ ├── connect │ │ │ ├── components │ │ │ │ ├── index.js │ │ │ │ └── KeyTap.vue │ │ │ ├── form.vue │ │ │ ├── db.vue │ │ │ └── keys.vue │ │ ├── layout │ │ │ ├── components │ │ │ │ ├── index.js │ │ │ │ ├── AppMain.vue │ │ │ │ ├── Sidebar │ │ │ │ │ ├── index.vue │ │ │ │ │ └── SidebarItem.vue │ │ │ │ ├── Navbar.vue │ │ │ │ └── SettingModal.vue │ │ │ └── Layout.vue │ │ └── dashboard │ │ │ └── index.vue │ ├── styles │ │ ├── variables.scss │ │ ├── mixin.scss │ │ ├── transition.scss │ │ ├── element-ui.scss │ │ ├── index.scss │ │ └── sidebar.scss │ ├── static │ │ └── font-awesome │ │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ │ └── css │ │ │ └── font-awesome.min.css │ ├── icons │ │ ├── index.js │ │ └── svg │ │ │ ├── table.svg │ │ │ ├── user.svg │ │ │ ├── example.svg │ │ │ ├── password.svg │ │ │ ├── form.svg │ │ │ ├── eye.svg │ │ │ ├── add.svg │ │ │ └── tree.svg │ ├── App.vue │ ├── store │ │ ├── index.js │ │ ├── getters.js │ │ └── modules │ │ │ ├── setting.js │ │ │ ├── app.js │ │ │ └── connect.js │ ├── utils │ │ ├── redis.js │ │ ├── validate.js │ │ ├── index.js │ │ └── localStore.js │ ├── api │ │ ├── StringElement.js │ │ ├── SetElement.js │ │ ├── HashElement.js │ │ ├── SortedSetElement.js │ │ ├── Element.js │ │ ├── ListElement.js │ │ └── index.js │ ├── main.js │ ├── components │ │ ├── SvgIcon │ │ │ └── index.vue │ │ ├── Hamburger │ │ │ └── index.vue │ │ ├── JsonEditor │ │ │ └── index.vue │ │ └── Breadcrumb │ │ │ └── index.vue │ └── router │ │ └── index.js ├── main │ ├── index.dev.js │ └── index.js └── index.ejs ├── .gitignore ├── .babelrc ├── appveyor.yml ├── .travis.yml ├── README.md ├── .electron-vue ├── dev-client.js ├── webpack.main.config.js ├── build.js ├── webpack.web.config.js ├── dev-runner.js └── webpack.renderer.config.js └── package.json /dist/web/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/electron/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/views/connect/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as KeyTap } from './KeyTap' -------------------------------------------------------------------------------- /src/renderer/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sidfate/RedisCX/HEAD/src/renderer/assets/logo.png -------------------------------------------------------------------------------- /src/renderer/styles/variables.scss: -------------------------------------------------------------------------------- 1 | //sidebar 2 | $menuBg:#304156; 3 | $subMenuBg:#1f2d3d; 4 | $menuHover:#001528; 5 | -------------------------------------------------------------------------------- /src/renderer/static/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sidfate/RedisCX/HEAD/src/renderer/static/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/renderer/static/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sidfate/RedisCX/HEAD/src/renderer/static/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/renderer/static/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sidfate/RedisCX/HEAD/src/renderer/static/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/renderer/static/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sidfate/RedisCX/HEAD/src/renderer/static/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/renderer/static/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sidfate/RedisCX/HEAD/src/renderer/static/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/renderer/views/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/electron/* 3 | dist/web/* 4 | build/* 5 | !build/icons 6 | node_modules/ 7 | npm-debug.log 8 | npm-debug.log.* 9 | thumbs.db 10 | !.gitkeep 11 | .idea/ 12 | package-lock.json -------------------------------------------------------------------------------- /src/renderer/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg组件 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const requireAll = requireContext => requireContext.keys().map(requireContext) 8 | const req = require.context('./svg', false, /\.svg$/) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 26 | -------------------------------------------------------------------------------- /src/renderer/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import app from './modules/app' 4 | import connect from './modules/connect' 5 | import setting from './modules/setting' 6 | import getters from './getters' 7 | 8 | Vue.use(Vuex) 9 | 10 | const store = new Vuex.Store({ 11 | modules: { 12 | app, 13 | connect, 14 | setting 15 | }, 16 | getters 17 | }) 18 | 19 | export default store 20 | -------------------------------------------------------------------------------- /src/renderer/utils/redis.js: -------------------------------------------------------------------------------- 1 | import Store from 'electron-store' 2 | 3 | const store = new Store() 4 | const CONNECT_MAP = 'connectMap' 5 | 6 | export function getConnectMap() { 7 | const map = store.get(CONNECT_MAP) 8 | return map ? map : {} 9 | } 10 | 11 | export function setConnectMap(connectionMap) { 12 | return store.set(CONNECT_MAP, connectionMap) 13 | } 14 | 15 | export function cleanConnect() { 16 | return store.delete(CONNECT_MAP) 17 | } -------------------------------------------------------------------------------- /src/renderer/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | device: state => state.app.device, 4 | connectMap: state => state.connect.connectMap, 5 | handler: state => state.connect.handler, 6 | selectedName: state => state.connect.selectedName, 7 | cacheOptions: state => state.connect.cacheOptions, 8 | autoSearch: state => state.setting.autoSearch, 9 | autoSearchLimit: state => state.setting.autoSearchLimit 10 | } 11 | export default getters 12 | -------------------------------------------------------------------------------- /src/renderer/api/StringElement.js: -------------------------------------------------------------------------------- 1 | 2 | import Element from './Element' 3 | 4 | export default class StringElement extends Element { 5 | 6 | async scan() { 7 | const string = await this.handler.get(this.key) 8 | console.log(string) 9 | let result = null 10 | try { 11 | result = JSON.parse(string) 12 | if(typeof result !== 'object' || !result) { 13 | result = string 14 | } 15 | }catch (e) { 16 | result = '' + string 17 | } 18 | 19 | return result 20 | } 21 | } -------------------------------------------------------------------------------- /src/renderer/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | &::-webkit-scrollbar { 14 | width: 6px; 15 | } 16 | &::-webkit-scrollbar-thumb { 17 | background: #99a9bf; 18 | border-radius: 20px; 19 | } 20 | } 21 | 22 | @mixin relative { 23 | position: relative; 24 | width: 100%; 25 | height: 100%; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/renderer/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/views/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "comments": false, 3 | "env": { 4 | "main": { 5 | "presets": [ 6 | ["env", { 7 | "targets": { "node": 7 } 8 | }], 9 | "stage-0" 10 | ] 11 | }, 12 | "renderer": { 13 | "presets": [ 14 | ["env", { 15 | "modules": false 16 | }], 17 | "stage-0" 18 | ] 19 | }, 20 | "web": { 21 | "presets": [ 22 | ["env", { 23 | "modules": false 24 | }], 25 | "stage-0" 26 | ] 27 | } 28 | }, 29 | "plugins": ["transform-runtime"] 30 | } 31 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 0.1.{build} 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | image: Visual Studio 2017 8 | platform: 9 | - x64 10 | 11 | cache: 12 | - node_modules 13 | - '%APPDATA%\npm-cache' 14 | - '%USERPROFILE%\.electron' 15 | - '%USERPROFILE%\AppData\Local\Yarn\cache' 16 | 17 | init: 18 | - git config --global core.autocrlf input 19 | 20 | install: 21 | - ps: Install-Product node 8 x64 22 | - choco install yarn --ignore-dependencies 23 | - git reset --hard HEAD 24 | - yarn 25 | - node --version 26 | 27 | build_script: 28 | - yarn build 29 | 30 | test: off 31 | -------------------------------------------------------------------------------- /src/renderer/styles/transition.scss: -------------------------------------------------------------------------------- 1 | //globl transition css 2 | 3 | /*fade*/ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /*fade*/ 15 | .breadcrumb-enter-active, 16 | .breadcrumb-leave-active { 17 | transition: all .5s; 18 | } 19 | 20 | .breadcrumb-enter, 21 | .breadcrumb-leave-active { 22 | opacity: 0; 23 | transform: translateX(20px); 24 | } 25 | 26 | .breadcrumb-move { 27 | transition: all .5s; 28 | } 29 | 30 | .breadcrumb-leave-active { 31 | position: absolute; 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | //to reset element-ui default css 2 | .el-upload { 3 | input[type="file"] { 4 | display: none !important; 5 | } 6 | } 7 | 8 | .el-upload__input { 9 | display: none; 10 | } 11 | 12 | //暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461 13 | .el-dialog { 14 | transform: none; 15 | left: 0; 16 | position: relative; 17 | margin: 0 auto; 18 | } 19 | 20 | //element ui upload 21 | .upload-container { 22 | .el-upload { 23 | width: 100%; 24 | .el-upload-dragger { 25 | width: 100%; 26 | height: 200px; 27 | } 28 | } 29 | } 30 | 31 | .el-card__header { 32 | padding: 15px 10px; 33 | } -------------------------------------------------------------------------------- /src/renderer/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/api/SetElement.js: -------------------------------------------------------------------------------- 1 | 2 | import Element from './Element' 3 | 4 | export default class SetElement extends Element { 5 | async save(element) { 6 | if(element.rawValue) { 7 | await this.handler.srem(this.key, element.rawValue) 8 | } 9 | await this.handler.sadd(this.key, element.value) 10 | } 11 | 12 | async batchDelete(list) { 13 | let pipeline = this.handler.pipeline() 14 | 15 | list.forEach((item) => { 16 | pipeline.srem(this.key, item.value) 17 | }) 18 | 19 | return this.exec(pipeline) 20 | } 21 | 22 | async scan(search) { 23 | const match = search ? '*'+search+'*' : '*' 24 | const list = await this.scanStream('sscan', match, 10) 25 | 26 | return list[0].map((item) => { 27 | return {key: null, value: item} 28 | }) 29 | } 30 | } -------------------------------------------------------------------------------- /src/renderer/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import App from './App' 4 | import router from './router' 5 | import store from './store' 6 | 7 | import 'normalize.css/normalize.css'// A modern alternative to CSS resets 8 | 9 | import ElementUI from 'element-ui' 10 | import 'element-ui/lib/theme-chalk/index.css' 11 | import locale from 'element-ui/lib/locale/lang/en' 12 | 13 | import '@/styles/index.scss' // global css 14 | import '@/icons' // icon 15 | import '@/static/font-awesome/css/font-awesome.min.css' 16 | 17 | Vue.use(ElementUI, { locale }) 18 | 19 | if (!process.env.IS_WEB) Vue.use(require('vue-electron')) 20 | Vue.config.productionTip = false 21 | 22 | /* eslint-disable no-new */ 23 | new Vue({ 24 | components: { App }, 25 | router, 26 | store, 27 | template: '' 28 | }).$mount('#app') 29 | -------------------------------------------------------------------------------- /src/renderer/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/views/layout/Layout.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 33 | 34 | 37 | -------------------------------------------------------------------------------- /src/renderer/views/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 35 | -------------------------------------------------------------------------------- /src/renderer/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | 34 | 43 | -------------------------------------------------------------------------------- /src/main/index.dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used specifically and only for development. It installs 3 | * `electron-debug` & `vue-devtools`. There shouldn't be any need to 4 | * modify this file, but it can be used to extend your development 5 | * environment. 6 | */ 7 | 8 | /* eslint-disable */ 9 | 10 | // Set environment for development 11 | process.env.NODE_ENV = 'development' 12 | 13 | // Install `electron-debug` with `devtron` 14 | require('electron-debug')({ showDevTools: true }) 15 | 16 | // Install `vue-devtools` 17 | require('electron').app.on('ready', () => { 18 | let installExtension = require('electron-devtools-installer') 19 | installExtension.default(installExtension.VUEJS_DEVTOOLS) 20 | .then(() => {}) 21 | .catch(err => { 22 | console.log('Unable to install `vue-devtools`: \n', err) 23 | }) 24 | }) 25 | 26 | // Require `main` process to boot app 27 | require('./index') 28 | -------------------------------------------------------------------------------- /src/renderer/api/HashElement.js: -------------------------------------------------------------------------------- 1 | 2 | import Element from './Element' 3 | 4 | export default class HashElement extends Element { 5 | 6 | async save(element) { 7 | await this.handler.hset(this.key, element.key, element.value) 8 | } 9 | 10 | async batchDelete(list) { 11 | let pipeline = this.handler.pipeline() 12 | 13 | list.forEach((item) => { 14 | pipeline.hdel(this.key, item.key) 15 | }) 16 | 17 | return this.exec(pipeline) 18 | } 19 | 20 | async scan(search) { 21 | const match = search ? '*'+search+'*' : '*' 22 | let list = await this.scanStream('hscan', match, 100) 23 | 24 | let result = [] 25 | list.map((item, index) => { 26 | item.map((one, index) => { 27 | if(index%2 === 0) { 28 | result.push({key: one, value: item[index+1]}) 29 | } 30 | }) 31 | }) 32 | 33 | return result 34 | } 35 | 36 | get hasElementKey() { 37 | return true 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/renderer/store/modules/setting.js: -------------------------------------------------------------------------------- 1 | import { getAutoSearch, getAutoSearchLimit, setAutoSearch, setAutoSearchLimit } from '@/utils/localStore' 2 | 3 | const setting = { 4 | state: { 5 | autoSearch: getAutoSearch(), 6 | autoSearchLimit: getAutoSearchLimit() 7 | }, 8 | mutations: { 9 | SET_AUTO_SEARCH: (state, status) => { 10 | state.autoSearch = status 11 | }, 12 | SET_AUTO_SEARCH_LIMIT: (state, limit) => { 13 | state.autoSearchLimit = limit 14 | } 15 | }, 16 | actions: { 17 | EnableAutoSearch: ({ commit }) => { 18 | setAutoSearch(true) 19 | commit('SET_AUTO_SEARCH', true) 20 | }, 21 | DisableAutoSearch: ({ commit }) => { 22 | setAutoSearch(false) 23 | commit('SET_AUTO_SEARCH', false) 24 | }, 25 | ChangeAutoSearchLimit: ({ commit }, limit) => { 26 | setAutoSearchLimit(limit) 27 | commit('SET_AUTO_SEARCH_LIMIT', limit) 28 | } 29 | } 30 | } 31 | 32 | export default setting 33 | -------------------------------------------------------------------------------- /src/renderer/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jiachenpan on 16/11/18. 3 | */ 4 | 5 | export function isvalidUsername(str) { 6 | const valid_map = ['admin', 'editor'] 7 | return valid_map.indexOf(str.trim()) >= 0 8 | } 9 | 10 | /* 合法uri*/ 11 | export function validateURL(textval) { 12 | const urlregex = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 13 | return urlregex.test(textval) 14 | } 15 | 16 | /* 小写字母*/ 17 | export function validateLowerCase(str) { 18 | const reg = /^[a-z]+$/ 19 | return reg.test(str) 20 | } 21 | 22 | /* 大写字母*/ 23 | export function validateUpperCase(str) { 24 | const reg = /^[A-Z]+$/ 25 | return reg.test(str) 26 | } 27 | 28 | /* 大小写字母*/ 29 | export function validatAlphabets(str) { 30 | const reg = /^[A-Za-z]+$/ 31 | return reg.test(str) 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/renderer/api/SortedSetElement.js: -------------------------------------------------------------------------------- 1 | 2 | import Element from './Element' 3 | 4 | export default class SortedSetElement extends Element { 5 | 6 | async save(element) { 7 | if(element.rawValue) { 8 | await this.handler.zrem(this.key, element.rawValue) 9 | } 10 | await this.handler.zadd(this.key, element.score, element.value) 11 | } 12 | 13 | async batchDelete(list) { 14 | let pipeline = this.handler.pipeline() 15 | 16 | list.forEach((item) => { 17 | pipeline.zrem(this.key, item.value) 18 | }) 19 | 20 | return this.exec(pipeline) 21 | } 22 | 23 | async scan(search) { 24 | const match = search ? '*'+search+'*' : '*' 25 | const list = await this.scanStream('zscan', match, 100) 26 | 27 | let result = [] 28 | list.map((item, index) => { 29 | item.map((one, index) => { 30 | if(index%2 === 0) { 31 | result.push({value: one, score: item[index+1]}) 32 | } 33 | }) 34 | }) 35 | 36 | return result 37 | } 38 | 39 | get hasElementScore() { 40 | return true 41 | } 42 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode8.3 2 | sudo: required 3 | dist: trusty 4 | language: c 5 | matrix: 6 | include: 7 | - os: osx 8 | - os: linux 9 | env: CC=clang CXX=clang++ npm_config_clang=1 10 | compiler: clang 11 | cache: 12 | directories: 13 | - node_modules 14 | - "$HOME/.electron" 15 | - "$HOME/.cache" 16 | addons: 17 | apt: 18 | packages: 19 | - libgnome-keyring-dev 20 | - icnsutils 21 | before_install: 22 | - mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v1.2.1/git-lfs-$([ 23 | "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-1.2.1.tar.gz 24 | | tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull 25 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi 26 | install: 27 | - nvm install 7 28 | - curl -o- -L https://yarnpkg.com/install.sh | bash 29 | - source ~/.bashrc 30 | - npm install -g xvfb-maybe 31 | - yarn 32 | script: 33 | - yarn run build 34 | branches: 35 | only: 36 | - master 37 | -------------------------------------------------------------------------------- /src/renderer/api/Element.js: -------------------------------------------------------------------------------- 1 | 2 | var _ = require('lodash') 3 | var Redis = require('ioredis') 4 | 5 | export default class Element { 6 | constructor(handler, key) { 7 | this.handler = handler 8 | this.key = key 9 | } 10 | 11 | get hasElementKey() { 12 | return false 13 | } 14 | 15 | get hasElementScore() { 16 | return false 17 | } 18 | 19 | scanStream(command, match, count) { 20 | return new Promise((resolve, reject) => { 21 | let allKeys = [] 22 | 23 | let stream = this.handler[command+'Stream'](this.key, { 24 | match, 25 | count 26 | }) 27 | 28 | stream.on('data', function (resultKeys) { 29 | allKeys.push(resultKeys) 30 | }) 31 | stream.on('end', function () { 32 | resolve(allKeys) 33 | }) 34 | }) 35 | } 36 | 37 | exec(pipeline) { 38 | console.log(pipeline) 39 | return new Promise((resolve, reject) => { 40 | pipeline.exec(function (err, results) { 41 | let error = _.find(results, (o)=> (o[0] instanceof Redis.ReplyError)) 42 | 43 | if(error) { 44 | reject(error[0].message) 45 | }else { 46 | resolve() 47 | } 48 | }) 49 | }) 50 | } 51 | } -------------------------------------------------------------------------------- /src/renderer/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RedisCX [![996.icu](https://img.shields.io/badge/link-996.icu-red.svg)](https://996.icu) 2 | 3 | ![logo](https://i.loli.net/2018/07/10/5b447752b3020.png) 4 | 5 | [HomePage](https://sidfate.github.io/RedisCX/) 6 | 7 | > Redis client for any platform. 8 | 9 | See more about [Client-X-Series](https://clientx.dev/) 10 | 11 | ### Feature 12 | 13 | **What's important!** 14 | 15 | * Support multiple connections. 16 | * Optimize searching for keys in Big-Data dbs. 17 | * Support reserving search history. 18 | * More lighter UI and simpler operation. 19 | * ... 20 | 21 | ### Downloads 22 | 23 | [Download Link](https://github.com/Sidfate/redisCX/releases) 24 | 25 | ### Snapshots 26 | 27 | ![1](https://i.loli.net/2018/08/29/5b864a207a78b.png) 28 | 29 | ![3](https://i.loli.net/2018/10/16/5bc54e99dd306.png) 30 | 31 | ![2](https://i.loli.net/2018/08/29/5b864a207c70d.png) 32 | 33 | ![3](https://i.loli.net/2018/08/29/5b864a207e6ed.png) 34 | 35 | ### Development 36 | 37 | ``` bash 38 | # install dependencies 39 | npm install 40 | 41 | # serve with hot reload at localhost:9080 42 | npm run dev 43 | 44 | # build electron application for production 45 | npm run build 46 | 47 | ``` 48 | 49 | --- 50 | 51 | ### License 52 | 53 | MIT 54 | -------------------------------------------------------------------------------- /src/renderer/api/ListElement.js: -------------------------------------------------------------------------------- 1 | 2 | import Element from './Element' 3 | import md5 from 'md5' 4 | 5 | export default class ListElement extends Element { 6 | 7 | async save(element) { 8 | const length = await this.handler.llen(this.key) 9 | 10 | if(!element.key || element.key >= length) { 11 | await this.handler.lpush(this.key, element.value) 12 | }else { 13 | await this.handler.lset(this.key, element.key, element.value) 14 | } 15 | } 16 | 17 | async batchDelete(list) { 18 | let pipeline = this.handler.pipeline() 19 | 20 | console.log(md5(111)) 21 | list.forEach((item) => { 22 | pipeline.lset(this.key, item.key, md5(item.key)) 23 | pipeline.lrem(this.key, 0, md5(item.key)) 24 | }) 25 | 26 | return this.exec(pipeline) 27 | } 28 | 29 | async scan(search) { 30 | const listLength = await this.handler.llen(this.key) 31 | const list = await this.handler.lrange(this.key, 0, listLength) 32 | 33 | 34 | let result = [] 35 | list.map((value, key) => { 36 | if(!search || value.indexOf(search) >= 0) { 37 | result.push({key, value}) 38 | } 39 | }) 40 | 41 | return result 42 | } 43 | 44 | get hasElementKey() { 45 | return true 46 | } 47 | } -------------------------------------------------------------------------------- /src/renderer/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const app = { 4 | state: { 5 | sidebar: { 6 | opened: !+Cookies.get('sidebarStatus'), 7 | withoutAnimation: false 8 | }, 9 | device: 'desktop' 10 | }, 11 | mutations: { 12 | TOGGLE_SIDEBAR: state => { 13 | if (state.sidebar.opened) { 14 | Cookies.set('sidebarStatus', 1) 15 | } else { 16 | Cookies.set('sidebarStatus', 0) 17 | } 18 | state.sidebar.opened = !state.sidebar.opened 19 | state.sidebar.withoutAnimation = false 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | Cookies.set('sidebarStatus', 1) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | TOGGLE_DEVICE: (state, device) => { 27 | state.device = device 28 | } 29 | }, 30 | actions: { 31 | ToggleSideBar: ({ commit }) => { 32 | commit('TOGGLE_SIDEBAR') 33 | }, 34 | CloseSideBar({ commit }, { withoutAnimation }) { 35 | commit('CLOSE_SIDEBAR', withoutAnimation) 36 | }, 37 | ToggleDevice({ commit }, device) { 38 | commit('TOGGLE_DEVICE', device) 39 | } 40 | } 41 | } 42 | 43 | export default app 44 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RedisCX 6 | <% if (htmlWebpackPlugin.options.nodeModules) { %> 7 | 8 | 11 | <% } %> 12 | 22 | 23 | 24 |
25 | 26 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/renderer/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.electron-vue/dev-client.js: -------------------------------------------------------------------------------- 1 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 2 | 3 | hotClient.subscribe(event => { 4 | /** 5 | * Reload browser when HTMLWebpackPlugin emits a new index.html 6 | * 7 | * Currently disabled until jantimon/html-webpack-plugin#680 is resolved. 8 | * https://github.com/SimulatedGREG/electron-vue/issues/437 9 | * https://github.com/jantimon/html-webpack-plugin/issues/680 10 | */ 11 | // if (event.action === 'reload') { 12 | // window.location.reload() 13 | // } 14 | 15 | /** 16 | * Notify `mainWindow` when `main` process is compiling, 17 | * giving notice for an expected reload of the `electron` process 18 | */ 19 | if (event.action === 'compiling') { 20 | document.body.innerHTML += ` 21 | 34 | 35 |
36 | Compiling Main Process... 37 |
38 | ` 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /src/renderer/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './transition.scss'; 4 | @import './element-ui.scss'; 5 | //@import './sidebar.scss'; 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | 24 | #app{ 25 | height: 100%; 26 | } 27 | 28 | *, 29 | *:before, 30 | *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | a, 35 | a:focus, 36 | a:hover { 37 | cursor: pointer; 38 | color: inherit; 39 | outline: none; 40 | text-decoration: none; 41 | } 42 | 43 | div:focus{ 44 | outline: none; 45 | } 46 | 47 | a:focus, 48 | a:active { 49 | outline: none; 50 | } 51 | 52 | a, 53 | a:focus, 54 | a:hover { 55 | cursor: pointer; 56 | color: inherit; 57 | text-decoration: none; 58 | } 59 | 60 | .clearfix { 61 | &:after { 62 | visibility: hidden; 63 | display: block; 64 | font-size: 0; 65 | content: " "; 66 | clear: both; 67 | height: 0; 68 | } 69 | } 70 | 71 | //main-container全局样式 72 | .app-main{ 73 | min-height: 100% 74 | } 75 | 76 | .app-container { 77 | padding: 20px; 78 | } 79 | 80 | .operation-container { 81 | margin-bottom: 20px; 82 | } 83 | 84 | .search-container { 85 | float: right; 86 | } 87 | -------------------------------------------------------------------------------- /src/renderer/api/index.js: -------------------------------------------------------------------------------- 1 | 2 | import HashElement from './HashElement' 3 | import StringElement from './StringElement' 4 | import ListElement from './ListElement' 5 | import SetElement from './SetElement' 6 | import SortedSetElement from './SortedSetElement' 7 | 8 | export default class Factory { 9 | constructor(redis, key) { 10 | this.redis = redis 11 | this.key = key 12 | } 13 | 14 | async build() { 15 | let element = null 16 | const type = await this.redis.type(this.key) 17 | const ttl = await this.redis.ttl(this.key) 18 | this.type = type 19 | this.ttl = ttl 20 | 21 | switch (type) { 22 | case 'string': 23 | element = new StringElement(this.redis, this.key) 24 | break 25 | case 'hash': 26 | element = new HashElement(this.redis, this.key) 27 | break 28 | case 'list': 29 | element = new ListElement(this.redis, this.key) 30 | break 31 | case 'set': 32 | element = new SetElement(this.redis, this.key) 33 | break 34 | case 'zset': 35 | element = new SortedSetElement(this.redis, this.key) 36 | break 37 | default: 38 | return null 39 | } 40 | this.element = element 41 | return this 42 | } 43 | 44 | async scan(search) { 45 | return await this.element.scan(search) 46 | } 47 | 48 | async batchDelete(list) { 49 | return await this.element.batchDelete(list) 50 | } 51 | 52 | async save(element) { 53 | return await this.element.save(element) 54 | } 55 | 56 | async getType() { 57 | return this.type 58 | } 59 | 60 | async getTtl() { 61 | return this.ttl 62 | } 63 | 64 | get hasElementKey() { 65 | return this.element.hasElementKey 66 | } 67 | 68 | get hasElementScore() { 69 | return this.element.hasElementScore 70 | } 71 | } -------------------------------------------------------------------------------- /.electron-vue/webpack.main.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'main' 4 | 5 | const path = require('path') 6 | const { dependencies } = require('../package.json') 7 | const webpack = require('webpack') 8 | 9 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 10 | 11 | let mainConfig = { 12 | entry: { 13 | main: path.join(__dirname, '../src/main/index.js') 14 | }, 15 | externals: [ 16 | ...Object.keys(dependencies || {}) 17 | ], 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js$/, 22 | use: 'babel-loader', 23 | exclude: /node_modules/ 24 | }, 25 | { 26 | test: /\.node$/, 27 | use: 'node-loader' 28 | } 29 | ] 30 | }, 31 | node: { 32 | __dirname: process.env.NODE_ENV !== 'production', 33 | __filename: process.env.NODE_ENV !== 'production' 34 | }, 35 | output: { 36 | filename: '[name].js', 37 | libraryTarget: 'commonjs2', 38 | path: path.join(__dirname, '../dist/electron') 39 | }, 40 | plugins: [ 41 | new webpack.NoEmitOnErrorsPlugin() 42 | ], 43 | resolve: { 44 | extensions: ['.js', '.json', '.node'] 45 | }, 46 | target: 'electron-main' 47 | } 48 | 49 | /** 50 | * Adjust mainConfig for development settings 51 | */ 52 | if (process.env.NODE_ENV !== 'production') { 53 | mainConfig.plugins.push( 54 | new webpack.DefinePlugin({ 55 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 56 | }) 57 | ) 58 | } 59 | 60 | /** 61 | * Adjust mainConfig for production settings 62 | */ 63 | if (process.env.NODE_ENV === 'production') { 64 | mainConfig.plugins.push( 65 | new BabiliWebpackPlugin(), 66 | new webpack.DefinePlugin({ 67 | 'process.env.NODE_ENV': '"production"' 68 | }) 69 | ) 70 | } 71 | 72 | module.exports = mainConfig 73 | -------------------------------------------------------------------------------- /src/renderer/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jiachenpan on 16/11/18. 3 | */ 4 | 5 | export function parseTime(time, cFormat) { 6 | if (arguments.length === 0) { 7 | return null 8 | } 9 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 10 | let date 11 | if (typeof time === 'object') { 12 | date = time 13 | } else { 14 | if (('' + time).length === 10) time = parseInt(time) * 1000 15 | date = new Date(time) 16 | } 17 | const formatObj = { 18 | y: date.getFullYear(), 19 | m: date.getMonth() + 1, 20 | d: date.getDate(), 21 | h: date.getHours(), 22 | i: date.getMinutes(), 23 | s: date.getSeconds(), 24 | a: date.getDay() 25 | } 26 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 27 | let value = formatObj[key] 28 | if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1] 29 | if (result.length > 0 && value < 10) { 30 | value = '0' + value 31 | } 32 | return value || 0 33 | }) 34 | return time_str 35 | } 36 | 37 | export function formatTime(time, option) { 38 | time = +time * 1000 39 | const d = new Date(time) 40 | const now = Date.now() 41 | 42 | const diff = (now - d) / 1000 43 | 44 | if (diff < 30) { 45 | return '刚刚' 46 | } else if (diff < 3600) { // less 1 hour 47 | return Math.ceil(diff / 60) + '分钟前' 48 | } else if (diff < 3600 * 24) { 49 | return Math.ceil(diff / 3600) + '小时前' 50 | } else if (diff < 3600 * 24 * 2) { 51 | return '1天前' 52 | } 53 | if (option) { 54 | return parseTime(time, option) 55 | } else { 56 | return d.getMonth() + 1 + '月' + d.getDate() + '日' + d.getHours() + '时' + d.getMinutes() + '分' 57 | } 58 | } 59 | 60 | export function formatString(key, num) { 61 | if(!num) { 62 | num = 10 63 | } 64 | 65 | let label = key 66 | if(key.length > num) { 67 | label = key.substring(0, num)+'...' 68 | } 69 | 70 | return label 71 | } 72 | -------------------------------------------------------------------------------- /src/renderer/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 30 | 31 | 45 | -------------------------------------------------------------------------------- /src/renderer/components/JsonEditor/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 55 | 56 | 74 | -------------------------------------------------------------------------------- /src/renderer/utils/localStore.js: -------------------------------------------------------------------------------- 1 | import Store from 'electron-store' 2 | 3 | const store = new Store() 4 | const CONNECT_MAP = 'connectMap' 5 | const SEARCH_HISTORY = 'searchHistory' 6 | const AUTO_SEARCH = 'autoSearch' 7 | const AUTO_SEARCH_LIMIT = 'autoSearchLimit' 8 | const DB_ASSIGN = 'dbAssign' 9 | 10 | //cleanSearchHistory() 11 | export function getConnectMap() { 12 | const map = store.get(CONNECT_MAP) 13 | return map ? map : {} 14 | } 15 | 16 | export function setConnectMap(connectionMap) { 17 | return store.set(CONNECT_MAP, connectionMap) 18 | } 19 | 20 | export function cleanConnect() { 21 | return store.delete(CONNECT_MAP) 22 | } 23 | 24 | export function getSearchHistory(domain) { 25 | const list = store.get(SEARCH_HISTORY+'.'+domain) 26 | return list ? list : [] 27 | } 28 | 29 | export function addSearchHistory(domain, list) { 30 | return store.set(SEARCH_HISTORY+'.'+domain, list) 31 | } 32 | 33 | export function cleanSearchHistory() { 34 | return store.delete(SEARCH_HISTORY) 35 | } 36 | 37 | export function getAutoSearch() { 38 | return store.has(AUTO_SEARCH) ? store.get(AUTO_SEARCH) : false; 39 | } 40 | 41 | export function setAutoSearch(status) { 42 | return store.set(AUTO_SEARCH, status) 43 | } 44 | 45 | export function getAutoSearchLimit() { 46 | return store.has(AUTO_SEARCH_LIMIT) ? store.get(AUTO_SEARCH_LIMIT) : 10000; 47 | } 48 | 49 | export function setAutoSearchLimit(limit) { 50 | return store.set(AUTO_SEARCH_LIMIT, limit) 51 | } 52 | 53 | // export function getMlDb(connection) { 54 | // const map = store.get(ML_DB+'.'+connection); 55 | // return map ? map : {} 56 | // } 57 | // 58 | // export function setMlDb(connection, map) { 59 | // return store.set(ML_DB+'.'+connection, map) 60 | // } 61 | 62 | export function getDbAssign(connection) { 63 | const list = store.get(DB_ASSIGN+'.'+connection); 64 | return list ? list : [] 65 | } 66 | 67 | export function setDbAssign(connection, list) { 68 | return store.set(DB_ASSIGN+'.'+connection, list) 69 | } -------------------------------------------------------------------------------- /src/renderer/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 67 | 68 | 84 | -------------------------------------------------------------------------------- /src/renderer/icons/svg/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/views/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 67 | 68 | 94 | 95 | -------------------------------------------------------------------------------- /src/renderer/store/modules/connect.js: -------------------------------------------------------------------------------- 1 | import { getConnectMap, setConnectMap, cleanConnect, cleanSearchHistory } from '@/utils/localStore' 2 | 3 | const connect = { 4 | state: { 5 | connectMap: getConnectMap(), 6 | handler: null, 7 | selectedName: null, 8 | cacheOptions: ['Search history', 'Connections'] 9 | }, 10 | mutations: { 11 | SET_CONNECT_MAP: (state) => { 12 | state.connectMap = getConnectMap() 13 | }, 14 | SET_HANDLER: (state, handler) => { 15 | if(state.handler !== null && handler === null) { 16 | state.handler.disconnect() 17 | } 18 | state.handler = handler 19 | }, 20 | ADD_CONNECTION: (state, connection) => { 21 | const add = {} 22 | add[connection.connectionName] = connection 23 | state.connectMap = Object.assign({}, state.connectMap, add) 24 | setConnectMap(state.connectMap) 25 | }, 26 | EDIT_CONNECTION: (state, {name, connection}) => { 27 | const map = Object.assign({}, state.connectMap) 28 | delete map[name] 29 | const add = {} 30 | add[connection.connectionName] = connection 31 | state.connectMap = Object.assign({}, map, add) 32 | setConnectMap(state.connectMap) 33 | }, 34 | DELETE_CONNECTION: (state, name) => { 35 | const map = Object.assign({}, state.connectMap) 36 | delete map[name] 37 | state.connectMap = Object.assign({}, map) 38 | setConnectMap(state.connectMap) 39 | }, 40 | SET_SELECTED: (state, name) => { 41 | state.selectedName = name 42 | } 43 | }, 44 | actions: { 45 | AddConnect: ({ commit }, connection) => { 46 | commit('ADD_CONNECTION', connection) 47 | }, 48 | EditConnect: ({ commit }, { name, connection }) => { 49 | commit('EDIT_CONNECTION', {name, connection}) 50 | }, 51 | DeleteConnect: ({ commit }, name) => { 52 | commit('DELETE_CONNECTION', name) 53 | }, 54 | SetHandler: ({ commit }, { handler, name }) => { 55 | console.log(name) 56 | commit('SET_HANDLER', handler) 57 | commit('SET_SELECTED', name) 58 | }, 59 | CleanCache: ({ commit }, cache) => { 60 | commit('SET_SELECTED', null) 61 | commit('SET_HANDLER', null) 62 | if(cache.indexOf('Search history') > -1) { 63 | cleanSearchHistory() 64 | } 65 | if(cache.indexOf('Connections') > -1) { 66 | cleanConnect() 67 | } 68 | 69 | commit('SET_CONNECT_MAP') 70 | }, 71 | CloseHandler: ({ commit }) => { 72 | commit('SET_HANDLER', null) 73 | commit('SET_SELECTED', null) 74 | }, 75 | } 76 | } 77 | 78 | export default connect 79 | -------------------------------------------------------------------------------- /src/renderer/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | // in development-env not use lazy-loading, because lazy-loading too many pages will cause webpack hot update too slow. so only in production use lazy-loading; 5 | // detail: https://panjiachen.github.io/vue-element-admin-site/#/lazy-loading 6 | 7 | Vue.use(Router) 8 | 9 | /* Layout */ 10 | import Layout from '../views/layout/Layout' 11 | 12 | /** 13 | * hidden: true if `hidden:true` will not show in the sidebar(default is false) 14 | * alwaysShow: true if set true, will always show the root menu, whatever its child routes length 15 | * if not set alwaysShow, only more than one route under the children 16 | * it will becomes nested mode, otherwise not show the root menu 17 | * redirect: noredirect if `redirect:noredirect` will no redirct in the breadcrumb 18 | * name:'router-name' the name is used by (must set!!!) 19 | * meta : { 20 | title: 'title' the name show in submenu and breadcrumb (recommend set) 21 | icon: 'svg-name' the icon show in the sidebar, 22 | } 23 | **/ 24 | export const constantRouterMap = [ 25 | 26 | { 27 | path: '/', 28 | component: Layout, 29 | redirect: '/dashboard', 30 | name: 'Dashboard', 31 | hidden: true, 32 | children: [{ 33 | path: 'dashboard', 34 | component: () => import('@/views/dashboard/index') 35 | }] 36 | }, 37 | 38 | { 39 | path: '/connect', 40 | component: Layout, 41 | children: [ 42 | { 43 | path: '/form/new', 44 | name: 'ConnectNewForm', 45 | component: () => import('@/views/connect/form'), 46 | meta: { title: 'New Connect', icon: 'form' }, 47 | props: { editable: false } 48 | }, 49 | { 50 | path: '/form/edit/:name', 51 | name: 'ConnectEditForm', 52 | component: () => import('@/views/connect/form'), 53 | meta: { title: 'Edit Connect', icon: 'form' }, 54 | props: { editable: true } 55 | }, 56 | { 57 | path: '/db/:name', 58 | name: 'DB', 59 | component: () => import('@/views/connect/db'), 60 | meta: { title: 'DB', icon: 'form' } 61 | }, 62 | { 63 | path: '/keys/:db', 64 | name: 'Keys', 65 | component: () => import('@/views/connect/keys'), 66 | meta: { title: 'Keys', icon: 'form' } 67 | } 68 | ] 69 | }, 70 | 71 | { path: '*', redirect: '/404', hidden: true } 72 | ] 73 | 74 | export default new Router({ 75 | // mode: 'history', //后端支持可开 76 | scrollBehavior: () => ({ y: 0 }), 77 | routes: constantRouterMap 78 | }) 79 | 80 | -------------------------------------------------------------------------------- /src/renderer/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | // 主体区域 3 | .main-container { 4 | min-height: 100%; 5 | transition: margin-left .28s; 6 | margin-left: 180px; 7 | } 8 | // 侧边栏 9 | .sidebar-container { 10 | transition: width 0.28s; 11 | width: 180px !important; 12 | height: 100%; 13 | position: fixed; 14 | font-size: 0px; 15 | top: 0; 16 | bottom: 0; 17 | left: 0; 18 | z-index: 1001; 19 | overflow: hidden; 20 | //reset element-ui css 21 | .horizontal-collapse-transition { 22 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; 23 | } 24 | .scrollbar-wrapper { 25 | .el-scrollbar__view { 26 | height: 100%; 27 | } 28 | } 29 | .is-horizontal { 30 | display: none; 31 | } 32 | a { 33 | display: inline-block; 34 | width: 100%; 35 | overflow: hidden; 36 | } 37 | .svg-icon { 38 | margin-right: 16px; 39 | } 40 | .el-menu { 41 | border: none; 42 | height: 100%; 43 | width: 100% !important; 44 | } 45 | } 46 | .hideSidebar { 47 | .sidebar-container { 48 | width: 36px !important; 49 | } 50 | .main-container { 51 | margin-left: 36px; 52 | } 53 | .submenu-title-noDropdown { 54 | padding-left: 10px !important; 55 | position: relative; 56 | .el-tooltip { 57 | padding: 0 10px !important; 58 | } 59 | } 60 | .el-submenu { 61 | overflow: hidden; 62 | &>.el-submenu__title { 63 | padding-left: 10px !important; 64 | .el-submenu__icon-arrow { 65 | display: none; 66 | } 67 | } 68 | } 69 | .el-menu--collapse { 70 | .el-submenu { 71 | &>.el-submenu__title { 72 | &>span { 73 | height: 0; 74 | width: 0; 75 | overflow: hidden; 76 | visibility: hidden; 77 | display: inline-block; 78 | } 79 | } 80 | } 81 | } 82 | } 83 | .sidebar-container .nest-menu .el-submenu>.el-submenu__title, 84 | .sidebar-container .el-submenu .el-menu-item { 85 | min-width: 180px !important; 86 | background-color: $subMenuBg !important; 87 | &:hover { 88 | background-color: $menuHover !important; 89 | } 90 | } 91 | .el-menu--collapse .el-menu .el-submenu { 92 | min-width: 180px !important; 93 | } 94 | 95 | //适配移动端 96 | .mobile { 97 | .main-container { 98 | margin-left: 0px; 99 | } 100 | .sidebar-container { 101 | transition: transform .28s; 102 | width: 180px !important; 103 | } 104 | &.hideSidebar { 105 | .sidebar-container { 106 | transition-duration: 0.3s; 107 | transform: translate3d(-180px, 0, 0); 108 | } 109 | } 110 | } 111 | .withoutAnimation { 112 | .main-container, 113 | .sidebar-container { 114 | transition: none; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, Menu } from 'electron' 2 | 3 | /** 4 | * Set `__static` path to static files in production 5 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html 6 | */ 7 | if (process.env.NODE_ENV !== 'development') { 8 | global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\') 9 | } 10 | 11 | let mainWindow 12 | const winURL = process.env.NODE_ENV === 'development' 13 | ? `http://localhost:9080` 14 | : `file://${__dirname}/index.html` 15 | 16 | function createWindow () { 17 | /** 18 | * global version 19 | */ 20 | var pack = require("../../package.json") 21 | global.version = pack.version 22 | 23 | /** 24 | * Initial window options 25 | */ 26 | mainWindow = new BrowserWindow({ 27 | height: 600, 28 | useContentSize: true, 29 | width: 1000, 30 | minWidth: 860, 31 | minHeight: 500 32 | }) 33 | 34 | mainWindow.loadURL(winURL) 35 | 36 | mainWindow.on('closed', () => { 37 | mainWindow = null 38 | }) 39 | 40 | //console.log(Menu.getApplicationMenu()) 41 | // Create the Application's main menu 42 | if(process.env.NODE_ENV !== 'development') { 43 | const template = [{ 44 | label: "Application", 45 | submenu: [ 46 | {label: "About Application", selector: "orderFrontStandardAboutPanel:"}, 47 | {type: "separator"}, 48 | { 49 | label: "Quit", accelerator: "Command+Q", click: function () { 50 | app.quit(); 51 | } 52 | } 53 | ] 54 | }, { 55 | label: "Edit", 56 | submenu: [ 57 | {label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:"}, 58 | {label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:"}, 59 | {type: "separator"}, 60 | {label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:"}, 61 | {label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:"}, 62 | {label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:"}, 63 | {label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:"} 64 | ] 65 | } 66 | ]; 67 | 68 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)); 69 | } 70 | } 71 | 72 | app.on('ready', createWindow) 73 | 74 | app.on('window-all-closed', () => { 75 | if (process.platform !== 'darwin') { 76 | app.quit() 77 | } 78 | }) 79 | 80 | app.on('activate', () => { 81 | if (mainWindow === null) { 82 | createWindow() 83 | } 84 | }) 85 | 86 | /** 87 | * Auto Updater 88 | * 89 | * Uncomment the following code below and install `electron-updater` to 90 | * support auto updating. Code Signing with a valid certificate is required. 91 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating 92 | */ 93 | 94 | /* 95 | import { autoUpdater } from 'electron-updater' 96 | 97 | autoUpdater.on('update-downloaded', () => { 98 | autoUpdater.quitAndInstall() 99 | }) 100 | 101 | app.on('ready', () => { 102 | if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates() 103 | }) 104 | */ 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RedisCX", 3 | "version": "0.5.5", 4 | "author": "Sidfate", 5 | "description": "Redis client for any platform", 6 | "license": "MIT", 7 | "main": "./dist/electron/main.js", 8 | "scripts": { 9 | "build": "node .electron-vue/build.js && electron-builder", 10 | "build:dir": "node .electron-vue/build.js && electron-builder --dir", 11 | "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js", 12 | "build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js", 13 | "dev": "node .electron-vue/dev-runner.js", 14 | "pack": "npm run pack:main && npm run pack:renderer", 15 | "pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js", 16 | "pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js", 17 | "postinstall": "" 18 | }, 19 | "build": { 20 | "productName": "RedisCX", 21 | "appId": "org.simulatedgreg.electron-vue", 22 | "directories": { 23 | "output": "build" 24 | }, 25 | "files": [ 26 | "dist/electron/**/*" 27 | ], 28 | "dmg": { 29 | "contents": [ 30 | { 31 | "x": 410, 32 | "y": 150, 33 | "type": "link", 34 | "path": "/Applications" 35 | }, 36 | { 37 | "x": 130, 38 | "y": 150, 39 | "type": "file" 40 | } 41 | ] 42 | }, 43 | "mac": { 44 | "icon": "build/icons/icon.icns" 45 | }, 46 | "win": { 47 | "icon": "build/icons/icon.ico" 48 | }, 49 | "linux": { 50 | "icon": "build/icons" 51 | } 52 | }, 53 | "dependencies": { 54 | "axios": "^0.19.0", 55 | "electron-store": "^2.0.0", 56 | "element-ui": "^2.3.8", 57 | "ioredis": "^4.0.0", 58 | "js-cookie": "2.2.0", 59 | "lodash": "^4.17.10", 60 | "markdown-it": "^8.4.2", 61 | "md5": "^2.2.1", 62 | "normalize.css": "7.0.0", 63 | "nprogress": "0.2.0", 64 | "vue": "^2.3.3", 65 | "vue-electron": "^1.0.6", 66 | "vue-router": "^2.5.3", 67 | "vuex": "^2.3.1" 68 | }, 69 | "devDependencies": { 70 | "babel-core": "^6.25.0", 71 | "babel-loader": "^7.1.1", 72 | "babel-plugin-transform-runtime": "^6.23.0", 73 | "babel-preset-env": "^1.6.0", 74 | "babel-preset-stage-0": "^6.24.1", 75 | "babel-register": "^6.24.1", 76 | "babili-webpack-plugin": "^0.1.2", 77 | "cfonts": "^1.1.3", 78 | "chalk": "^2.1.0", 79 | "codemirror": "^5.38.0", 80 | "copy-webpack-plugin": "^4.0.1", 81 | "cross-env": "^5.0.5", 82 | "css-loader": "^0.28.4", 83 | "del": "^3.0.0", 84 | "devtron": "^1.4.0", 85 | "electron": "^1.7.5", 86 | "electron-builder": "^19.19.1", 87 | "electron-debug": "^1.4.0", 88 | "electron-devtools-installer": "^2.2.0", 89 | "extract-text-webpack-plugin": "^3.0.0", 90 | "file-loader": "^0.11.2", 91 | "html-webpack-plugin": "^2.30.1", 92 | "jsonlint": "^1.6.3", 93 | "multispinner": "^0.2.1", 94 | "node-loader": "^0.6.0", 95 | "node-sass": "^4.7.2", 96 | "sass-loader": "6.0.6", 97 | "script-loader": "^0.7.2", 98 | "style-loader": "^0.18.2", 99 | "svg-sprite-loader": "^3.8.0", 100 | "url-loader": "^0.5.9", 101 | "vue-html-loader": "^1.2.4", 102 | "vue-loader": "^13.0.5", 103 | "vue-style-loader": "^3.0.1", 104 | "vue-template-compiler": "^2.4.2", 105 | "webpack": "^3.5.2", 106 | "webpack-dev-server": "^2.7.1", 107 | "webpack-hot-middleware": "^2.18.2" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /.electron-vue/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | const { say } = require('cfonts') 6 | const chalk = require('chalk') 7 | const del = require('del') 8 | const { spawn } = require('child_process') 9 | const webpack = require('webpack') 10 | const Multispinner = require('multispinner') 11 | 12 | 13 | const mainConfig = require('./webpack.main.config') 14 | const rendererConfig = require('./webpack.renderer.config') 15 | const webConfig = require('./webpack.web.config') 16 | 17 | const doneLog = chalk.bgGreen.white(' DONE ') + ' ' 18 | const errorLog = chalk.bgRed.white(' ERROR ') + ' ' 19 | const okayLog = chalk.bgBlue.white(' OKAY ') + ' ' 20 | const isCI = process.env.CI || false 21 | 22 | if (process.env.BUILD_TARGET === 'clean') clean() 23 | else if (process.env.BUILD_TARGET === 'web') web() 24 | else build() 25 | 26 | function clean () { 27 | del.sync(['build/*', '!build/icons', '!build/icons/icon.*']) 28 | console.log(`\n${doneLog}\n`) 29 | process.exit() 30 | } 31 | 32 | function build () { 33 | greeting() 34 | 35 | del.sync(['dist/electron/*', '!.gitkeep']) 36 | 37 | const tasks = ['main', 'renderer'] 38 | const m = new Multispinner(tasks, { 39 | preText: 'building', 40 | postText: 'process' 41 | }) 42 | 43 | let results = '' 44 | 45 | m.on('success', () => { 46 | process.stdout.write('\x1B[2J\x1B[0f') 47 | console.log(`\n\n${results}`) 48 | console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`) 49 | process.exit() 50 | }) 51 | 52 | pack(mainConfig).then(result => { 53 | results += result + '\n\n' 54 | m.success('main') 55 | }).catch(err => { 56 | m.error('main') 57 | console.log(`\n ${errorLog}failed to build main process`) 58 | console.error(`\n${err}\n`) 59 | process.exit(1) 60 | }) 61 | 62 | pack(rendererConfig).then(result => { 63 | results += result + '\n\n' 64 | m.success('renderer') 65 | }).catch(err => { 66 | m.error('renderer') 67 | console.log(`\n ${errorLog}failed to build renderer process`) 68 | console.error(`\n${err}\n`) 69 | process.exit(1) 70 | }) 71 | } 72 | 73 | function pack (config) { 74 | return new Promise((resolve, reject) => { 75 | webpack(config, (err, stats) => { 76 | if (err) reject(err.stack || err) 77 | else if (stats.hasErrors()) { 78 | let err = '' 79 | 80 | stats.toString({ 81 | chunks: false, 82 | colors: true 83 | }) 84 | .split(/\r?\n/) 85 | .forEach(line => { 86 | err += ` ${line}\n` 87 | }) 88 | 89 | reject(err) 90 | } else { 91 | resolve(stats.toString({ 92 | chunks: false, 93 | colors: true 94 | })) 95 | } 96 | }) 97 | }) 98 | } 99 | 100 | function web () { 101 | del.sync(['dist/web/*', '!.gitkeep']) 102 | webpack(webConfig, (err, stats) => { 103 | if (err || stats.hasErrors()) console.log(err) 104 | 105 | console.log(stats.toString({ 106 | chunks: false, 107 | colors: true 108 | })) 109 | 110 | process.exit() 111 | }) 112 | } 113 | 114 | function greeting () { 115 | const cols = process.stdout.columns 116 | let text = '' 117 | 118 | if (cols > 85) text = 'lets-build' 119 | else if (cols > 60) text = 'lets-|build' 120 | else text = false 121 | 122 | if (text && !isCI) { 123 | say(text, { 124 | colors: ['yellow'], 125 | font: 'simple3d', 126 | space: false 127 | }) 128 | } else console.log(chalk.yellow.bold('\n lets-build')) 129 | console.log() 130 | } 131 | -------------------------------------------------------------------------------- /.electron-vue/webpack.web.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'web' 4 | 5 | const path = require('path') 6 | const webpack = require('webpack') 7 | 8 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 9 | const CopyWebpackPlugin = require('copy-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const HtmlWebpackPlugin = require('html-webpack-plugin') 12 | 13 | let webConfig = { 14 | devtool: '#cheap-module-eval-source-map', 15 | entry: { 16 | web: path.join(__dirname, '../src/renderer/main.js') 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.css$/, 22 | use: ExtractTextPlugin.extract({ 23 | fallback: 'style-loader', 24 | use: 'css-loader' 25 | }) 26 | }, 27 | { 28 | test: /\.html$/, 29 | use: 'vue-html-loader' 30 | }, 31 | { 32 | test: /\.js$/, 33 | use: 'babel-loader', 34 | include: [ path.resolve(__dirname, '../src/renderer') ], 35 | exclude: /node_modules/ 36 | }, 37 | { 38 | test: /\.vue$/, 39 | use: { 40 | loader: 'vue-loader', 41 | options: { 42 | extractCSS: true, 43 | loaders: { 44 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 45 | scss: 'vue-style-loader!css-loader!sass-loader' 46 | } 47 | } 48 | } 49 | }, 50 | { 51 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 52 | use: { 53 | loader: 'url-loader', 54 | query: { 55 | limit: 10000, 56 | name: 'imgs/[name].[ext]' 57 | } 58 | } 59 | }, 60 | { 61 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 62 | use: { 63 | loader: 'url-loader', 64 | query: { 65 | limit: 10000, 66 | name: 'fonts/[name].[ext]' 67 | } 68 | } 69 | } 70 | ] 71 | }, 72 | plugins: [ 73 | new ExtractTextPlugin('styles.css'), 74 | new HtmlWebpackPlugin({ 75 | filename: 'index.html', 76 | template: path.resolve(__dirname, '../src/index.ejs'), 77 | minify: { 78 | collapseWhitespace: true, 79 | removeAttributeQuotes: true, 80 | removeComments: true 81 | }, 82 | nodeModules: false 83 | }), 84 | new webpack.DefinePlugin({ 85 | 'process.env.IS_WEB': 'true' 86 | }), 87 | new webpack.HotModuleReplacementPlugin(), 88 | new webpack.NoEmitOnErrorsPlugin() 89 | ], 90 | output: { 91 | filename: '[name].js', 92 | path: path.join(__dirname, '../dist/web') 93 | }, 94 | resolve: { 95 | alias: { 96 | '@': path.join(__dirname, '../src/renderer'), 97 | 'vue$': 'vue/dist/vue.esm.js' 98 | }, 99 | extensions: ['.js', '.vue', '.json', '.css'] 100 | }, 101 | target: 'web' 102 | } 103 | 104 | /** 105 | * Adjust webConfig for production settings 106 | */ 107 | if (process.env.NODE_ENV === 'production') { 108 | webConfig.devtool = '' 109 | 110 | webConfig.plugins.push( 111 | new BabiliWebpackPlugin(), 112 | new CopyWebpackPlugin([ 113 | { 114 | from: path.join(__dirname, '../static'), 115 | to: path.join(__dirname, '../dist/web/static'), 116 | ignore: ['.*'] 117 | } 118 | ]), 119 | new webpack.DefinePlugin({ 120 | 'process.env.NODE_ENV': '"production"' 121 | }), 122 | new webpack.LoaderOptionsPlugin({ 123 | minimize: true 124 | }) 125 | ) 126 | } 127 | 128 | module.exports = webConfig 129 | -------------------------------------------------------------------------------- /src/renderer/views/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 98 | 99 | 106 | -------------------------------------------------------------------------------- /src/renderer/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 49 | 50 | 112 | -------------------------------------------------------------------------------- /src/renderer/views/connect/form.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 125 | 126 | -------------------------------------------------------------------------------- /.electron-vue/dev-runner.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chalk = require('chalk') 4 | const electron = require('electron') 5 | const path = require('path') 6 | const { say } = require('cfonts') 7 | const { spawn } = require('child_process') 8 | const webpack = require('webpack') 9 | const WebpackDevServer = require('webpack-dev-server') 10 | const webpackHotMiddleware = require('webpack-hot-middleware') 11 | 12 | const mainConfig = require('./webpack.main.config') 13 | const rendererConfig = require('./webpack.renderer.config') 14 | 15 | let electronProcess = null 16 | let manualRestart = false 17 | let hotMiddleware 18 | 19 | function logStats (proc, data) { 20 | let log = '' 21 | 22 | log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`) 23 | log += '\n\n' 24 | 25 | if (typeof data === 'object') { 26 | data.toString({ 27 | colors: true, 28 | chunks: false 29 | }).split(/\r?\n/).forEach(line => { 30 | log += ' ' + line + '\n' 31 | }) 32 | } else { 33 | log += ` ${data}\n` 34 | } 35 | 36 | log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n' 37 | 38 | console.log(log) 39 | } 40 | 41 | function startRenderer () { 42 | return new Promise((resolve, reject) => { 43 | rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer) 44 | 45 | const compiler = webpack(rendererConfig) 46 | hotMiddleware = webpackHotMiddleware(compiler, { 47 | log: false, 48 | heartbeat: 2500 49 | }) 50 | 51 | compiler.plugin('compilation', compilation => { 52 | compilation.plugin('html-webpack-plugin-after-emit', (data, cb) => { 53 | hotMiddleware.publish({ action: 'reload' }) 54 | cb() 55 | }) 56 | }) 57 | 58 | compiler.plugin('done', stats => { 59 | logStats('Renderer', stats) 60 | }) 61 | 62 | const server = new WebpackDevServer( 63 | compiler, 64 | { 65 | contentBase: path.join(__dirname, '../'), 66 | quiet: true, 67 | before (app, ctx) { 68 | app.use(hotMiddleware) 69 | ctx.middleware.waitUntilValid(() => { 70 | resolve() 71 | }) 72 | } 73 | } 74 | ) 75 | 76 | server.listen(9080) 77 | }) 78 | } 79 | 80 | function startMain () { 81 | return new Promise((resolve, reject) => { 82 | mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main) 83 | 84 | const compiler = webpack(mainConfig) 85 | 86 | compiler.plugin('watch-run', (compilation, done) => { 87 | logStats('Main', chalk.white.bold('compiling...')) 88 | hotMiddleware.publish({ action: 'compiling' }) 89 | done() 90 | }) 91 | 92 | compiler.watch({}, (err, stats) => { 93 | if (err) { 94 | console.log(err) 95 | return 96 | } 97 | 98 | logStats('Main', stats) 99 | 100 | if (electronProcess && electronProcess.kill) { 101 | manualRestart = true 102 | process.kill(electronProcess.pid) 103 | electronProcess = null 104 | startElectron() 105 | 106 | setTimeout(() => { 107 | manualRestart = false 108 | }, 5000) 109 | } 110 | 111 | resolve() 112 | }) 113 | }) 114 | } 115 | 116 | function startElectron () { 117 | electronProcess = spawn(electron, ['--inspect=5858', path.join(__dirname, '../dist/electron/main.js')]) 118 | 119 | electronProcess.stdout.on('data', data => { 120 | electronLog(data, 'blue') 121 | }) 122 | electronProcess.stderr.on('data', data => { 123 | electronLog(data, 'red') 124 | }) 125 | 126 | electronProcess.on('close', () => { 127 | if (!manualRestart) process.exit() 128 | }) 129 | } 130 | 131 | function electronLog (data, color) { 132 | let log = '' 133 | data = data.toString().split(/\r?\n/) 134 | data.forEach(line => { 135 | log += ` ${line}\n` 136 | }) 137 | if (/[0-9A-z]+/.test(log)) { 138 | console.log( 139 | chalk[color].bold('┏ Electron -------------------') + 140 | '\n\n' + 141 | log + 142 | chalk[color].bold('┗ ----------------------------') + 143 | '\n' 144 | ) 145 | } 146 | } 147 | 148 | function greeting () { 149 | const cols = process.stdout.columns 150 | let text = '' 151 | 152 | if (cols > 104) text = 'electron-vue' 153 | else if (cols > 76) text = 'electron-|vue' 154 | else text = false 155 | 156 | if (text) { 157 | say(text, { 158 | colors: ['yellow'], 159 | font: 'simple3d', 160 | space: false 161 | }) 162 | } else console.log(chalk.yellow.bold('\n electron-vue')) 163 | console.log(chalk.blue(' getting ready...') + '\n') 164 | } 165 | 166 | function init () { 167 | greeting() 168 | 169 | Promise.all([startRenderer(), startMain()]) 170 | .then(() => { 171 | startElectron() 172 | }) 173 | .catch(err => { 174 | console.error(err) 175 | }) 176 | } 177 | 178 | init() 179 | -------------------------------------------------------------------------------- /.electron-vue/webpack.renderer.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'renderer' 4 | 5 | const path = require('path') 6 | const { dependencies } = require('../package.json') 7 | const webpack = require('webpack') 8 | 9 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 10 | const CopyWebpackPlugin = require('copy-webpack-plugin') 11 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 12 | const HtmlWebpackPlugin = require('html-webpack-plugin') 13 | 14 | /** 15 | * List of node_modules to include in webpack bundle 16 | * 17 | * Required for specific packages like Vue UI libraries 18 | * that provide pure *.vue files that need compiling 19 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals 20 | */ 21 | let whiteListedModules = ['vue', 'element-ui'] 22 | 23 | let rendererConfig = { 24 | devtool: '#cheap-module-eval-source-map', 25 | entry: { 26 | renderer: path.join(__dirname, '../src/renderer/main.js') 27 | }, 28 | externals: [ 29 | ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)) 30 | ], 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.css$/, 35 | use: ExtractTextPlugin.extract({ 36 | fallback: 'style-loader', 37 | use: 'css-loader' 38 | }) 39 | }, 40 | { 41 | test: /\.(sass|scss)$/, 42 | loader:"style-loader!css-loader!sass-loader" 43 | }, 44 | { 45 | test: /\.svg$/, 46 | loader: 'svg-sprite-loader', 47 | include: [path.join(__dirname, '../src/renderer/icons')], 48 | options: { 49 | symbolId: 'icon-[name]' 50 | } 51 | }, 52 | { 53 | test: /\.html$/, 54 | use: 'vue-html-loader' 55 | }, 56 | { 57 | test: /\.js$/, 58 | use: 'babel-loader', 59 | exclude: /node_modules/ 60 | }, 61 | { 62 | test: /\.node$/, 63 | use: 'node-loader' 64 | }, 65 | { 66 | test: /\.vue$/, 67 | use: { 68 | loader: 'vue-loader', 69 | options: { 70 | extractCSS: process.env.NODE_ENV === 'production', 71 | loaders: { 72 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 73 | scss: 'vue-style-loader!css-loader!sass-loader' 74 | } 75 | } 76 | } 77 | }, 78 | { 79 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 80 | exclude: [ 81 | path.join(__dirname, '../src/renderer/icons'), 82 | ], 83 | use: { 84 | loader: 'url-loader', 85 | query: { 86 | limit: 10000, 87 | name: 'imgs/[name]--[folder].[ext]' 88 | } 89 | } 90 | }, 91 | { 92 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 93 | loader: 'url-loader', 94 | options: { 95 | limit: 10000, 96 | name: 'media/[name]--[folder].[ext]' 97 | } 98 | }, 99 | { 100 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 101 | use: { 102 | loader: 'url-loader', 103 | query: { 104 | limit: 10000, 105 | name: 'fonts/[name]--[folder].[ext]' 106 | } 107 | } 108 | } 109 | ] 110 | }, 111 | node: { 112 | __dirname: process.env.NODE_ENV !== 'production', 113 | __filename: process.env.NODE_ENV !== 'production' 114 | }, 115 | plugins: [ 116 | new ExtractTextPlugin('styles.css'), 117 | new HtmlWebpackPlugin({ 118 | filename: 'index.html', 119 | template: path.resolve(__dirname, '../src/index.ejs'), 120 | minify: { 121 | collapseWhitespace: true, 122 | removeAttributeQuotes: true, 123 | removeComments: true 124 | }, 125 | nodeModules: process.env.NODE_ENV !== 'production' 126 | ? path.resolve(__dirname, '../node_modules') 127 | : false 128 | }), 129 | new webpack.HotModuleReplacementPlugin(), 130 | new webpack.NoEmitOnErrorsPlugin() 131 | ], 132 | output: { 133 | filename: '[name].js', 134 | libraryTarget: 'commonjs2', 135 | path: path.join(__dirname, '../dist/electron') 136 | }, 137 | resolve: { 138 | alias: { 139 | '@': path.join(__dirname, '../src/renderer'), 140 | 'vue$': 'vue/dist/vue.esm.js' 141 | }, 142 | extensions: ['.js', '.vue', '.json', '.css', '.node'] 143 | }, 144 | target: 'electron-renderer' 145 | } 146 | 147 | /** 148 | * Adjust rendererConfig for development settings 149 | */ 150 | if (process.env.NODE_ENV !== 'production') { 151 | rendererConfig.plugins.push( 152 | new webpack.DefinePlugin({ 153 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 154 | }) 155 | ) 156 | } 157 | 158 | /** 159 | * Adjust rendererConfig for production settings 160 | */ 161 | if (process.env.NODE_ENV === 'production') { 162 | rendererConfig.devtool = '' 163 | 164 | rendererConfig.plugins.push( 165 | new BabiliWebpackPlugin(), 166 | new CopyWebpackPlugin([ 167 | { 168 | from: path.join(__dirname, '../static'), 169 | to: path.join(__dirname, '../dist/electron/static'), 170 | ignore: ['.*'] 171 | } 172 | ]), 173 | new webpack.DefinePlugin({ 174 | 'process.env.NODE_ENV': '"production"' 175 | }), 176 | new webpack.LoaderOptionsPlugin({ 177 | minimize: true 178 | }) 179 | ) 180 | } 181 | 182 | module.exports = rendererConfig 183 | -------------------------------------------------------------------------------- /src/renderer/views/layout/components/SettingModal.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 154 | 155 | -------------------------------------------------------------------------------- /src/renderer/views/connect/components/KeyTap.vue: -------------------------------------------------------------------------------- 1 | 116 | 117 | 333 | 334 | 391 | -------------------------------------------------------------------------------- /src/renderer/views/connect/db.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 379 | 380 | -------------------------------------------------------------------------------- /src/renderer/views/connect/keys.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 493 | 494 | 499 | -------------------------------------------------------------------------------- /src/renderer/static/font-awesome/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} 5 | --------------------------------------------------------------------------------