├── 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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
33 |
34 |
37 |
--------------------------------------------------------------------------------
/src/renderer/views/layout/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
16 |
17 |
18 |
35 |
--------------------------------------------------------------------------------
/src/renderer/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
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 [](https://996.icu)
2 |
3 | 
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 | 
28 |
29 | 
30 |
31 | 
32 |
33 | 
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 |
2 |
3 |
12 |
13 |
14 |
15 |
30 |
31 |
45 |
--------------------------------------------------------------------------------
/src/renderer/components/JsonEditor/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 | {{ item.title }}
6 |
7 |
8 |
9 |
10 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
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 |
2 |
19 |
20 |
21 |
98 |
99 |
106 |
--------------------------------------------------------------------------------
/src/renderer/views/dashboard/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |

6 |
7 |
8 |
9 |
17 |
18 |
19 |
20 | RedisCX
21 | A cross-platform redis gui client created by Sidfate.
22 |
23 |
24 |
25 |
26 |
27 |
49 |
50 |
112 |
--------------------------------------------------------------------------------
/src/renderer/views/connect/form.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Save
18 | Test Connection
19 | Reset
20 |
21 |
22 |
23 |
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 |
2 |
3 |
4 |
5 | Auto Search
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Save
22 |
23 |
24 |
25 |
26 | Cache
27 |
28 |
29 |
All
30 |
31 |
32 | {{option}}
33 |
34 |
35 | Clean Cache
36 |
37 |
38 | About
39 |
40 |
41 |

42 |
43 |
44 |
RedisCX
45 |
{{version}}
46 |
47 |
48 |
54 | Check for update
55 |
56 |
57 |
58 |
59 |
60 |
154 |
155 |
--------------------------------------------------------------------------------
/src/renderer/views/connect/components/KeyTap.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
KEY
8 |
9 | {{ item.key | getKeyLabel }}
10 |
11 |
12 |
13 |
TYPE
14 |
15 | {{ item.type.toUpperCase() }}
16 |
17 |
18 |
19 |
TTL
20 |
21 | {{ item.ttl }}
22 |
23 |
24 |
25 |
26 |
27 | Refresh
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Save
36 |
37 |
38 |
39 |
Delete
40 |
Create new element
41 |
42 |
43 |
48 |
49 |
50 |
51 |
52 |
60 |
63 |
64 |
65 |
66 | {{ scope.$index+1 }}
67 |
68 |
69 |
70 |
71 |
72 | {{ scope.row.key }}
73 |
74 |
75 |
76 |
77 |
78 |
79 | {{ scope.row.score }}
80 |
81 |
82 |
83 |
84 |
85 | {{ scope.row.value }}
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
113 |
114 |
115 |
116 |
117 |
333 |
334 |
391 |
--------------------------------------------------------------------------------
/src/renderer/views/connect/db.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
Flush
17 |
18 |
19 |
27 |
28 |
29 |
30 |
31 |
32 |
42 |
45 |
46 |
51 |
52 | {{ scope.row.db }}
53 |
54 |
55 |
56 |
60 |
61 |
64 |
65 |
73 | {{tag}}
74 |
75 |
76 |
77 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
379 |
380 |
--------------------------------------------------------------------------------
/src/renderer/views/connect/keys.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Keys
6 |
7 |
10 |
11 |
Delete
12 |
Create a new key
13 |
14 |
15 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
42 |
45 |
46 |
47 |
48 | {{scope.row}}
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
84 |
85 |
89 |
90 |
91 |
92 |
96 |
97 |
98 |
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 |
--------------------------------------------------------------------------------