The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .editorconfig
├── .eslintrc.json
├── .gitattributes
├── .github
    ├── FUNDING.yml
    ├── ISSUE_TEMPLATE.md
    └── workflows
    │   ├── build_linux.yml
    │   ├── build_mac.yml
    │   ├── build_windows.yml
    │   ├── codeql-analysis.yml.bak
    │   ├── gen_sponsors.yaml
    │   └── publish_winget.yml
├── .gitignore
├── .jshintrc
├── .postcssrc.js
├── LICENSE
├── PRIVACY.md
├── README.md
├── README.zh-CN.md
├── SECURITY.md
├── babel.config.json
├── build
    ├── build.js
    ├── check-versions.js
    ├── utils.js
    ├── vue-loader.conf.js
    ├── webpack.base.conf.js
    ├── webpack.dev.conf.js
    └── webpack.prod.conf.js
├── config
    ├── dev.env.js
    ├── index.js
    └── prod.env.js
├── element-variables.scss
├── index.html
├── pack
    ├── electron
    │   ├── electron-main.js
    │   ├── font-manager.js
    │   ├── icons
    │   │   ├── icon.icns
    │   │   ├── icon.ico
    │   │   └── icon.png
    │   ├── package.json
    │   ├── update.js
    │   └── win-state.js
    └── scripts
    │   └── notarize.js
├── package-lock.json
├── package.json
├── src
    ├── App.vue
    ├── Aside.vue
    ├── addon.js
    ├── assets
    │   ├── custom_tree.png
    │   ├── key_tree_toggle.png
    │   └── logo.png
    ├── bus.js
    ├── commands.js
    ├── components
    │   ├── CliContent.vue
    │   ├── CliTab.vue
    │   ├── CommandLog.vue
    │   ├── ConnectionMenu.vue
    │   ├── ConnectionWrapper.vue
    │   ├── Connections.vue
    │   ├── CustomFormatter.vue
    │   ├── DeleteBatch.vue
    │   ├── FileInput.vue
    │   ├── FormatViewer.vue
    │   ├── HotKeys.vue
    │   ├── InputBinary.vue
    │   ├── InputPassword.vue
    │   ├── JsonEditor.vue
    │   ├── KeyDetail.vue
    │   ├── KeyHeader.vue
    │   ├── KeyList.vue
    │   ├── KeyListNormal.vue
    │   ├── KeyListVirtualTree.vue
    │   ├── LanguageSelector.vue
    │   ├── MemoryAnalysis.vue
    │   ├── NewConnectionDialog.vue
    │   ├── OperateItem.vue
    │   ├── PaginationTable.vue
    │   ├── RightClickMenu.vue
    │   ├── ScrollToTop.vue
    │   ├── Setting.vue
    │   ├── SlowLog.vue
    │   ├── Status.vue
    │   ├── Tabs.vue
    │   ├── UpdateCheck.vue
    │   ├── contents
    │   │   ├── KeyContentHash.vue
    │   │   ├── KeyContentList.vue
    │   │   ├── KeyContentReJson.vue
    │   │   ├── KeyContentSet.vue
    │   │   ├── KeyContentStream.vue
    │   │   ├── KeyContentString.vue
    │   │   └── KeyContentZset.vue
    │   └── viewers
    │   │   ├── ViewerBinary.vue
    │   │   ├── ViewerBrotli.vue
    │   │   ├── ViewerCustom.vue
    │   │   ├── ViewerDeflate.vue
    │   │   ├── ViewerDeflateRaw.vue
    │   │   ├── ViewerGzip.vue
    │   │   ├── ViewerHex.vue
    │   │   ├── ViewerJavaSerialize.vue
    │   │   ├── ViewerJson.vue
    │   │   ├── ViewerMsgpack.vue
    │   │   ├── ViewerOverSize.vue
    │   │   ├── ViewerPHPSerialize.vue
    │   │   ├── ViewerPickle.vue
    │   │   ├── ViewerProtobuf.vue
    │   │   └── ViewerText.vue
    ├── i18n
    │   ├── i18n.js
    │   └── langs
    │   │   ├── cn.js
    │   │   ├── de.js
    │   │   ├── en.js
    │   │   ├── es.js
    │   │   ├── fr.js
    │   │   ├── it.js
    │   │   ├── ko.js
    │   │   ├── pt.js
    │   │   ├── ru.js
    │   │   ├── tr.js
    │   │   ├── tw.js
    │   │   ├── ua.js
    │   │   └── vi.js
    ├── main.js
    ├── redisClient.js
    ├── router
    │   └── index.js
    ├── shortcut.js
    ├── storage.js
    └── util.js
└── static
    ├── .gitkeep
    └── theme
        ├── dark
            ├── fonts
            │   ├── element-icons.ttf
            │   └── element-icons.woff
            └── index.css
        └── light
            ├── fonts
                ├── element-icons.ttf
                └── element-icons.woff
            └── index.css


/.editorconfig:
--------------------------------------------------------------------------------
 1 | root = true
 2 | 
 3 | [*]
 4 | charset = utf-8
 5 | indent_style = space
 6 | indent_size = 2
 7 | end_of_line = lf
 8 | insert_final_newline = true
 9 | trim_trailing_whitespace = true
10 | 
11 | [*.js]
12 | quote_type = single
13 | 
14 | [*.{html,less,css,json}]
15 | quote_type = double


--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
 1 | {
 2 |     "env": {
 3 |         "browser": true,
 4 |         "es6": true
 5 |     },
 6 |     "extends": ["eslint:recommended", "airbnb-base", "plugin:vue/essential"],
 7 |     "globals": {
 8 |         "Atomics": "readonly",
 9 |         "SharedArrayBuffer": "readonly"
10 |     },
11 |     "parserOptions": {
12 |         "ecmaVersion": 2018
13 |     },
14 |     "plugins": [
15 |         "vue"
16 |     ],
17 |     "rules": {
18 |     }
19 | }


--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.vue linguist-language=JavaScript
2 | 


--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
 1 | # These are supported funding model platforms
 2 | 
 3 | github: qishibo
 4 | patreon: # Replace with a single Patreon username
 5 | open_collective: AnotherRedisDesktopManager
 6 | ko_fi: # Replace with a single Ko-fi username
 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
 9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: https://cdn.jsdelivr.net/gh/qishibo/img/wechat.jpeg
13 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
 1 | ## OS
 2 | 
 3 | Windows or Linux or Mac
 4 | 
 5 | ## VERSION
 6 | 
 7 | Version in settings
 8 | 
 9 | 
10 | ## ISSUE DESCRIPTION
11 | 
12 | Bug reproduction process and configuration screenshot if possible
13 | 


--------------------------------------------------------------------------------
/.github/workflows/build_linux.yml:
--------------------------------------------------------------------------------
 1 | name: build_linux
 2 | 
 3 | on:
 4 |   release:
 5 |     types: [published]
 6 | 
 7 | jobs:
 8 |   build:
 9 | 
10 |     runs-on: ubuntu-latest
11 | 
12 |     strategy:
13 |       matrix:
14 |         node-version: [14.x]
15 | 
16 |     steps:
17 |     - uses: actions/checkout@v2
18 |     - name: Use Node.js ${{ matrix.node-version }}
19 |       uses: actions/setup-node@v1
20 |       with:
21 |         node-version: ${{ matrix.node-version }}
22 |     - run: npm ci
23 |     - run: npm run pack:prepare
24 |     - run: npm run pack:linux:publish
25 |       env:
26 |         GH_TOKEN: ${{secrets.GH_TOKEN}}
27 | 


--------------------------------------------------------------------------------
/.github/workflows/build_mac.yml:
--------------------------------------------------------------------------------
 1 | name: build_mac
 2 | 
 3 | on:
 4 |   release:
 5 |     types: [published]
 6 | 
 7 | jobs:
 8 |   build:
 9 | 
10 |     runs-on: macos-latest
11 | 
12 |     env:
13 |       GH_TOKEN: ${{secrets.GH_TOKEN}}
14 |       CSC_LINK: ${{secrets.CSC_LINK}}
15 |       CSC_KEY_PASSWORD: ${{secrets.CSC_KEY_PASSWORD}}
16 |       APPLEID: ${{secrets.APPLEID}}
17 |       APPLEID_PASSWORD: ${{secrets.APPLEID_PASSWORD}}
18 | 
19 |     steps:
20 |     - name: Import signing keychain
21 |       uses: apple-actions/import-codesign-certs@v2
22 |       with:
23 |         keychain: signing_temp
24 |         p12-file-base64: ${{secrets.CSC_LINK}}
25 |         p12-password: ${{secrets.CSC_KEY_PASSWORD}}
26 | 
27 |     - uses: actions/checkout@v4
28 |     - name: Use Node.js
29 |       uses: actions/setup-node@v4
30 |       with:
31 |         node-version: 16
32 |     - run: npm ci
33 |     - run: npm run pack:prepare
34 |     # - run: npm run pack:macm1:publish
35 |     - run: npm run pack:mac:publish
36 | 
37 |     - name: Cleanup keychain
38 |       if: always()
39 |       shell: bash
40 |       run: |
41 |         # Don't fail if the keychain doesn't exist.
42 |         security delete-keychain signing_temp.keychain || true
43 | 


--------------------------------------------------------------------------------
/.github/workflows/build_windows.yml:
--------------------------------------------------------------------------------
 1 | name: build_windows
 2 | 
 3 | on:
 4 |   release:
 5 |     types: [published]
 6 | 
 7 | jobs:
 8 |   build:
 9 | 
10 |     runs-on: windows-latest
11 | 
12 |     strategy:
13 |       matrix:
14 |         node-version: [14.x]
15 | 
16 |     steps:
17 |     - uses: actions/checkout@v2
18 |     - name: Use Node.js ${{ matrix.node-version }}
19 |       uses: actions/setup-node@v1
20 |       with:
21 |         node-version: ${{ matrix.node-version }}
22 |     - run: npm ci
23 |     - run: npm run pack:prepare
24 |     - run: npm run pack:win:publish
25 |       env:
26 |         GH_TOKEN: ${{secrets.GH_TOKEN}}
27 | 


--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml.bak:
--------------------------------------------------------------------------------
 1 | # For most projects, this workflow file will not need changing; you simply need
 2 | # to commit it to your repository.
 3 | #
 4 | # You may wish to alter this file to override the set of languages analyzed,
 5 | # or to provide custom queries or build logic.
 6 | #
 7 | # ******** NOTE ********
 8 | # We have attempted to detect the languages in your repository. Please check
 9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 | 
14 | on:
15 |   push:
16 |     branches: [ master ]
17 |   pull_request:
18 |     # The branches below must be a subset of the branches above
19 |     branches: [ master ]
20 |   schedule:
21 |     - cron: '15 20 * * 6'
22 | 
23 | jobs:
24 |   analyze:
25 |     name: Analyze
26 |     runs-on: ubuntu-latest
27 |     permissions:
28 |       actions: read
29 |       contents: read
30 |       security-events: write
31 | 
32 |     strategy:
33 |       fail-fast: false
34 |       matrix:
35 |         language: [ 'javascript' ]
36 |         # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 |         # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 | 
39 |     steps:
40 |     - name: Checkout repository
41 |       uses: actions/checkout@v2
42 | 
43 |     # Initializes the CodeQL tools for scanning.
44 |     - name: Initialize CodeQL
45 |       uses: github/codeql-action/init@v1
46 |       with:
47 |         languages: ${{ matrix.language }}
48 |         # If you wish to specify custom queries, you can do so here or in a config file.
49 |         # By default, queries listed here will override any specified in a config file.
50 |         # Prefix the list here with "+" to use these queries and those in the config file.
51 |         # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 | 
53 |     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
54 |     # If this step fails, then you should remove it and run the build manually (see below)
55 |     - name: Autobuild
56 |       uses: github/codeql-action/autobuild@v1
57 | 
58 |     # ℹ️ Command-line programs to run using the OS shell.
59 |     # 📚 https://git.io/JvXDl
60 | 
61 |     # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 |     #    and modify them (or add more) to build your code if your project
63 |     #    uses a compiled language
64 | 
65 |     #- run: |
66 |     #   make bootstrap
67 |     #   make release
68 | 
69 |     - name: Perform CodeQL Analysis
70 |       uses: github/codeql-action/analyze@v1
71 | 


--------------------------------------------------------------------------------
/.github/workflows/gen_sponsors.yaml:
--------------------------------------------------------------------------------
 1 | name: Generate Sponsors To README
 2 | on:
 3 |   workflow_dispatch
 4 | permissions:
 5 |   contents: write
 6 | jobs:
 7 |   deploy:
 8 |     runs-on: ubuntu-latest
 9 |     steps:
10 |       - name: Checkout
11 |         uses: actions/checkout@v2
12 | 
13 |       - name: Generate Sponsors
14 |         uses: JamesIves/github-sponsors-readme-action@v1
15 |         with:
16 |           token: ${{ secrets.PAT }}
17 |           file: 'README.md'
18 |           active-only: false,
19 |           template: '<a href="https://github.com/{{ login }}"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url={{ avatarUrl }}" width="60px" alt="{{ name }}" /></a>'
20 | 
21 |       - name: Generate Sponsors zh-CN
22 |         uses: JamesIves/github-sponsors-readme-action@v1
23 |         with:
24 |           token: ${{ secrets.PAT }}
25 |           file: 'README.zh-CN.md'
26 |           active-only: false,
27 |           template: '<a href="https://github.com/{{ login }}"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url={{ avatarUrl }}" width="60px" alt="{{ name }}" /></a>'
28 | 
29 |       - name: Commit To README
30 |         uses: JamesIves/github-pages-deploy-action@v4
31 |         with:
32 |           branch: master
33 |           folder: '.'
34 | 


--------------------------------------------------------------------------------
/.github/workflows/publish_winget.yml:
--------------------------------------------------------------------------------
 1 | name: Publish to WinGet
 2 | on:
 3 |   release:
 4 |     types: [released]
 5 | jobs:
 6 |   publish:
 7 |     runs-on: windows-latest
 8 |     steps:
 9 |       - uses: vedantmgoyal9/winget-releaser@main
10 |         with:
11 |           identifier: qishibo.AnotherRedisDesktopManager
12 |           installers-regex: '\.exe
#39; # Only .exe files
13 |           token: ${{ secrets.WINGET_TOKEN }}
14 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | .DS_Store
 2 | node_modules/
 3 | /dist/
 4 | *.log
 5 | npm-debug.log*
 6 | yarn-debug.log*
 7 | yarn-error.log*
 8 | 
 9 | # Editor directories and files
10 | .idea
11 | .vscode
12 | *.suo
13 | *.ntvs*
14 | *.njsproj
15 | *.sln
16 | 


--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 |   "esversion": 6,
3 |   "bitwise": false,
4 |   "freeze": true
5 | }
6 | 


--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
 1 | // https://github.com/michael-ciniawsky/postcss-load-config
 2 | 
 3 | module.exports = {
 4 |   "plugins": {
 5 |     "postcss-import": {},
 6 |     "postcss-url": {},
 7 |     "autoprefixer": {}
 8 |   }
 9 | }
10 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | The MIT License (MIT)
 2 | 
 3 | Copyright (c) shibo
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
 1 | # Privacy Policy
 2 | 
 3 | We takes your privacy seriously. To better protect your privacy we provide this privacy policy notice explaining the way your personal information is collected and used.
 4 | 
 5 | 
 6 | ## Collection of Routine Information
 7 | 
 8 | This app track basic information about their users. This information includes, but is not limited to, App details, timestamps, bug stacks and referring pages. None of this information can personally identify specific user to this app. The information is tracked for bug fixing and app maintenance purposes.
 9 | 
10 | And please note that this routine information is stored locally by the user. Unless the user provides it voluntarily, we cannot obtain this information and we won't upload this information to the internet.
11 | 
12 | 
13 | ## Links to Third Party Websites
14 | 
15 | We might included links on this app for your use and reference. we are not responsible for the privacy policies on these websites. You should be aware that the privacy policies of these websites may differ from ours.
16 | 
17 | 
18 | ## Changes To This Privacy Policy
19 | 
20 | This Privacy Policy is effective as of 2020-01-01 and will remain in effect except with respect to any changes in its provisions in the future, which will be in effect immediately after being posted on this page.
21 | 
22 | We reserve the right to update or change our Privacy Policy at any time and you should check this Privacy Policy periodically. If we make any material changes to this Privacy Policy, we will notify you either through the email address you have provided us, or by placing a prominent notice on our app.
23 | 
24 | 
25 | ## Contact Information
26 | 
27 | For any questions or concerns regarding the privacy policy, please contact by shiboqi123@gmail.com.


--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
 1 | # Security Policy
 2 | 
 3 | ## Reporting a Vulnerability
 4 | 
 5 | If there are any vulnerabilities in **Another Redis Desktop Manager**, don't hesitate to _report them_.
 6 | 
 7 | 1. Mail to `shiboqi123@gmail.com`
 8 | 2. Describe the vulnerability.
 9 | 
10 |    If you have a fix, that is most welcome -- please attach or summarize it in your message!
11 | 
12 | 3. We will evaluate the vulnerability and, if necessary, release a fix or mitigating steps to address it. We will contact you to let you know the outcome, and will credit you in the report.
13 | 
14 |    Please **do not disclose the vulnerability publicly** until a fix is released!
15 | 
16 | 4. Once we have either a) published a fix, or b) declined to address the vulnerability for whatever reason, you are free to publicly disclose it.
17 | 
18 | 5. Thx!
19 | 


--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "presets": [
 3 |     "@babel/env",
 4 |     "@vue/babel-preset-jsx"
 5 |   ],
 6 |   "sourceType": "unambiguous",
 7 |   "plugins": [
 8 |     "@babel/plugin-transform-runtime",
 9 |     "@babel/plugin-syntax-dynamic-import",
10 |     "@babel/plugin-proposal-object-rest-spread"
11 |   ]
12 | }
13 | 


--------------------------------------------------------------------------------
/build/build.js:
--------------------------------------------------------------------------------
 1 | 'use strict'
 2 | require('./check-versions')()
 3 | 
 4 | process.env.NODE_ENV = 'production'
 5 | 
 6 | const ora = require('ora')
 7 | const rm = require('rimraf')
 8 | const path = require('path')
 9 | const chalk = require('chalk')
10 | const webpack = require('webpack')
11 | const config = require('../config')
12 | const webpackConfig = require('./webpack.prod.conf')
13 | 
14 | const spinner = ora('building for production...')
15 | spinner.start()
16 | 
17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
18 |   if (err) throw err
19 |   webpack(webpackConfig, (err, stats) => {
20 |     spinner.stop()
21 |     if (err) throw err
22 |     process.stdout.write(stats.toString({
23 |       colors: true,
24 |       modules: false,
25 |       children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
26 |       chunks: false,
27 |       chunkModules: false
28 |     }) + '\n\n')
29 | 
30 |     if (stats.hasErrors()) {
31 |       console.log(chalk.red('  Build failed with errors.\n'))
32 |       process.exit(1)
33 |     }
34 | 
35 |     console.log(chalk.cyan('  Build complete.\n'))
36 |     console.log(chalk.yellow(
37 |       '  Tip: built files are meant to be served over an HTTP server.\n' +
38 |       '  Opening index.html over file:// won\'t work.\n'
39 |     ))
40 |   })
41 | })
42 | 


--------------------------------------------------------------------------------
/build/check-versions.js:
--------------------------------------------------------------------------------
 1 | 'use strict'
 2 | const chalk = require('chalk')
 3 | const semver = require('semver')
 4 | const packageConfig = require('../package.json')
 5 | const shell = require('shelljs')
 6 | 
 7 | function exec (cmd) {
 8 |   return require('child_process').execSync(cmd).toString().trim()
 9 | }
10 | 
11 | const versionRequirements = [
12 |   {
13 |     name: 'node',
14 |     currentVersion: semver.clean(process.version),
15 |     versionRequirement: packageConfig.engines.node
16 |   }
17 | ]
18 | 
19 | if (shell.which('npm')) {
20 |   versionRequirements.push({
21 |     name: 'npm',
22 |     currentVersion: exec('npm --version'),
23 |     versionRequirement: packageConfig.engines.npm
24 |   })
25 | }
26 | 
27 | module.exports = function () {
28 |   const warnings = []
29 | 
30 |   for (let i = 0; i < versionRequirements.length; i++) {
31 |     const mod = versionRequirements[i]
32 | 
33 |     if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
34 |       warnings.push(mod.name + ': ' +
35 |         chalk.red(mod.currentVersion) + ' should be ' +
36 |         chalk.green(mod.versionRequirement)
37 |       )
38 |     }
39 |   }
40 | 
41 |   if (warnings.length) {
42 |     console.log('')
43 |     console.log(chalk.yellow('To use this template, you must update following to modules:'))
44 |     console.log()
45 | 
46 |     for (let i = 0; i < warnings.length; i++) {
47 |       const warning = warnings[i]
48 |       console.log('  ' + warning)
49 |     }
50 | 
51 |     console.log()
52 |     process.exit(1)
53 |   }
54 | }
55 | 


--------------------------------------------------------------------------------
/build/utils.js:
--------------------------------------------------------------------------------
  1 | 'use strict'
  2 | const path = require('path')
  3 | const config = require('../config')
  4 | // const ExtractTextPlugin = require('extract-text-webpack-plugin')
  5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
  6 | const packageConfig = require('../package.json')
  7 | 
  8 | exports.assetsPath = function (_path) {
  9 |   const assetsSubDirectory = process.env.NODE_ENV === 'production'
 10 |     ? config.build.assetsSubDirectory
 11 |     : config.dev.assetsSubDirectory
 12 | 
 13 |   return path.posix.join(assetsSubDirectory, _path)
 14 | }
 15 | 
 16 | exports.cssLoaders = function (options) {
 17 |   options = options || {}
 18 | 
 19 |   const cssLoader = {
 20 |     loader: 'css-loader',
 21 |     options: {
 22 |       sourceMap: options.sourceMap
 23 |     }
 24 |   }
 25 | 
 26 |   const postcssLoader = {
 27 |     loader: 'postcss-loader',
 28 |     options: {
 29 |       sourceMap: options.sourceMap
 30 |     }
 31 |   }
 32 | 
 33 |   // generate loader string to be used with extract text plugin
 34 |   function generateLoaders (loader, loaderOptions) {
 35 |     const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
 36 | 
 37 |     if (loader) {
 38 |       loaders.push({
 39 |         loader: loader + '-loader',
 40 |         options: Object.assign({}, loaderOptions, {
 41 |           sourceMap: options.sourceMap
 42 |         })
 43 |       })
 44 |     }
 45 | 
 46 |     // Extract CSS when that option is specified
 47 |     // (which is the case during production build)
 48 |     if (options.extract) {
 49 |       // return ExtractTextPlugin.extract({
 50 |       //   use: loaders,
 51 |       //   fallback: 'vue-style-loader',
 52 |       //   // for font path import by css files
 53 |       //   publicPath:"../../",
 54 |       // })
 55 |       return [MiniCssExtractPlugin.loader].concat(loaders)
 56 |     } else {
 57 |       return ['vue-style-loader'].concat(loaders)
 58 |     }
 59 |   }
 60 | 
 61 |   // https://vue-loader.vuejs.org/en/configurations/extract-css.html
 62 |   return {
 63 |     css: generateLoaders(),
 64 |     postcss: generateLoaders(),
 65 |     less: generateLoaders('less'),
 66 |     sass: generateLoaders('sass', { indentedSyntax: true }),
 67 |     scss: generateLoaders('sass'),
 68 |     stylus: generateLoaders('stylus'),
 69 |     styl: generateLoaders('stylus')
 70 |   }
 71 | }
 72 | 
 73 | // Generate loaders for standalone style files (outside of .vue)
 74 | exports.styleLoaders = function (options) {
 75 |   const output = []
 76 |   const loaders = exports.cssLoaders(options)
 77 | 
 78 |   for (const extension in loaders) {
 79 |     const loader = loaders[extension]
 80 |     output.push({
 81 |       test: new RegExp('\\.' + extension + '
#39;),
 82 |       use: loader
 83 |     })
 84 |   }
 85 | 
 86 |   return output
 87 | }
 88 | 
 89 | exports.createNotifierCallback = () => {
 90 |   const notifier = require('node-notifier')
 91 | 
 92 |   return (severity, errors) => {
 93 |     if (severity !== 'error') return
 94 | 
 95 |     const error = errors[0]
 96 |     const filename = error.file && error.file.split('!').pop()
 97 | 
 98 |     notifier.notify({
 99 |       title: packageConfig.name,
100 |       message: severity + ': ' + error.name,
101 |       subtitle: filename || '',
102 |       icon: path.join(__dirname, 'logo.png')
103 |     })
104 |   }
105 | }
106 | 


--------------------------------------------------------------------------------
/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
 1 | 'use strict'
 2 | const utils = require('./utils')
 3 | const config = require('../config')
 4 | const isProduction = process.env.NODE_ENV === 'production'
 5 | const sourceMapEnabled = isProduction
 6 |   ? config.build.productionSourceMap
 7 |   : config.dev.cssSourceMap
 8 | 
 9 | module.exports = {
10 |   loaders: utils.cssLoaders({
11 |     sourceMap: sourceMapEnabled,
12 |     extract: isProduction
13 |   }),
14 |   cssSourceMap: sourceMapEnabled,
15 |   cacheBusting: config.dev.cacheBusting,
16 |   transformToRequire: {
17 |     video: ['src', 'poster'],
18 |     source: 'src',
19 |     img: 'src',
20 |     image: 'xlink:href'
21 |   }
22 | }
23 | 


--------------------------------------------------------------------------------
/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
  1 | 'use strict'
  2 | const path = require('path')
  3 | const utils = require('./utils')
  4 | const config = require('../config')
  5 | const vueLoaderConfig = require('./vue-loader.conf')
  6 | 
  7 | function resolve (dir) {
  8 |   return path.join(__dirname, '..', dir)
  9 | }
 10 | 
 11 | 
 12 | 
 13 | module.exports = {
 14 |   mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
 15 |   context: path.resolve(__dirname, '../'),
 16 |   entry: {
 17 |     app: './src/main.js'
 18 |   },
 19 |   output: {
 20 |     path: config.build.assetsRoot,
 21 |     filename: '[name].js',
 22 |     publicPath: process.env.NODE_ENV === 'production'
 23 |       ? config.build.assetsPublicPath
 24 |       : config.dev.assetsPublicPath
 25 |   },
 26 |   target: 'electron-renderer',
 27 |   resolve: {
 28 |     extensions: ['.js', '.vue', '.json'],
 29 |     alias: {
 30 |       'vue
#39;: 'vue/dist/vue.esm.js',
 31 |       '@': resolve('src'),
 32 |     }
 33 |   },
 34 |   module: {
 35 |     rules: [
 36 |       {
 37 |         test: /\.node$/,
 38 |         loader: "node-loader",
 39 |         options: {
 40 |           // map sourceMap
 41 |           name(resourcePath, resourceQuery) {
 42 |             if (process.env.NODE_ENV === "development") {
 43 |               return "[path][name].[ext]";
 44 |             }
 45 | 
 46 |             return "[contenthash].[ext]";
 47 |           },
 48 |         },
 49 |       },
 50 |       {
 51 |         test: /\.vue$/,
 52 |         loader: 'vue-loader',
 53 |         options: vueLoaderConfig,
 54 |         // include: [
 55 |         //   resolve('src'), resolve('test'),
 56 |         //   // resolve('node_modules/@qii404/vue-easy-tree/src/')
 57 |         // ],
 58 |       },
 59 |       {
 60 |         test: /\.js$/,
 61 |         loader: 'babel-loader',
 62 |         include: [
 63 |           // resolve('src'),
 64 |           // resolve('test'),
 65 |           // resolve('node_modules/webpack-dev-server/client'),
 66 |           resolve('node_modules/pickleparser')
 67 |         ]
 68 |       },
 69 |       {
 70 |         test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
 71 |         loader: 'url-loader',
 72 |         options: {
 73 |           limit: 10000,
 74 |           name: utils.assetsPath('img/[name].[hash:7].[ext]')
 75 |         }
 76 |       },
 77 |       {
 78 |         test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
 79 |         loader: 'url-loader',
 80 |         options: {
 81 |           limit: 10000,
 82 |           name: utils.assetsPath('media/[name].[hash:7].[ext]')
 83 |         }
 84 |       },
 85 |       {
 86 |         test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
 87 |         loader: 'url-loader',
 88 |         options: {
 89 |           limit: 10000,
 90 |           name: utils.assetsPath('fonts/[name].[hash:7].[ext]'),
 91 |           // this is vital important for fonts loads, added before 'static/fonts'
 92 |           publicPath: '../../'
 93 |         }
 94 |       },
 95 | 
 96 |     ]
 97 |   },
 98 |   node: {
 99 |     // prevent webpack from injecting useless setImmediate polyfill because Vue
100 |     // source contains it (although only uses it if it's native).
101 |     setImmediate: false,
102 |     // prevent webpack from injecting mocks to Node native modules
103 |     // that does not make sense for the client
104 |     dgram: 'empty',
105 |     fs: 'empty',
106 |     net: 'empty',
107 |     tls: 'empty',
108 |     child_process: 'empty'
109 |   }
110 | }
111 | 


--------------------------------------------------------------------------------
/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
  1 | 'use strict'
  2 | const utils = require('./utils')
  3 | const webpack = require('webpack')
  4 | const config = require('../config')
  5 | const merge = require('webpack-merge')
  6 | const path = require('path')
  7 | const baseWebpackConfig = require('./webpack.base.conf')
  8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
  9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
 11 | const portfinder = require('portfinder')
 12 | 
 13 | const VueLoaderPlugin = require('vue-loader/lib/plugin');
 14 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
 15 | 
 16 | const HOST = process.env.HOST
 17 | const PORT = process.env.PORT && Number(process.env.PORT)
 18 | 
 19 | const devWebpackConfig = merge(baseWebpackConfig, {
 20 |   mode: 'development',
 21 |   module: {
 22 |     rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
 23 |   },
 24 |   // cheap-module-eval-source-map is faster for development
 25 |   devtool: config.dev.devtool,
 26 | 
 27 |   // these devServer options should be customized in /config/index.js
 28 |   devServer: {
 29 |     clientLogLevel: 'warning',
 30 |     historyApiFallback: {
 31 |       rewrites: [
 32 |         { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
 33 |       ],
 34 |     },
 35 |     hot: true,
 36 |     contentBase: false, // since we use CopyWebpackPlugin.
 37 |     compress: true,
 38 |     host: HOST || config.dev.host,
 39 |     port: PORT || config.dev.port,
 40 |     open: config.dev.autoOpenBrowser,
 41 |     overlay: config.dev.errorOverlay
 42 |       ? { warnings: false, errors: true }
 43 |       : false,
 44 |     publicPath: config.dev.assetsPublicPath,
 45 |     proxy: config.dev.proxyTable,
 46 |     quiet: true, // necessary for FriendlyErrorsPlugin
 47 |     watchOptions: {
 48 |       poll: config.dev.poll,
 49 |     }
 50 |   },
 51 |   plugins: [
 52 |     new VueLoaderPlugin(),
 53 |     new MonacoWebpackPlugin({languages: ['json'], features: []}),
 54 |     // new webpack.DefinePlugin({
 55 |     //   'process.env': require('../config/dev.env')
 56 |     // }),
 57 |     new webpack.HotModuleReplacementPlugin(),
 58 |     // new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
 59 |     // new webpack.NoEmitOnErrorsPlugin(),
 60 |     // https://github.com/ampedandwired/html-webpack-plugin
 61 |     new HtmlWebpackPlugin({
 62 |       filename: 'index.html',
 63 |       template: 'index.html',
 64 |       inject: true
 65 |     }),
 66 |     // copy custom static assets
 67 |     new CopyWebpackPlugin([
 68 |       {
 69 |         from: path.resolve(__dirname, '../static'),
 70 |         to: config.dev.assetsSubDirectory,
 71 |         ignore: ['.*']
 72 |       }
 73 |     ])
 74 |   ]
 75 | })
 76 | 
 77 | module.exports = new Promise((resolve, reject) => {
 78 |   portfinder.basePort = process.env.PORT || config.dev.port
 79 |   portfinder.getPort((err, port) => {
 80 |     if (err) {
 81 |       reject(err)
 82 |     } else {
 83 |       // publish the new Port, necessary for e2e tests
 84 |       process.env.PORT = port
 85 |       // add port to devServer config
 86 |       devWebpackConfig.devServer.port = port
 87 | 
 88 |       // Add FriendlyErrorsPlugin
 89 |       devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
 90 |         compilationSuccessInfo: {
 91 |           messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
 92 |         },
 93 |         onErrors: config.dev.notifyOnErrors
 94 |         ? utils.createNotifierCallback()
 95 |         : undefined
 96 |       }))
 97 | 
 98 |       resolve(devWebpackConfig)
 99 |     }
100 |   })
101 | })
102 | 


--------------------------------------------------------------------------------
/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
  1 | 'use strict'
  2 | const path = require('path')
  3 | const utils = require('./utils')
  4 | const webpack = require('webpack')
  5 | const config = require('../config')
  6 | const merge = require('webpack-merge')
  7 | const baseWebpackConfig = require('./webpack.base.conf')
  8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
  9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
 10 | // const ExtractTextPlugin = require('extract-text-webpack-plugin')
 11 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
 12 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
 13 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
 14 | 
 15 | const VueLoaderPlugin = require('vue-loader/lib/plugin');
 16 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
 17 | 
 18 | const env = require('../config/prod.env')
 19 | 
 20 | const webpackConfig = merge(baseWebpackConfig, {
 21 |   mode: 'production',
 22 |   module: {
 23 |     rules: utils.styleLoaders({
 24 |       sourceMap: config.build.productionSourceMap,
 25 |       extract: true,
 26 |       usePostCSS: true
 27 |     })
 28 |   },
 29 |   devtool: config.build.productionSourceMap ? config.build.devtool : false,
 30 |   performance: {
 31 |     hints: false
 32 |   },
 33 |   output: {
 34 |     path: config.build.assetsRoot,
 35 |     filename: utils.assetsPath('js/[name].[chunkhash].js'),
 36 |     chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
 37 |   },
 38 |   plugins: [
 39 |     // show bundle analysis if need
 40 |     // new BundleAnalyzerPlugin(),
 41 |     new VueLoaderPlugin(),
 42 |     new MonacoWebpackPlugin({languages: ['json'], features: []}),
 43 |     // http://vuejs.github.io/vue-loader/en/workflow/production.html
 44 |     // new webpack.DefinePlugin({
 45 |     //   'process.env': env
 46 |     // }),
 47 |     // new UglifyJsPlugin({
 48 |     //   uglifyOptions: {
 49 |     //     compress: {
 50 |     //       warnings: false,
 51 |     //       drop_debugger: true,
 52 |     //       drop_console: true,
 53 |     //     }
 54 |     //   },
 55 |     //   sourceMap: config.build.productionSourceMap,
 56 |     //   parallel: true
 57 |     // }),
 58 |     // extract css into its own file
 59 |     // new ExtractTextPlugin({
 60 |     //   filename: utils.assetsPath('css/[name].[contenthash].css'),
 61 |     //   // Setting the following option to `false` will not extract CSS from codesplit chunks.
 62 |     //   // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
 63 |     //   // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
 64 |     //   // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
 65 |     //   allChunks: true,
 66 |     // }),
 67 |     new MiniCssExtractPlugin({
 68 |       filename: utils.assetsPath('css/[name].css'),
 69 |       chunkFilename: utils.assetsPath('css/[name].[contenthash].css')
 70 |     }),
 71 |     // Compress extracted CSS. We are using this plugin so that possible
 72 |     // duplicated CSS from different components can be deduped.
 73 |     new OptimizeCSSPlugin({
 74 |       cssProcessorOptions: config.build.productionSourceMap
 75 |         ? { safe: true, map: { inline: false } }
 76 |         : { safe: true }
 77 |     }),
 78 |     // generate dist index.html with correct asset hash for caching.
 79 |     // you can customize output by editing /index.html
 80 |     // see https://github.com/ampedandwired/html-webpack-plugin
 81 |     new HtmlWebpackPlugin({
 82 |       filename: config.build.index,
 83 |       template: 'index.html',
 84 |       inject: true,
 85 |       minify: {
 86 |         removeComments: true,
 87 |         collapseWhitespace: true,
 88 |         removeAttributeQuotes: true
 89 |         // more options:
 90 |         // https://github.com/kangax/html-minifier#options-quick-reference
 91 |       },
 92 |       // necessary to consistently work with multiple chunks via CommonsChunkPlugin
 93 |       // chunksSortMode: 'dependency'
 94 |     }),
 95 |     // keep module.id stable when vendor modules does not change
 96 |     new webpack.HashedModuleIdsPlugin(),
 97 |     // enable scope hoisting
 98 |     // new webpack.optimize.ModuleConcatenationPlugin(),
 99 |     // split vendor js into its own file
100 |     // new webpack.optimize.CommonsChunkPlugin({
101 |     //   name: 'vendor',
102 |     //   minChunks (module) {
103 |     //     // any required modules inside node_modules are extracted to vendor
104 |     //     return (
105 |     //       module.resource &&
106 |     //       /\.js$/.test(module.resource) &&
107 |     //       module.resource.indexOf(
108 |     //         path.join(__dirname, '../node_modules')
109 |     //       ) === 0
110 |     //     )
111 |     //   }
112 |     // }),
113 |     // // extract webpack runtime and module manifest to its own file in order to
114 |     // // prevent vendor hash from being updated whenever app bundle is updated
115 |     // new webpack.optimize.CommonsChunkPlugin({
116 |     //   name: 'manifest',
117 |     //   minChunks: Infinity
118 |     // }),
119 |     // // This instance extracts shared chunks from code splitted chunks and bundles them
120 |     // // in a separate chunk, similar to the vendor chunk
121 |     // // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
122 |     // new webpack.optimize.CommonsChunkPlugin({
123 |     //   name: 'app',
124 |     //   async: 'vendor-async',
125 |     //   children: true,
126 |     //   minChunks: 3
127 |     // }),
128 | 
129 |     // copy custom static assets
130 |     new CopyWebpackPlugin([
131 |       {
132 |         from: path.resolve(__dirname, '../static'),
133 |         to: config.build.assetsSubDirectory,
134 |         ignore: ['.*']
135 |       }
136 |     ])
137 |   ],
138 |   optimization: {
139 |     runtimeChunk: {
140 |       name: 'manifest'
141 |     },
142 |     minimizer: [
143 |       new UglifyJsPlugin({
144 |         cache: true,
145 |         parallel: true,
146 |         sourceMap: config.build.productionSourceMap,
147 |         uglifyOptions: {
148 |           warnings: false
149 |         },
150 |       }),
151 |       new OptimizeCSSPlugin({
152 |         cssProcessorOptions: config.build.productionSourceMap
153 |           ? { safe: true, map: { inline: false } }
154 |           : { safe: true }
155 |       }),
156 |     ],
157 |     splitChunks: {
158 |       chunks: 'all',
159 |       // cacheGroups: {
160 |       //   commons: {
161 |       //     test: /[\\/]node_modules[\\/]/,
162 |       //     name: 'vendors',
163 |       //     chunks: 'all',
164 |       //   },
165 |       // }
166 |     }
167 |   },
168 | })
169 | 
170 | if (config.build.productionGzip) {
171 |   const CompressionWebpackPlugin = require('compression-webpack-plugin')
172 | 
173 |   webpackConfig.plugins.push(
174 |     new CompressionWebpackPlugin({
175 |       asset: '[path].gz[query]',
176 |       algorithm: 'gzip',
177 |       test: new RegExp(
178 |         '\\.(' +
179 |         config.build.productionGzipExtensions.join('|') +
180 |         ')
#39;
181 |       ),
182 |       threshold: 10240,
183 |       minRatio: 0.8
184 |     })
185 |   )
186 | }
187 | 
188 | if (config.build.bundleAnalyzerReport) {
189 |   const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
190 |   webpackConfig.plugins.push(new BundleAnalyzerPlugin())
191 | }
192 | 
193 | module.exports = webpackConfig
194 | 


--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const prodEnv = require('./prod.env')
4 | 
5 | module.exports = merge(prodEnv, {
6 |   NODE_ENV: '"development"'
7 | })
8 | 


--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
 1 | 'use strict'
 2 | // Template version: 1.3.1
 3 | // see http://vuejs-templates.github.io/webpack for documentation.
 4 | 
 5 | const path = require('path')
 6 | 
 7 | module.exports = {
 8 |   dev: {
 9 | 
10 |     // Paths
11 |     assetsSubDirectory: 'static',
12 |     assetsPublicPath: '/',
13 |     proxyTable: {},
14 | 
15 |     // Various Dev Server settings
16 |     host: 'localhost', // can be overwritten by process.env.HOST
17 |     port: 9988, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
18 |     autoOpenBrowser: false,
19 |     errorOverlay: true,
20 |     notifyOnErrors: true,
21 |     poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
22 | 
23 | 
24 |     /**
25 |      * Source Maps
26 |      */
27 | 
28 |     // https://webpack.js.org/configuration/devtool/#development
29 |     devtool: 'cheap-module-eval-source-map',
30 | 
31 |     // If you have problems debugging vue-files in devtools,
32 |     // set this to false - it *may* help
33 |     // https://vue-loader.vuejs.org/en/options.html#cachebusting
34 |     cacheBusting: true,
35 | 
36 |     cssSourceMap: true
37 |   },
38 | 
39 |   build: {
40 |     // Template for index.html
41 |     index: path.resolve(__dirname, '../dist/index.html'),
42 | 
43 |     // Paths
44 |     assetsRoot: path.resolve(__dirname, '../dist'),
45 |     assetsSubDirectory: 'static',
46 |     // when not web conditon, such as index.html as main, relative path
47 |     assetsPublicPath: './',
48 | 
49 |     /**
50 |      * Source Maps
51 |      */
52 | 
53 |     productionSourceMap: false,
54 |     // https://webpack.js.org/configuration/devtool/#production
55 |     // devtool: '#source-map',
56 | 
57 |     // Gzip off by default as many popular static hosts such as
58 |     // Surge or Netlify already gzip all static assets for you.
59 |     // Before setting to `true`, make sure to:
60 |     // npm install --save-dev compression-webpack-plugin
61 |     productionGzip: false,
62 |     productionGzipExtensions: ['js', 'css'],
63 | 
64 |     // Run the build command with an extra argument to
65 |     // View the bundle analyzer report after build finishes:
66 |     // `npm run build --report`
67 |     // Set to `true` or `false` to always turn it on or off
68 |     bundleAnalyzerReport: process.env.npm_config_report
69 |   }
70 | }
71 | 


--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 |   NODE_ENV: '"production"'
4 | }
5 | 


--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html>
 3 |   <head>
 4 |     <meta charset="utf-8">
 5 |     <meta name="viewport" content="width=device-width,initial-scale=1.0">
 6 |     <link rel="stylesheet" type="text/css" id="theme-link">
 7 |     <title>Another Redis Desktop Manager</title>
 8 |   </head>
 9 |   <body>
10 |     <!-- this script must be placed here after body -->
11 |     <script type="text/javascript">
12 |       const ipcRenderer = require('electron').ipcRenderer;
13 | 
14 |       function changeCSS(theme = 'light') {
15 |         const themeHref = `static/theme/${theme}/index.css`;
16 |         document.getElementById('theme-link').href = themeHref;
17 |         theme == 'dark' ? document.body.classList.add('dark-mode') :
18 |                           document.body.classList.remove('dark-mode');
19 |       }
20 | 
21 |       function globalChangeTheme(theme) {
22 |         ipcRenderer.invoke('changeTheme', theme).then(shouldUseDarkColors => {
23 |           // delay to avoid stuck
24 |           setTimeout(() => {
25 |             changeCSS(shouldUseDarkColors ? 'dark' : 'light');
26 |           }, 100);
27 |         });
28 |       }
29 | 
30 |       // triggered by OS theme changed
31 |       ipcRenderer.on('os-theme-updated', (event, arg) => {
32 |         // auto change only when theme set to 'system'
33 |         if (arg.themeSource != 'system') {
34 |           return;
35 |         }
36 | 
37 |         setTimeout(() => {
38 |           changeCSS(arg.shouldUseDarkColors ? 'dark' : 'light');
39 |         }, 100);
40 |       });
41 | 
42 |       // theme init at startup
43 |       (() => {
44 |         let theme = localStorage.theme;
45 | 
46 |         if (!['system', 'light', 'dark'].includes(theme)) {
47 |           theme = 'system';
48 |         }
49 | 
50 |         // follow system OS
51 |         if (theme == 'system') {
52 |           const dark = (new URL(window.location.href)).searchParams.get('dark');
53 |           changeCSS(dark === 'true' ? 'dark' : 'light');
54 |         }
55 |         // setted light or dark
56 |         else {
57 |           changeCSS(theme);
58 |           ipcRenderer.invoke('changeTheme', theme);
59 |         }
60 |       })();
61 |     </script>
62 | 
63 |     <div id="app"></div>
64 |     <!-- built files will be auto injected -->
65 |   </body>
66 | </html>
67 | 


--------------------------------------------------------------------------------
/pack/electron/electron-main.js:
--------------------------------------------------------------------------------
  1 | // Modules to control application life and create native browser window
  2 | const {
  3 |   app, BrowserWindow, Menu, ipcMain, dialog, nativeTheme,
  4 | } = require('electron');
  5 | const url = require('url');
  6 | const path = require('path');
  7 | const fontManager = require('./font-manager');
  8 | const winState = require('./win-state');
  9 | 
 10 | // disable GPU for some white screen issues
 11 | // app.disableHardwareAcceleration();
 12 | // app.commandLine.appendSwitch('disable-gpu');
 13 | 
 14 | global.APP_ENV = (process.env.ARDM_ENV === 'development') ? 'development' : 'production';
 15 | 
 16 | // Keep a global reference of the window object, if you don't, the window will
 17 | // be closed automatically when the JavaScript object is garbage collected.
 18 | let mainWindow;
 19 | 
 20 | // handle uncaught exception
 21 | process.on('uncaughtException', (err, origin) => {
 22 |   if (!err) {
 23 |     return;
 24 |   }
 25 | 
 26 |   dialog.showMessageBoxSync(mainWindow, {
 27 |     type: 'error',
 28 |     title: 'Whoops! Uncaught Exception',
 29 |     message: err.stack,
 30 |     detail: '\nDon\'t worry, I will fix it! 😎😎\n\n'
 31 |             + 'Submit issue to: \nhttps://github.com/qishibo/AnotherRedisDesktopManager/',
 32 |   });
 33 | 
 34 |   process.exit();
 35 | });
 36 | 
 37 | // auto update
 38 | if (APP_ENV === 'production') {
 39 |   require('./update')();
 40 | }
 41 | 
 42 | function createWindow() {
 43 |   // get last win stage
 44 |   const lastWinStage = winState.getLastState();
 45 | 
 46 |   // Create the browser window.
 47 |   mainWindow = new BrowserWindow({
 48 |     x: lastWinStage.x,
 49 |     y: lastWinStage.y,
 50 |     width: lastWinStage.width,
 51 |     height: lastWinStage.height,
 52 |     icon: `${__dirname}/icons/icon.png`,
 53 |     autoHideMenuBar: true,
 54 |     webPreferences: {
 55 |       nodeIntegration: true,
 56 |       // add this to keep 'remote' module avaiable. Tips: it will be removed in electron 14
 57 |       enableRemoteModule: true,
 58 |       contextIsolation: false,
 59 |     },
 60 |   });
 61 | 
 62 |   if (lastWinStage.maximized) {
 63 |     mainWindow.maximize();
 64 |   }
 65 | 
 66 |   winState.watchClose(mainWindow);
 67 | 
 68 |   // and load the index.html of the app.
 69 |   if (APP_ENV === 'production') {
 70 |     // mainWindow.loadFile('index.html');
 71 |     mainWindow.loadURL(url.format({
 72 |       protocol: 'file',
 73 |       pathname: path.join(__dirname, 'index.html'),
 74 |       query: {version: app.getVersion(), dark: nativeTheme.shouldUseDarkColors},
 75 |     }));
 76 |   } else {
 77 |     mainWindow.loadURL(url.format({
 78 |       protocol: 'http',
 79 |       host: 'localhost:9988',
 80 |       query: {version: app.getVersion(), dark: nativeTheme.shouldUseDarkColors},
 81 |     }));
 82 |   }
 83 | 
 84 |   // Open the DevTools.
 85 |   // mainWindow.webContents.openDevTools();
 86 | 
 87 |   mainWindow.on('close', () => {
 88 |     mainWindow.webContents.send('closingWindow');
 89 |   });
 90 | 
 91 |   // Emitted when the window is closed.
 92 |   mainWindow.on('closed', () => {
 93 |     // Dereference the window object, usually you would store windows
 94 |     // in an array if your app supports multi windows, this is the time
 95 |     // when you should delete the corresponding element.
 96 |     mainWindow = null;
 97 |   });
 98 | 
 99 |   // const contents = mainWindow.webContents;
100 |   // // contents.openFindWindow();
101 |   // contents.findInPage('133');
102 | }
103 | 
104 | // This method will be called when Electron has finished
105 | // initialization and is ready to create browser windows.
106 | // Some APIs can only be used after this event occurs.
107 | app.on('ready', createWindow);
108 | 
109 | // Quit when all windows are closed.
110 | app.on('window-all-closed', () => {
111 |   app.quit();
112 |   // On macOS it is common for applications and their menu bar
113 |   // to stay active until the user quits explicitly with Cmd + Q
114 |   // if (process.platform !== 'darwin') {
115 |   //   app.quit();
116 |   // }
117 | });
118 | 
119 | app.on('activate', () => {
120 |   // On macOS it's common to re-create a window in the app when the
121 |   // dock icon is clicked and there are no other windows open.
122 |   if (mainWindow === null) {
123 |     createWindow();
124 |   }
125 | });
126 | 
127 | // hide window
128 | ipcMain.on('hideWindow', () => {
129 |   mainWindow && mainWindow.hide();
130 | });
131 | // minimize window
132 | ipcMain.on('minimizeWindow', () => {
133 |   mainWindow && mainWindow.minimize();
134 | });
135 | // toggle maximize
136 | ipcMain.on('toggleMaximize', () => {
137 |   if (mainWindow) {
138 |     // restore failed on MacOS, use unmaximize instead
139 |     mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize();
140 |   }
141 | });
142 | 
143 | ipcMain.handle('getMainArgs', (event, arg) => ({
144 |   argv: process.argv,
145 |   version: app.getVersion(),
146 | }));
147 | 
148 | ipcMain.handle('changeTheme', (event, theme) => {
149 |   nativeTheme.themeSource = theme;
150 |   return nativeTheme.shouldUseDarkColors;
151 | });
152 | 
153 | // OS theme changed
154 | nativeTheme.on('updated', () => {
155 |   // delay send to prevent webcontent stuck
156 |   setTimeout(() => {
157 |     mainWindow.webContents.send('os-theme-updated', {
158 |       shouldUseDarkColors: nativeTheme.shouldUseDarkColors,
159 |       themeSource: nativeTheme.themeSource
160 |     });
161 |   }, 50);
162 | });
163 | 
164 | ipcMain.handle('getTempPath', (event, arg) => app.getPath('temp'));
165 | 
166 | // for mac copy paset shortcut
167 | if (process.platform === 'darwin') {
168 |   const template = [
169 |     // { role: 'appMenu' },
170 |     {
171 |       label: app.name,
172 |       submenu: [
173 |         { role: 'about' },
174 |         { type: 'separator' },
175 |         { role: 'services' },
176 |         { type: 'separator' },
177 |         { role: 'hide' },
178 |         { role: 'hideothers' },
179 |         { role: 'unhide' },
180 |         { type: 'separator' },
181 |         { role: 'quit' },
182 |       ],
183 |     },
184 |     { role: 'editMenu' },
185 |     // { role: 'viewMenu' },
186 |     {
187 |       label: 'View',
188 |       submenu: [
189 |         ...(
190 |           (APP_ENV === 'production') ? [] : [{ role: 'toggledevtools' }]
191 |         ),
192 |         { role: 'togglefullscreen' },
193 |       ],
194 |     },
195 |     // { role: 'windowMenu' },
196 |     {
197 |       role: 'window',
198 |       submenu: [
199 |         { role: 'minimize' },
200 |         { role: 'zoom' },
201 |         { type: 'separator' },
202 |         { role: 'front' },
203 |         { type: 'separator' },
204 |         // { role: 'window' }
205 |       ],
206 |     },
207 |     {
208 |       role: 'help',
209 |       submenu: [
210 |         {
211 |           label: 'Learn More',
212 |           click: async () => {
213 |             const { shell } = require('electron');
214 |             await shell.openExternal('https://github.com/qishibo/AnotherRedisDesktopManager');
215 |           },
216 |         },
217 |       ],
218 |     },
219 |   ];
220 | 
221 |   menu = Menu.buildFromTemplate(template);
222 |   Menu.setApplicationMenu(menu);
223 | }
224 | 
225 | // In this file you can include the rest of your app's specific main process
226 | // code. You can also put them in separate files and require them here.
227 | 


--------------------------------------------------------------------------------
/pack/electron/font-manager.js:
--------------------------------------------------------------------------------
 1 | const { ipcMain } = require('electron');
 2 | 
 3 | ipcMain.on('get-all-fonts', (event, arg) => {
 4 |   try {
 5 |     require('font-list').getFonts().then((fonts) => {
 6 |       if (!fonts || !fonts.length) {
 7 |         fonts = [];
 8 |       }
 9 | 
10 |       fonts = fonts.map(font => font.replace('"', '').replace('"', ''));
11 | 
12 |       event.sender.send('send-all-fonts', fonts);
13 |     }).catch(e => {
14 |       event.sender.send('send-all-fonts', ['Default Initial']);
15 |     });
16 |   } catch (e) {
17 |     event.sender.send('send-all-fonts', ['Default Initial']);
18 |   }
19 | });
20 | 


--------------------------------------------------------------------------------
/pack/electron/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qishibo/AnotherRedisDesktopManager/ae576b362b37d20385447f27c3ec3698914ad168/pack/electron/icons/icon.icns


--------------------------------------------------------------------------------
/pack/electron/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qishibo/AnotherRedisDesktopManager/ae576b362b37d20385447f27c3ec3698914ad168/pack/electron/icons/icon.ico


--------------------------------------------------------------------------------
/pack/electron/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qishibo/AnotherRedisDesktopManager/ae576b362b37d20385447f27c3ec3698914ad168/pack/electron/icons/icon.png


--------------------------------------------------------------------------------
/pack/electron/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "another-redis-desktop-manager",
 3 |   "version": "1.7.1",
 4 |   "description": "A faster, better and more stable redis desktop manager.",
 5 |   "author": "Another",
 6 |   "private": true,
 7 |   "main": "electron-main.js",
 8 |   "dependencies": {
 9 |     "electron-updater": "4.6.5",
10 |     "font-list": "^1.4.5"
11 |   },
12 |   "repository": "github:qishibo/AnotherRedisDesktopManager",
13 |   "build": {
14 |     "appId": "me.qii404.another-redis-desktop-manager",
15 |     "productName": "Another Redis Desktop Manager",
16 |     "artifactName": "${productName}-${os}-${version}-${arch}.${ext}",
17 |     "copyright": "Copyright © 2024 qii404",
18 |     "asar": true,
19 |     "directories": {
20 |       "output": "build-apps",
21 |       "buildResources": "./"
22 |     },
23 |     "electronVersion": "12.2.3",
24 |     "files": [
25 |       "!static/js/*.map",
26 |       "!static/css/*.map",
27 |       "!*.map"
28 |     ],
29 |     "publish": [{
30 |       "provider": "github",
31 |       "owner": "qishibo",
32 |       "repo": "AnotherRedisDesktopManager",
33 |       "releaseType": "prerelease"
34 |     }],
35 |     "win": {
36 |       "icon": "icons/icon.ico",
37 |       "target": [
38 |         {"target": "nsis", "arch": ["x64", "arm64"]},
39 |         {"target": "zip", "arch": ["x64"]}
40 |       ]
41 |     },
42 |     "nsis": {
43 |       "allowToChangeInstallationDirectory": true,
44 |       "oneClick": false,
45 |       "menuCategory": true,
46 |       "allowElevation": true
47 |     },
48 |     "linux": {
49 |       "icon": "icons/icon.png",
50 |       "category": "Utility",
51 |       "target": [
52 |         {"target": "AppImage", "arch": ["x64", "arm64"]}
53 |       ]
54 |     },
55 |     "snap": {
56 |       "plugs": ["default", "ssh-keys"]
57 |     },
58 |     "mac": {
59 |       "icon": "icons/icon.icns",
60 |       "type": "development",
61 |       "category": "public.app-category.developer-tools",
62 |       "target": [
63 |         {"target": "dmg", "arch": ["x64", "arm64"]}
64 |       ],
65 |       "extendInfo": {
66 |         "ElectronTeamID": "68JN8DV835"
67 |       }
68 |     },
69 |     "afterSign": "pack/scripts/notarize.js"
70 |   }
71 | }
72 | 


--------------------------------------------------------------------------------
/pack/electron/update.js:
--------------------------------------------------------------------------------
 1 | const { session, ipcMain, net } = require('electron');
 2 | const { autoUpdater } = require('electron-updater');
 3 | 
 4 | // disable auto download
 5 | autoUpdater.autoDownload = false;
 6 | 
 7 | let mainEvent;
 8 | 
 9 | const update = () => {
10 |   bindMainListener();
11 | 
12 |   ipcMain.on('update-check', (event, arg) => {
13 |     mainEvent = event;
14 |     autoUpdater.checkForUpdates()
15 |       .then(() => {})
16 |       .catch((err) => {
17 |         // mainEvent.sender.send('update-error', err);
18 |       });
19 |   });
20 | 
21 |   ipcMain.on('continue-update', (event, arg) => {
22 |     autoUpdater.downloadUpdate()
23 |       .then(() => {})
24 |       .catch((err) => {
25 |         // mainEvent.sender.send('update-error', err);
26 |       });
27 |   });
28 | };
29 | 
30 | function bindMainListener() {
31 |   autoUpdater.on('checking-for-update', () => {});
32 | 
33 |   autoUpdater.on('update-available', (info) => {
34 |     mainEvent.sender.send('update-available', info);
35 |   });
36 | 
37 |   autoUpdater.on('update-not-available', (info) => {
38 |     mainEvent.sender.send('update-not-available', info);
39 |   });
40 | 
41 |   autoUpdater.on('error', (err) => {
42 |     mainEvent.sender.send('update-error', err);
43 |   });
44 | 
45 |   autoUpdater.on('download-progress', (progressObj) => {
46 |     mainEvent.sender.send('download-progress', progressObj);
47 |   });
48 | 
49 |   autoUpdater.on('update-downloaded', (info) => {
50 |     mainEvent.sender.send('update-downloaded', info);
51 |   });
52 | }
53 | 
54 | module.exports = update;
55 | 


--------------------------------------------------------------------------------
/pack/electron/win-state.js:
--------------------------------------------------------------------------------
  1 | const { app, screen } = require('electron');
  2 | const path = require('path');
  3 | const fs = require('fs');
  4 | 
  5 | const winState = {
  6 |   // {x, y, width, height, maximized}
  7 |   getLastState() {
  8 |     let data = '{}';
  9 | 
 10 |     try {
 11 |       data = fs.readFileSync(this.getStateFile());
 12 |     } catch (err) {}
 13 | 
 14 |     const lastWinStage = this.parseJson(data);
 15 |     const lastX = lastWinStage.x;
 16 |     const lastY = lastWinStage.y;
 17 | 
 18 |     const primary = screen.getPrimaryDisplay();
 19 | 
 20 |     // recovery position only when app in primary screen
 21 |     // if in external screens, reset position for uncaught display issues
 22 |     if (
 23 |       lastX < 0 || lastY < 0
 24 |       || lastX > primary.workAreaSize.width || lastY > primary.workAreaSize.height
 25 |     ) {
 26 |       lastWinStage.x = null;
 27 |       lastWinStage.y = null;
 28 |     }
 29 | 
 30 |     // adjust extremely small window
 31 |     (lastWinStage.width < 250) && (lastWinStage.width = 1100);
 32 |     (lastWinStage.height < 250) && (lastWinStage.height = 728);
 33 | 
 34 |     return lastWinStage;
 35 | 
 36 |     // // there is some uncaught display issues when display in external screens
 37 |     // // such as windows disappears even x < width
 38 |     // let screenCanDisplay = false;
 39 |     // const displays = screen.getAllDisplays()
 40 | 
 41 |     // for (const display of displays) {
 42 |     //   const bounds = display.workArea;
 43 |     //   // check if there is a screen can display this position
 44 |     //   if (bounds.x * lastX > 0 && bounds.y * lastY > 0) {
 45 |     //     if (bounds.width > Math.abs(lastX) && bounds.height > Math.abs(lastY)) {
 46 |     //       screenCanDisplay = true;
 47 |     //       break;
 48 |     //     }
 49 |     //   }
 50 |     // }
 51 | 
 52 |     // let state = {...lastWinStage, x: null, y: null};
 53 | 
 54 |     // // recovery to last position
 55 |     // if (screenCanDisplay) {
 56 |     //   state.x = lastX;
 57 |     //   state.y = lastY;
 58 |     // }
 59 | 
 60 |     // return state;
 61 |   },
 62 | 
 63 |   watchClose(win) {
 64 |     win.on('close', () => {
 65 |       const winState = this.getWinState(win);
 66 | 
 67 |       if (!winState) {
 68 |         return;
 69 |       }
 70 | 
 71 |       this.saveStateToStorage(winState);
 72 |     });
 73 |   },
 74 | 
 75 |   getWinState(win) {
 76 |     try {
 77 |       const winBounds = win.getBounds();
 78 | 
 79 |       const state = {
 80 |         x: winBounds.x,
 81 |         y: winBounds.y,
 82 |         width: winBounds.width,
 83 |         height: winBounds.height,
 84 |         maximized: win.isMaximized(),
 85 |       };
 86 | 
 87 |       return state;
 88 |     } catch (err) {
 89 |       return false;
 90 |     }
 91 |   },
 92 | 
 93 |   saveStateToStorage(winState) {
 94 |     fs.writeFile(this.getStateFile(), JSON.stringify(winState), (err) => {});
 95 |   },
 96 | 
 97 |   getStateFile() {
 98 |     const userPath = app.getPath('userData');
 99 |     const fileName = 'ardm-win-state.json';
100 | 
101 |     return path.join(userPath, fileName);
102 |   },
103 | 
104 |   parseJson(str) {
105 |     let json = false;
106 | 
107 |     try {
108 |       json = JSON.parse(str);
109 |     } catch (err) {}
110 | 
111 |     return json;
112 |   },
113 | };
114 | 
115 | module.exports = winState;
116 | 


--------------------------------------------------------------------------------
/pack/scripts/notarize.js:
--------------------------------------------------------------------------------
 1 | const { notarize } = require('@electron/notarize');
 2 | 
 3 | exports.default = async function notarizing(context) {
 4 |   const { electronPlatformName, appOutDir } = context;
 5 |   if (electronPlatformName !== 'darwin') {
 6 |     return;
 7 |   }
 8 | 
 9 |   const appName = context.packager.appInfo.productFilename;
10 | 
11 |   return await notarize({
12 |     appBundleId: 'me.qii404.another-redis-desktop-manager',
13 |     appPath: `${appOutDir}/${appName}.app`,
14 |     appleId: process.env.APPLEID,
15 |     appleIdPassword: process.env.APPLEID_PASSWORD,
16 |     teamId: '68JN8DV835',
17 |   });
18 | };
19 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "name": "another-redis-desktop-manager",
  3 |   "version": "1.1.1",
  4 |   "description": "A faster, better and more stable redis desktop manager, compatible with Linux, windows, mac",
  5 |   "author": "qii404",
  6 |   "private": true,
  7 |   "scripts": {
  8 |     "dev": "webpack serve --mode development --progress --config build/webpack.dev.conf.js",
  9 |     "start": "npm run dev",
 10 |     "build": "node build/build.js",
 11 |     "electron": "cross-env ARDM_ENV=development electron pack/electron",
 12 |     "lint": "eslint --ext js,vue src --fix",
 13 |     "lint:init": "eslint --init",
 14 |     "asar": "asar pack dist dist/app.asar --unpack-dir \"{app.asar,build-apps,app.asar.unpacked}\"",
 15 |     "pack:prepare": "npm run build && cp -r pack/electron/* dist/",
 16 |     "pack:win": "electron-builder --project=dist -w -p never",
 17 |     "pack:win32": "electron-builder --project=dist -w --ia32 -p never",
 18 |     "pack:mac": "electron-builder --project=dist -m -p never",
 19 |     "pack:mas": "CSC_IDENTITY_AUTO_DISCOVERY=false npm run pack:mac",
 20 |     "pack:linux": "electron-builder --project=dist -l -p never",
 21 |     "pack:win:publish": "electron-builder --project=dist -w -p always",
 22 |     "pack:mac:publish": "electron-builder --project=dist -m -p always",
 23 |     "pack:macm1:publish": "electron-builder --project=dist -m --arm64 -p always -c.artifactName='${productName} M1 ${arch} ${version}.${ext}'",
 24 |     "pack:linux:publish": "electron-builder --project=dist -l -p always",
 25 |     "et": "et -m",
 26 |     "sign": "cd pack/mas; bash sign.sh"
 27 |   },
 28 |   "dependencies": {
 29 |     "@qii404/json-bigint": "^1.0.0",
 30 |     "@qii404/redis-splitargs": "^1.0.1",
 31 |     "@qii404/vue-easy-tree": "^1.0.10",
 32 |     "algo-msgpack-with-bigint": "^2.1.1",
 33 |     "element-ui": "^2.4.11",
 34 |     "font-awesome": "^4.7.0",
 35 |     "getopts": "^2.3.0",
 36 |     "ioredis": "^5.3.2",
 37 |     "java-object-serialization": "^0.1.1",
 38 |     "keymaster": "^1.6.2",
 39 |     "monaco-editor": "^0.30.1",
 40 |     "node-version-compare": "^1.0.3",
 41 |     "php-serialize": "^4.0.2",
 42 |     "pickleparser": "^0.2.1",
 43 |     "protobufjs": "^6.11.2",
 44 |     "rawproto": "^0.7.6",
 45 |     "sortablejs": "^1.14.0",
 46 |     "tunnel-ssh": "^5.1.2",
 47 |     "vue": "^2.6.11",
 48 |     "vue-i18n": "^8.7.0",
 49 |     "vxe-table": "^3.9.0-rc.23"
 50 |   },
 51 |   "devDependencies": {
 52 |     "@babel/core": "^7.0.0",
 53 |     "@babel/plugin-proposal-class-properties": "^7.0.0",
 54 |     "@babel/plugin-proposal-decorators": "^7.0.0",
 55 |     "@babel/plugin-proposal-export-namespace-from": "^7.0.0",
 56 |     "@babel/plugin-proposal-function-sent": "^7.0.0",
 57 |     "@babel/plugin-proposal-json-strings": "^7.0.0",
 58 |     "@babel/plugin-proposal-numeric-separator": "^7.0.0",
 59 |     "@babel/plugin-proposal-throw-expressions": "^7.0.0",
 60 |     "@babel/plugin-syntax-dynamic-import": "^7.0.0",
 61 |     "@babel/plugin-syntax-import-meta": "^7.0.0",
 62 |     "@babel/plugin-syntax-jsx": "^7.0.0",
 63 |     "@babel/plugin-transform-runtime": "^7.0.0",
 64 |     "@babel/preset-env": "^7.0.0",
 65 |     "@electron/notarize": "^2.3.0",
 66 |     "@vue/babel-preset-jsx": "^1.2.4",
 67 |     "asar": "^1.0.0",
 68 |     "autoprefixer": "^7.1.2",
 69 |     "babel-helper-vue-jsx-merge-props": "^2.0.3",
 70 |     "babel-loader": "^8.0.0",
 71 |     "babel-plugin-component": "^1.1.1",
 72 |     "chalk": "^2.0.1",
 73 |     "copy-webpack-plugin": "^4.6.0",
 74 |     "cross-env": "^5.2.0",
 75 |     "css-loader": "^0.28.0",
 76 |     "electron": "^12.2.3",
 77 |     "electron-builder": "^23.0.2",
 78 |     "element-theme-chalk": "^2.13.0",
 79 |     "eslint": "^5.14.1",
 80 |     "eslint-config-airbnb-base": "^13.1.0",
 81 |     "eslint-plugin-import": "^2.16.0",
 82 |     "eslint-plugin-vue": "^5.2.2",
 83 |     "file-loader": "^1.1.4",
 84 |     "font-list": "^1.4.5",
 85 |     "friendly-errors-webpack-plugin": "^1.6.1",
 86 |     "html-webpack-plugin": "^4.5.2",
 87 |     "js-yaml": ">=3.13.1",
 88 |     "mini-css-extract-plugin": "^0.11.3",
 89 |     "monaco-editor-webpack-plugin": "^6.0.0",
 90 |     "node-loader": "^1.0.3",
 91 |     "node-notifier": "^5.1.2",
 92 |     "optimize-css-assets-webpack-plugin": "^5.0.3",
 93 |     "ora": "^1.2.0",
 94 |     "portfinder": "^1.0.13",
 95 |     "postcss-import": "^11.0.0",
 96 |     "postcss-loader": "^2.0.8",
 97 |     "postcss-url": "^7.2.1",
 98 |     "rimraf": "^2.6.0",
 99 |     "semver": "^5.3.0",
100 |     "shelljs": "^0.8.4",
101 |     "uglifyjs-webpack-plugin": "^2.2.0",
102 |     "url-loader": "^0.5.8",
103 |     "vue-loader": "^15.9.8",
104 |     "vue-style-loader": "^3.0.1",
105 |     "vue-template-compiler": "^2.6.11",
106 |     "webpack": "^4.46.0",
107 |     "webpack-bundle-analyzer": "^4.6.1",
108 |     "webpack-cli": "^4.9.1",
109 |     "webpack-dev-server": "^3.11.2",
110 |     "webpack-merge": "^4.2.2"
111 |   },
112 |   "engines": {
113 |     "node": ">= 6.0.0",
114 |     "npm": ">= 3.0.0"
115 |   },
116 |   "browserslist": [
117 |     "> 1%",
118 |     "last 2 versions",
119 |     "not ie <= 8"
120 |   ]
121 | }
122 | 


--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <el-container class="wrap-container" spellcheck="false">
  3 |     <!-- left aside draggable container -->
  4 |     <div class="aside-drag-container" :style="{width: sideWidth + 'px'}">
  5 |       <!-- connections -->
  6 |       <el-aside class="aside-connection">
  7 |         <Aside></Aside>
  8 |       </el-aside>
  9 | 
 10 |       <!-- drag area -->
 11 |       <div id="drag-resize-container">
 12 |         <div id="drag-resize-pointer"></div>
 13 |       </div>
 14 |     </div>
 15 | 
 16 |     <!-- right main container -->
 17 |     <el-container class='right-main-container'>
 18 |       <!-- tab container -->
 19 |       <el-main class='main-tabs-container'>
 20 |         <Tabs></Tabs>
 21 |       </el-main>
 22 |     </el-container>
 23 | 
 24 |     <UpdateCheck></UpdateCheck>
 25 |   </el-container>
 26 | </template>
 27 | 
 28 | <script>
 29 | import Aside from '@/Aside';
 30 | import Tabs from '@/components/Tabs';
 31 | import UpdateCheck from '@/components/UpdateCheck';
 32 | import addon from './addon';
 33 | 
 34 | export default {
 35 |   name: 'App',
 36 |   data() {
 37 |     return {
 38 |       sideWidth: 265,
 39 |     };
 40 |   },
 41 |   created() {
 42 |     this.$bus.$on('reloadSettings', () => {
 43 |       addon.reloadSettings();
 44 |     });
 45 | 
 46 |     // restore side bar width
 47 |     this.restoreSideBarWidth();
 48 |   },
 49 |   components: { Aside, Tabs, UpdateCheck },
 50 |   methods: {
 51 |     bindSideBarDrag() {
 52 |       const that = this;
 53 |       const dragPointer = document.getElementById('drag-resize-pointer');
 54 | 
 55 |       function mousemove(e) {
 56 |         const mouseX = e.x;
 57 |         const dragSideWidth = mouseX - 17;
 58 | 
 59 |         if ((dragSideWidth > 200) && (dragSideWidth < 1500)) {
 60 |           that.sideWidth = dragSideWidth;
 61 |         }
 62 |       }
 63 | 
 64 |       function mouseup(e) {
 65 |         document.documentElement.removeEventListener('mousemove', mousemove);
 66 |         document.documentElement.removeEventListener('mouseup', mouseup);
 67 | 
 68 |         // store side bar with
 69 |         localStorage.sideWidth = that.sideWidth;
 70 |       }
 71 | 
 72 |       dragPointer.addEventListener('mousedown', (e) => {
 73 |         e.preventDefault();
 74 | 
 75 |         document.documentElement.addEventListener('mousemove', mousemove);
 76 |         document.documentElement.addEventListener('mouseup', mouseup);
 77 |       });
 78 |     },
 79 |     restoreSideBarWidth() {
 80 |       const { sideWidth } = localStorage;
 81 |       sideWidth && (this.sideWidth = sideWidth);
 82 |     },
 83 |   },
 84 |   mounted() {
 85 |     setTimeout(() => {
 86 |       this.$bus.$emit('update-check');
 87 |     }, 2000);
 88 | 
 89 |     this.bindSideBarDrag();
 90 |     // addon init setup
 91 |     addon.setup();
 92 |   },
 93 | };
 94 | </script>
 95 | 
 96 | <style type="text/css">
 97 | html {
 98 |   height: 100%;
 99 | }
100 | body {
101 |   height: 100%;
102 |   padding: 8px;
103 |   margin: 0;
104 |   box-sizing: border-box;
105 |   -webkit-font-smoothing: antialiased;
106 | 
107 |   /*fix body scroll-y caused by tooltip in table*/
108 |   overflow: hidden;
109 | }
110 | 
111 | button, input, textarea, .vjs__tree {
112 |   font-family: inherit !important;
113 | }
114 | a {
115 |   color: #8e8d8d;
116 | }
117 | 
118 | 
119 | /*fix el-select bottom scroll bar*/
120 | .el-scrollbar__wrap {
121 |   overflow-x: hidden;
122 | }
123 | 
124 | /*scrollbar style start*/
125 | ::-webkit-scrollbar {
126 |   width: 9px;
127 | }
128 | /*track*/
129 | ::-webkit-scrollbar-track {
130 |   background: #eaeaea;
131 |   border-radius: 4px;
132 | }
133 | .dark-mode ::-webkit-scrollbar-track {
134 |   background: #425057;
135 | }
136 | /*track hover*/
137 | ::-webkit-scrollbar-track:hover {
138 |   background: #e0e0dd;
139 | }
140 | .dark-mode ::-webkit-scrollbar-track:hover {
141 |   background: #495961;
142 | }
143 | /*thumb*/
144 | ::-webkit-scrollbar-thumb {
145 |   border-radius: 8px;
146 |   background: #c1c1c1;
147 | }
148 | .dark-mode ::-webkit-scrollbar-thumb {
149 |   background: #5a6f7a;
150 | }
151 | /*thumb hover*/
152 | ::-webkit-scrollbar-thumb:hover {
153 |   background: #7f7f7f;
154 | }
155 | .dark-mode ::-webkit-scrollbar-thumb:hover {
156 |   background: #6a838f;
157 | }
158 | /*scrollbar style end*/
159 | 
160 | /*list index*/
161 | li .list-index {
162 |   color: #828282;
163 |   /*font-size: 80%;*/
164 |   user-select: none;
165 |   margin-right: 10px;
166 |   min-width: 28px;
167 | }
168 | .dark-mode li .list-index {
169 |   color: #adacac;
170 | }
171 | 
172 | .wrap-container {
173 |   height: 100%;
174 | }
175 | .aside-drag-container {
176 |   position: relative;
177 |   user-select: none;
178 |   /*max-width: 50%;*/
179 | }
180 | .aside-connection {
181 |   height: 100%;
182 |   width: 100% !important;
183 |   border-right: 1px solid #e4e0e0;
184 |   overflow: hidden;
185 | }
186 | /*fix right container imdraggable*/
187 | .right-main-container {
188 |   width: 10%;
189 | }
190 | .right-main-container .main-tabs-container {
191 |   overflow-y: hidden;
192 |   padding-top: 0px;
193 |   padding-right: 4px;
194 | }
195 | 
196 | .el-message-box .el-message-box__message {
197 |   word-break: break-all;
198 |   overflow-y: auto;
199 |   max-height: 80vh;
200 | }
201 | 
202 | #drag-resize-container {
203 |   position: absolute;
204 |   /*height: 100%;*/
205 |   width: 10px;
206 |   right: -12px;
207 |   top: 0px;
208 | }
209 | #drag-resize-pointer {
210 |   position: fixed;
211 |   height: 100%;
212 |   width: 10px;
213 |   cursor: col-resize;
214 | }
215 | #drag-resize-pointer::after {
216 |   content: "";
217 |   display: inline-block;
218 |   width: 2px;
219 |   height: 20px;
220 |   border-left: 1px solid #adabab;
221 |   border-right: 1px solid #adabab;
222 | 
223 |   position: absolute;
224 |   top: 0;
225 |   right: 0;
226 |   bottom: 0;
227 |   margin: auto;
228 | }
229 | .dark-mode #drag-resize-pointer::after {
230 |   border-left: 1px solid #b9b8b8;
231 |   border-right: 1px solid #b9b8b8;
232 | }
233 | 
234 | @keyframes rotate {
235 |   to{ transform: rotate(360deg); }
236 | }
237 | 
238 | /*vxe-table dark-mode color*/
239 | html .dark-mode {
240 |   --vxe-ui-table-header-background-color: #273239 !important;
241 |   --vxe-ui-layout-background-color: #273239 !important;
242 |   --vxe-ui-table-row-striped-background-color: #3b4b54 !important;
243 |   --vxe-ui-table-row-hover-background-color: #3b4b54 !important;
244 |   --vxe-ui-table-row-hover-striped-background-color: #50646f !important;
245 |   /*border color*/
246 |   --vxe-ui-table-border-color: #7f8ea5 !important;
247 |   /*font color*/
248 |   --vxe-ui-font-color: #f3f3f4 !important;
249 |   --vxe-ui-table-header-font-color: #f3f3f4 !important;
250 | }
251 | </style>
252 | 


--------------------------------------------------------------------------------
/src/Aside.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div class="aside-outer-container">
  3 |     <div>
  4 |       <!-- new connection button -->
  5 |       <div class="aside-top-container">
  6 |         <el-button class='aside-setting-btn' type="primary" icon="el-icon-time" @click="$refs.commandLogDialog.show()" :title='$t("message.command_log")+" Ctrl+g"' plain></el-button>
  7 |         <el-button class='aside-setting-btn' type="primary" icon="el-icon-setting" @click="$refs.settingDialog.show()" :title='$t("message.settings")+" Ctrl+,"' plain></el-button>
  8 | 
  9 |         <div class="aside-new-connection-container">
 10 |           <el-button class="aside-new-connection-btn" type="info" @click="addNewConnection" icon="el-icon-circle-plus" :title='$t("message.new_connection")+" Ctrl+n"'>{{ $t('message.new_connection') }}</el-button>
 11 |         </div>
 12 |       </div>
 13 | 
 14 |       <!-- new connection dialog -->
 15 |       <NewConnectionDialog
 16 |         @editConnectionFinished="editConnectionFinished"
 17 |         ref="newConnectionDialog">
 18 |       </NewConnectionDialog>
 19 | 
 20 |       <!-- user settings -->
 21 |       <Setting ref="settingDialog"></Setting>
 22 | 
 23 |       <!-- redis command logs -->
 24 |       <CommandLog ref='commandLogDialog'></CommandLog>
 25 |       <!-- hot key tips dialog -->
 26 |       <HotKeys ref='hotKeysDialog'></HotKeys>
 27 |       <!-- custom shell formatter -->
 28 |       <CustomFormatter></CustomFormatter>
 29 |     </div>
 30 | 
 31 |     <!-- connection list -->
 32 |     <Connections ref="connections"></Connections>
 33 |   </div>
 34 | </template>
 35 | 
 36 | <script type="text/javascript">
 37 | import Setting from '@/components/Setting';
 38 | import Connections from '@/components/Connections';
 39 | import NewConnectionDialog from '@/components/NewConnectionDialog';
 40 | import CommandLog from '@/components/CommandLog';
 41 | import HotKeys from '@/components/HotKeys';
 42 | import CustomFormatter from '@/components/CustomFormatter';
 43 | 
 44 | export default {
 45 |   data() {
 46 |     return {};
 47 |   },
 48 |   components: {
 49 |     Connections, NewConnectionDialog, Setting, CommandLog, HotKeys, CustomFormatter,
 50 |   },
 51 |   methods: {
 52 |     editConnectionFinished() {
 53 |       this.$refs.connections.initConnections();
 54 |     },
 55 |     addNewConnection() {
 56 |       this.$refs.newConnectionDialog.show();
 57 |     },
 58 |     initShortcut() {
 59 |       // new connection
 60 |       this.$shortcut.bind('ctrl+n, ⌘+n', () => {
 61 |         this.$refs.newConnectionDialog.show();
 62 |         return false;
 63 |       });
 64 |       // settings
 65 |       this.$shortcut.bind('ctrl+,', () => {
 66 |         this.$refs.settingDialog.show();
 67 |         return false;
 68 |       });
 69 |       this.$shortcut.bind('⌘+,', () => {
 70 |         this.$refs.settingDialog.show();
 71 |         return false;
 72 |       });
 73 |       // logs
 74 |       this.$shortcut.bind('ctrl+g, ⌘+g', () => {
 75 |         this.$refs.commandLogDialog.show();
 76 |         return false;
 77 |       });
 78 |     },
 79 |   },
 80 |   mounted() {
 81 |     this.initShortcut();
 82 |   },
 83 | };
 84 | </script>
 85 | 
 86 | <style type="text/css">
 87 |   .aside-top-container {
 88 |     margin-right: 8px;
 89 |   }
 90 |   .aside-top-container .aside-new-connection-container {
 91 |     margin-right: 109px;
 92 |   }
 93 |   .aside-new-connection-container .aside-new-connection-btn {
 94 |     width: 100%;
 95 |     overflow: hidden;
 96 |     text-overflow: ellipsis;
 97 |   }
 98 |   .aside-top-container .aside-setting-btn {
 99 |     float: right;
100 |     width: 44px;
101 |     margin-right: 5px;
102 |   }
103 | 
104 |   .dark-mode .aside-top-container .el-button--info {
105 |     color: #52a6fd;
106 |     background: inherit;
107 |   }
108 | </style>
109 | 


--------------------------------------------------------------------------------
/src/addon.js:
--------------------------------------------------------------------------------
  1 | import getopts from 'getopts';
  2 | import { ipcRenderer } from 'electron';
  3 | import bus from './bus';
  4 | import storage from './storage';
  5 | 
  6 | export default {
  7 |   setup() {
  8 |     // reload settings when init
  9 |     this.reloadSettings();
 10 |     // init args start from cli
 11 |     this.bindCliArgs();
 12 |     // bing href click
 13 |     this.openHrefInBrowser();
 14 |   },
 15 |   reloadSettings() {
 16 |     this.initFont();
 17 |     this.initZoom();
 18 |   },
 19 |   initFont() {
 20 |     const fontFamily = storage.getFontFamily();
 21 |     document.body.style.fontFamily = fontFamily;
 22 |     // tell monaco editor
 23 |     bus.$emit('fontInited', fontFamily);
 24 |   },
 25 |   initZoom() {
 26 |     let zoomFactor = storage.getSetting('zoomFactor');
 27 |     zoomFactor = zoomFactor || 1.0;
 28 | 
 29 |     const { webFrame } = require('electron');
 30 |     webFrame.setZoomFactor(zoomFactor);
 31 |   },
 32 |   openHrefInBrowser() {
 33 |     const { shell } = require('electron');
 34 | 
 35 |     document.addEventListener('click', (event) => {
 36 |       const ele = event.target;
 37 | 
 38 |       if (ele && (ele.nodeName.toLowerCase() === 'a') && ele.href.startsWith('http')) {
 39 |         event.preventDefault();
 40 |         shell.openExternal(ele.href);
 41 |       }
 42 |     });
 43 |   },
 44 |   bindCliArgs() {
 45 |     ipcRenderer.invoke('getMainArgs').then((result) => {
 46 |       if (!result.argv) {
 47 |         return;
 48 |       }
 49 |       const mainArgs = getopts(result.argv);
 50 | 
 51 |       if (!mainArgs.host) {
 52 |         return;
 53 |       }
 54 | 
 55 |       // common args
 56 |       const connection = {
 57 |         host: mainArgs.host,
 58 |         port: mainArgs.port ? mainArgs.port : 6379,
 59 |         auth: mainArgs.auth,
 60 |         username: mainArgs.username,
 61 |         name: mainArgs.name,
 62 |         separator: mainArgs.separator,
 63 |         connectionReadOnly: mainArgs.readonly,
 64 |       };
 65 | 
 66 |       // cluster args
 67 |       if (mainArgs.cluster) {
 68 |         connection.cluster = true;
 69 |       }
 70 | 
 71 |       // ssh args
 72 |       if (mainArgs['ssh-host'] && mainArgs['ssh-username']) {
 73 |         const sshOptions = {
 74 |           host: mainArgs['ssh-host'],
 75 |           port: mainArgs['ssh-port'] ? mainArgs['ssh-port'] : 22,
 76 |           username: mainArgs['ssh-username'],
 77 |           password: mainArgs['ssh-password'],
 78 |           privatekey: mainArgs['ssh-private-key'],
 79 |           passphrase: mainArgs['ssh-passphrase'],
 80 |           timeout: mainArgs['ssh-timeout'],
 81 |         };
 82 | 
 83 |         connection.sshOptions = sshOptions;
 84 |       }
 85 | 
 86 |       // sentinel args
 87 |       if (mainArgs['sentinel-master-name']) {
 88 |         const sentinelOptions = {
 89 |           masterName: mainArgs['sentinel-master-name'],
 90 |           nodePassword: mainArgs['sentinel-node-password'],
 91 |         };
 92 | 
 93 |         connection.sentinelOptions = sentinelOptions;
 94 |       }
 95 | 
 96 |       // ssl args
 97 |       if (mainArgs.ssl) {
 98 |         const sslOptions = {
 99 |           key: mainArgs['ssl-key'],
100 |           ca: mainArgs['ssl-ca'],
101 |           cert: mainArgs['ssl-cert'],
102 |         };
103 | 
104 |         connection.sslOptions = sslOptions;
105 |       }
106 | 
107 |       // add to storage
108 |       storage.addConnection(connection);
109 |       bus.$emit('refreshConnections');
110 | 
111 |       // open connection after added
112 |       setTimeout(() => {
113 |         bus.$emit('openConnection', connection.name);
114 |         // tmp connection, delete it after opened
115 |         if (!mainArgs.save) {
116 |           storage.deleteConnection(connection);
117 |         }
118 |       }, 300);
119 |     });
120 |   },
121 | };
122 | 


--------------------------------------------------------------------------------
/src/assets/custom_tree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qishibo/AnotherRedisDesktopManager/ae576b362b37d20385447f27c3ec3698914ad168/src/assets/custom_tree.png


--------------------------------------------------------------------------------
/src/assets/key_tree_toggle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qishibo/AnotherRedisDesktopManager/ae576b362b37d20385447f27c3ec3698914ad168/src/assets/key_tree_toggle.png


--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qishibo/AnotherRedisDesktopManager/ae576b362b37d20385447f27c3ec3698914ad168/src/assets/logo.png


--------------------------------------------------------------------------------
/src/bus.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue';
 2 | 
 3 | const eventHub = new Vue();
 4 | 
 5 | export default {
 6 |   $on(...event) {
 7 |     eventHub.$on(...event);
 8 |   },
 9 |   $off(...event) {
10 |     eventHub.$off(...event);
11 |   },
12 |   $once(...event) {
13 |     eventHub.$once(...event);
14 |   },
15 |   $emit(...event) {
16 |     eventHub.$emit(...event);
17 |   },
18 | };
19 | 


--------------------------------------------------------------------------------
/src/components/CliContent.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div class="cli-content-container">
  3 |     <!-- monaco editor div -->
  4 |     <div class="monaco-editor-con" ref="editor"></div>
  5 |   </div>
  6 | </template>
  7 | 
  8 | <script type="text/javascript">
  9 | // import * as monaco from 'monaco-editor';
 10 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
 11 | 
 12 | export default {
 13 |   props: {
 14 |     content: { type: String, default: () => {} },
 15 |   },
 16 |   created() {
 17 |     // listen font family change and reset options
 18 |     // to avoid cursor offset
 19 |     this.$bus.$on('fontInited', this.changeFont);
 20 |   },
 21 |   watch: {
 22 |     // refresh
 23 |     content(newVal) {
 24 |       this.monacoEditor.setValue(newVal);
 25 |     },
 26 |   },
 27 |   methods: {
 28 |     changeFont(fontFamily) {
 29 |       this.monacoEditor && this.monacoEditor.updateOptions({
 30 |         fontFamily,
 31 |       });
 32 |     },
 33 |     scrollToBottom() {
 34 |       this.monacoEditor.revealLine(this.monacoEditor.getModel().getLineCount());
 35 |     },
 36 |   },
 37 | 
 38 |   mounted() {
 39 |     this.monacoEditor = monaco.editor.create(
 40 |       this.$refs.editor,
 41 |       {
 42 |         value: this.content,
 43 |         theme: 'vs-dark',
 44 |         language: 'plaintext',
 45 |         links: false,
 46 |         readOnly: true,
 47 |         cursorStyle: 'underline-thin',
 48 |         lineNumbers: 'off',
 49 |         contextmenu: false,
 50 |         // set fontsize and family to avoid cursor offset
 51 |         fontSize: 14,
 52 |         fontFamily: this.$storage.getFontFamily(),
 53 |         showFoldingControls: 'always',
 54 |         // auto layout, performance cost
 55 |         automaticLayout: true,
 56 |         wordWrap: 'on',
 57 |         // wordWrapColumn: 120,
 58 |         // long text indent when wrapped
 59 |         wrappingIndent: 'none',
 60 |         // cursor line highlight
 61 |         renderLineHighlight: 'none',
 62 |         // highlight word when cursor in
 63 |         occurrencesHighlight: false,
 64 |         // disable scroll one page at last line
 65 |         scrollBeyondLastLine: false,
 66 |         // hide scroll sign of current line
 67 |         hideCursorInOverviewRuler: true,
 68 |         minimap: {
 69 |           enabled: false,
 70 |         },
 71 |         // vertical line
 72 |         guides: {
 73 |           indentation: false,
 74 |           highlightActiveIndentation: false,
 75 |         },
 76 |         scrollbar: {
 77 |           useShadows: false,
 78 |           verticalScrollbarSize: '9px',
 79 |           horizontalScrollbarSize: '9px',
 80 |         },
 81 |       },
 82 |     );
 83 | 
 84 |     // hide tooltip in readonly mode
 85 |     const messageContribution = this.monacoEditor.getContribution('editor.contrib.messageController');
 86 |     this.monacoEditor.onDidAttemptReadOnlyEdit(() => {
 87 |       messageContribution.dispose();
 88 |     });
 89 |   },
 90 |   destroyed() {
 91 |     this.monacoEditor.dispose();
 92 |     this.$bus.$off('fontInited', this.changeFont);
 93 |   },
 94 | };
 95 | </script>
 96 | 
 97 | <style type="text/css">
 98 |   .cli-content-container .monaco-editor-con {
 99 |     min-height: 150px;
100 |     height: calc(100vh - 123px);
101 |     clear: both;
102 |     overflow: hidden;
103 |     background: #263238;
104 |     border: 1px solid #e4e7ed;
105 |     border-bottom: 0px;
106 |     border-radius: 4px 4px 0 0;
107 |   }
108 |   .dark-mode .cli-content-container .monaco-editor-con {
109 |     background: #324148;
110 |     border-color: #7f8ea5;
111 |   }
112 | 
113 |   /* font color*/
114 |   .cli-content-container .monaco-editor-con .mtk1 {
115 |     color: #d3d5d9;
116 |   }
117 |   .dark-mode .cli-content-container .monaco-editor-con .mtk1 {
118 |     color: #e8e8e8;
119 |   }
120 | 
121 |   /*hide cursor*/
122 |   .cli-content-container .monaco-editor .cursors-layer > .cursor {
123 |     display: none !important;
124 |   }
125 | 
126 |   /*change default scrollbar style*/
127 |   .cli-content-container .monaco-editor .scrollbar {
128 |     background: #eaeaea;
129 |     border-radius: 4px;
130 |   }
131 |   .dark-mode .cli-content-container .monaco-editor .scrollbar {
132 |     background: #425057;
133 |   }
134 |   .cli-content-container .monaco-editor .scrollbar:hover {
135 |     background: #e0e0dd;
136 |   }
137 |   .dark-mode .cli-content-container .monaco-editor .scrollbar:hover {
138 |     background: #495961;
139 |   }
140 | 
141 |   .cli-content-container .monaco-editor-con .monaco-editor .slider {
142 |     border-radius: 4px;
143 |     background: #c1c1c1;
144 |   }
145 |   .dark-mode .cli-content-container .monaco-editor-con .monaco-editor .slider {
146 |     background: #5a6f7a;
147 |   }
148 |   .cli-content-container .monaco-editor-con .monaco-editor .slider:hover {
149 |     background: #7f7f7f;
150 |   }
151 |   .dark-mode .cli-content-container .monaco-editor-con .monaco-editor .slider:hover {
152 |     background: #6a838f;
153 |   }
154 | 
155 |   /*remove background color*/
156 |   .cli-content-container .monaco-editor .margin {
157 |     background-color: inherit;
158 |   }
159 | 
160 |   .cli-content-container .monaco-editor-con .monaco-editor,
161 |   .cli-content-container .monaco-editor-con .monaco-editor-background,
162 |   .cli-content-container .monaco-editor-con .monaco-editor .inputarea.ime-input {
163 |     background-color: inherit;
164 |   }
165 | </style>
166 | 


--------------------------------------------------------------------------------
/src/components/CommandLog.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 | <el-dialog @open='openDialog' :title="$t('message.command_log')" :visible.sync="visible" custom-class='command-log-dialog' width="90%" append-to-body>
  3 |   <!-- key list -->
  4 |   <div class="command-log-list">
  5 |     <vxe-table
  6 |       ref="commandLogList"
  7 |       size="mini" max-height="100%"
  8 |       border="none" show-overflow="title"
  9 |       :scroll-y="{enabled: true}"
 10 |       :row-config="{isHover: true, height: 24}"
 11 |       :column-config="{resizable: true}"
 12 |       :empty-text="$t('el.table.emptyText')"
 13 |       :data="logsShow">
 14 |       <vxe-column field="time" title="Time" width="90"></vxe-column>
 15 |       <vxe-column field="name" title="Connection" width="168"></vxe-column>
 16 |       <vxe-column field="cmd" title="CMD" width="130" class-name="command-cmd"></vxe-column>
 17 |       <vxe-column field="args" title="Args" min-width="90"></vxe-column>
 18 |       <vxe-column field="cost" title="Cost(ms)" width="90" class-name="command-cost"></vxe-column>
 19 |     </vxe-table>
 20 |   </div>
 21 | 
 22 |   <!-- filter -->
 23 |   <el-input v-model='filter' size='mini' style='max-width: 200px;' :placeholder="$t('message.key_to_search')"></el-input>&nbsp;
 24 |   <!-- show only write commands -->
 25 |   <el-checkbox v-model='showOnlyWrite'>Only Write</el-checkbox>
 26 | 
 27 |   <div slot="footer" class="dialog-footer">
 28 |     <el-button @click="logs=[]">{{ $t('el.colorpicker.clear') }}</el-button>
 29 |     <el-button @click="visible=false">{{ $t('el.messagebox.cancel') }}</el-button>
 30 |   </div>
 31 | </el-dialog>
 32 | </template>
 33 | 
 34 | <script type="text/javascript">
 35 | import { writeCMD } from '@/commands.js';
 36 | import { VxeTable, VxeColumn } from 'vxe-table';
 37 | 
 38 | export default {
 39 |   data() {
 40 |     return {
 41 |       visible: false,
 42 |       logs: [],
 43 |       maxLength: 5000,
 44 |       filter: '',
 45 |       showOnlyWrite: false,
 46 |     };
 47 |   },
 48 |   components: { VxeTable, VxeColumn, },
 49 |   created() {
 50 |     this.$bus.$on('commandLog', (record) => {
 51 |       // hide ping
 52 |       if (record.command.name === 'ping') {
 53 |         return;
 54 |       }
 55 | 
 56 |       this.logs.push({
 57 |         cmd: record.command.name,
 58 |         args: (record.command.name === 'auth') ? '***' : record.command.args.map(item => (item.length > 100 ? (`${item.slice(0, 100)}...`) : item.toString())).join(' '),
 59 |         cost: record.cost.toFixed(2),
 60 |         time: record.time.toTimeString().substr(0, 8),
 61 |         name: record.connectionName,
 62 |       });
 63 | 
 64 |       this.logs.length > this.maxLength && (this.logs = this.logs.slice(-this.maxLength));
 65 |       this.visible && this.scrollToBottom();
 66 |     });
 67 |   },
 68 |   computed: {
 69 |     logsShow() {
 70 |       let { logs } = this;
 71 | 
 72 |       if (this.showOnlyWrite) {
 73 |         logs = logs.filter(item => writeCMD[item.cmd.toUpperCase()]);
 74 |       }
 75 | 
 76 |       if (this.filter) {
 77 |         logs = logs.filter(item => item.cmd.includes(this.filter) || item.args.includes(this.filter));
 78 |       }
 79 | 
 80 |       return logs;
 81 |     },
 82 |   },
 83 |   methods: {
 84 |     show() {
 85 |       this.visible = true;
 86 |     },
 87 |     openDialog() {
 88 |       this.scrollToBottom();
 89 |     },
 90 |     scrollToBottom() {
 91 |       setTimeout(() => {
 92 |         this.$refs.commandLogList &&
 93 |           this.$refs.commandLogList.scrollTo(0, 99999999);
 94 |       }, 0);
 95 |     },
 96 |   },
 97 | };
 98 | </script>
 99 | 
100 | <style type="text/css">
101 |   .command-log-dialog.el-dialog {
102 |     margin-top: 10vh !important;
103 |   }
104 |   .command-log-list {
105 |     padding: 6px;
106 |     min-height: 150px;
107 |     height: calc(90vh - 307px);
108 |     border: 1px solid grey;
109 |     border-radius: 5px;
110 |     margin-bottom: 12px;
111 |   }
112 | 
113 |   .command-log-list .command-cmd {
114 |     font-weight: bold;
115 |     font-size: 110%;
116 |   }
117 |   .command-log-list .command-cost {
118 |     color: #e59090;
119 |   }
120 | </style>
121 | 


--------------------------------------------------------------------------------
/src/components/ConnectionWrapper.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <el-menu
  3 |     ref="connectionMenu"
  4 |     :collapse-transition='false'
  5 |     :id="connectionAnchor"
  6 |     @open="openConnection()"
  7 |     class="connection-menu"
  8 |     active-text-color="#ffd04b">
  9 |     <el-submenu :index="config.connectionName">
 10 |       <!-- connection menu -->
 11 |       <ConnectionMenu
 12 |         slot="title"
 13 |         :config="config"
 14 |         :client='client'
 15 |         @changeColor='setColor'
 16 |         @refreshConnection='openConnection(false, true)'>
 17 |       </ConnectionMenu>
 18 | 
 19 |       <!-- db search operate -->
 20 |       <OperateItem
 21 |         ref='operateItem'
 22 |         :config="config"
 23 |         :client='client'>
 24 |       </OperateItem>
 25 | 
 26 |       <!-- key list -->
 27 |       <KeyList
 28 |         ref='keyList'
 29 |         :config="config"
 30 |         :globalSettings='globalSettings'
 31 |         :client='client'>
 32 |       </KeyList>
 33 |     </el-submenu>
 34 |   </el-menu>
 35 | </template>
 36 | 
 37 | <script type="text/javascript">
 38 | import redisClient from '@/redisClient.js';
 39 | import KeyList from '@/components/KeyList';
 40 | import OperateItem from '@/components/OperateItem';
 41 | import ConnectionMenu from '@/components/ConnectionMenu';
 42 | 
 43 | export default {
 44 |   data() {
 45 |     return {
 46 |       client: null,
 47 |       pingTimer: null,
 48 |       pingInterval: 10000, // ms
 49 |       lastSelectedDb: 0,
 50 |     };
 51 |   },
 52 |   props: ['config', 'globalSettings', 'index'],
 53 |   components: { ConnectionMenu, OperateItem, KeyList },
 54 |   created() {
 55 |     this.$bus.$on('closeConnection', (connectionName = false) => {
 56 |       this.closeConnection(connectionName);
 57 |     });
 58 |     // open connection
 59 |     this.$bus.$on('openConnection', (connectionName) => {
 60 |       if (connectionName && (connectionName == this.config.connectionName)) {
 61 |         this.openConnection();
 62 |         this.$refs.connectionMenu.open(this.config.connectionName);
 63 |       }
 64 |     });
 65 |   },
 66 |   computed: {
 67 |     connectionAnchor() {
 68 |       return `connection-anchor-${this.config.connectionName}`;
 69 |     },
 70 |   },
 71 |   methods: {
 72 |     initShow() {
 73 |       this.$refs.operateItem.initShow();
 74 |       this.$refs.keyList.initShow();
 75 |     },
 76 |     initLastSelectedDb() {
 77 |       const db = parseInt(localStorage.getItem(`lastSelectedDb_${this.config.connectionName}`));
 78 | 
 79 |       if (db > 0 && this.lastSelectedDb != db) {
 80 |         this.lastSelectedDb = db;
 81 |         this.$refs.operateItem && this.$refs.operateItem.setDb(db);
 82 |       }
 83 |     },
 84 |     openConnection(callback = false, forceOpen = false) {
 85 |       // scroll to connection
 86 |       this.scrollToConnection();
 87 |       // recovery last selected db
 88 |       this.initLastSelectedDb();
 89 | 
 90 |       // opened, do nothing
 91 |       if (this.client) {
 92 |         return forceOpen ? this.afterOpenConnection(this.client, callback) : false;
 93 |       }
 94 | 
 95 |       // set searching status first
 96 |       this.$refs.operateItem.searchIcon = 'el-icon-loading';
 97 | 
 98 |       // create a new client
 99 |       const clientPromise = this.getRedisClient(this.config);
100 | 
101 |       clientPromise.then((realClient) => {
102 |         this.afterOpenConnection(realClient, callback);
103 |       }).catch((e) => {});
104 |     },
105 |     afterOpenConnection(client, callback = false) {
106 |       // new connection, not ready
107 |       if (client.status != 'ready') {
108 |         client.on('ready', () => {
109 |           if (client.readyInited) {
110 |             return;
111 |           }
112 | 
113 |           client.readyInited = true;
114 |           // open status tab
115 |           this.$bus.$emit('openStatus', client, this.config.connectionName);
116 |           this.startPingInterval();
117 | 
118 |           this.initShow();
119 |           callback && callback();
120 |         });
121 |       }
122 | 
123 |       // connection is ready
124 |       else {
125 |         this.initShow();
126 |         callback && callback();
127 |       }
128 |     },
129 |     closeConnection(connectionName) {
130 |       // if connectionName is not passed, close all connections
131 |       if (connectionName && (connectionName != this.config.connectionName)) {
132 |         return;
133 |       }
134 | 
135 |       this.$refs.connectionMenu
136 |       && this.$refs.connectionMenu.close(this.config.connectionName);
137 |       this.$bus.$emit('removeAllTab', connectionName);
138 | 
139 |       // clear ping interval
140 |       clearInterval(this.pingTimer);
141 | 
142 |       // reset operateItem items
143 |       this.$refs.operateItem && this.$refs.operateItem.resetStatus();
144 |       // reset keyList items
145 |       this.$refs.keyList && this.$refs.keyList.resetKeyList(true);
146 | 
147 |       this.client && this.client.quit && this.client.quit();
148 |       this.client = null;
149 |     },
150 |     startPingInterval() {
151 |       this.pingTimer = setInterval(() => {
152 |         this.client && this.client.ping().then((reply) => {}).catch((e) => {
153 |           // this.$message.error('Ping Error: ' + e.message);
154 |         });
155 |       }, this.pingInterval);
156 |     },
157 |     getRedisClient(config) {
158 |       // prevent changing back to raw config, such as config.db
159 |       const configCopy = JSON.parse(JSON.stringify(config));
160 |       // select db
161 |       configCopy.db = this.lastSelectedDb;
162 | 
163 |       // ssh client
164 |       if (configCopy.sshOptions) {
165 |         var clientPromise = redisClient.createSSHConnection(
166 |           configCopy.sshOptions, configCopy.host, configCopy.port, configCopy.auth, configCopy,
167 |         );
168 |       }
169 |       // normal client
170 |       else {
171 |         var clientPromise = redisClient.createConnection(
172 |           configCopy.host, configCopy.port, configCopy.auth, configCopy,
173 |         );
174 |       }
175 | 
176 |       clientPromise.then((client) => {
177 |         this.client = client;
178 | 
179 |         client.on('error', (error) => {
180 |           this.$message.error({
181 |             message: `Client On Error: ${error} Config right?`,
182 |             duration: 3000,
183 |             customClass: 'redis-on-error-message',
184 |           });
185 | 
186 |           this.$bus.$emit('closeConnection');
187 |         });
188 |       }).catch((error) => {
189 |         this.$message.error(error.message);
190 |         this.$bus.$emit('closeConnection');
191 |       });
192 | 
193 |       return clientPromise;
194 |     },
195 |     setColor(color, save = true) {
196 |       const ulDom = this.$refs.connectionMenu.$el;
197 |       const className = 'menu-with-custom-color';
198 | 
199 |       // save to setting
200 |       save && this.$storage.editConnectionItem(this.config, { color });
201 | 
202 |       if (!color) {
203 |         ulDom.classList.remove(className);
204 |       } else {
205 |         ulDom.classList.add(className);
206 |         this.$el.style.setProperty('--menu-color', color);
207 |       }
208 |     },
209 |     scrollToConnection() {
210 |       this.$nextTick(() => {
211 |         // 300ms after menu expand animination
212 |         setTimeout(() => {
213 |           let scrollTop = 0;
214 |           const menus = document.querySelectorAll('.connections-wrap .connections-list>ul');
215 | 
216 |           // calc height sum of all above menus
217 |           for (const menu of menus) {
218 |             if (menu.id === this.connectionAnchor) {
219 |               break;
220 |             }
221 |             scrollTop += (menu.clientHeight + 8);
222 |           }
223 | 
224 |           // if connections filter input exists, scroll more
225 |           // 32 = height('.filter-input')+margin
226 |           const offset = document.querySelector('.connections-wrap .filter-input') ? 32 : 0;
227 |           document.querySelector('.connections-wrap').scrollTo({
228 |             top: scrollTop + offset,
229 |             behavior: 'smooth',
230 |           });
231 |         }, 320);
232 |       });
233 |     },
234 |   },
235 |   mounted() {
236 |     this.setColor(this.config.color, false);
237 |   },
238 |   beforeDestroy() {
239 |     this.closeConnection(this.config.connectionName);
240 |   },
241 | };
242 | </script>
243 | 
244 | <style type="text/css">
245 |   /*menu ul*/
246 |   .connection-menu {
247 |     margin-bottom: 8px;
248 |     padding-right: 6px;
249 |     border-right: 0;
250 |   }
251 | 
252 |   .connection-menu.menu-with-custom-color li.el-submenu {
253 |     border-left: 5px solid var(--menu-color);
254 |     border-radius: 4px 0 0 4px;
255 |     padding-left: 3px;
256 |   }
257 | 
258 |   /*this error shows first*/
259 |   .redis-on-error-message {
260 |     z-index:9999 !important;
261 |   }
262 | </style>
263 | 


--------------------------------------------------------------------------------
/src/components/Connections.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div class="connections-wrap">
  3 |     <!-- search connections input -->
  4 |     <div v-if="connections.length>=filterEnableNum" class="filter-input">
  5 |       <el-input
  6 |         v-model="filterMode"
  7 |         suffix-icon="el-icon-search"
  8 |         :placeholder="$t('message.search_connection')"
  9 |         clearable
 10 |         size="mini">
 11 |       </el-input>
 12 |     </div>
 13 | 
 14 |     <!-- connections list -->
 15 |     <div class="connections-list">
 16 |       <ConnectionWrapper
 17 |         v-for="item, index of filteredConnections"
 18 |         :key="item.key ? item.key : item.connectionName"
 19 |         :index="index"
 20 |         :globalSettings="globalSettings"
 21 |         :config='item'>
 22 |       </ConnectionWrapper>
 23 |     </div>
 24 | 
 25 |     <ScrollToTop parentNum='1' :posRight='false'></ScrollToTop>
 26 |   </div>
 27 | </template>
 28 | 
 29 | <script type="text/javascript">
 30 | import storage from '@/storage.js';
 31 | import ConnectionWrapper from '@/components/ConnectionWrapper';
 32 | import ScrollToTop from '@/components/ScrollToTop';
 33 | import Sortable from 'sortablejs';
 34 | 
 35 | 
 36 | export default {
 37 |   data() {
 38 |     return {
 39 |       connections: [],
 40 |       globalSettings: this.$storage.getSetting(),
 41 |       filterEnableNum: 4,
 42 |       filterMode: '',
 43 |     };
 44 |   },
 45 |   components: { ConnectionWrapper, ScrollToTop },
 46 |   created() {
 47 |     this.$bus.$on('refreshConnections', () => {
 48 |       this.initConnections();
 49 |     });
 50 |     this.$bus.$on('reloadSettings', (settings) => {
 51 |       this.globalSettings = settings;
 52 |     });
 53 |   },
 54 |   computed: {
 55 |     filteredConnections() {
 56 |       if (!this.filterMode) {
 57 |         return this.connections;
 58 |       }
 59 | 
 60 |       return this.connections.filter(item => {
 61 |         return item.name.toLowerCase().includes(this.filterMode.toLowerCase());
 62 |       });
 63 |     },
 64 |   },
 65 |   methods: {
 66 |     initConnections() {
 67 |       const connections = storage.getConnections(true);
 68 |       const slovedConnections = [];
 69 |       // this.connections = [];
 70 | 
 71 |       for (const item of connections) {
 72 |         item.connectionName = storage.getConnectionName(item);
 73 |         // fix history bug, prevent db into config
 74 |         delete item.db;
 75 |         slovedConnections.push(item);
 76 |       }
 77 | 
 78 |       this.connections = slovedConnections;
 79 |     },
 80 |     sortOrder() {
 81 |       const dragWrapper = document.querySelector('.connections-list');
 82 |       Sortable.create(dragWrapper, {
 83 |         handle: '.el-submenu__title',
 84 |         animation: 400,
 85 |         direction: 'vertical',
 86 |         onEnd: (e) => {
 87 |           const { newIndex } = e;
 88 |           const { oldIndex } = e;
 89 |           // change in connections
 90 |           const currentRow = this.connections.splice(oldIndex, 1)[0];
 91 |           this.connections.splice(newIndex, 0, currentRow);
 92 |           // store
 93 |           this.$storage.reOrderAndStore(this.connections);
 94 |         },
 95 |       });
 96 |     },
 97 |   },
 98 |   mounted() {
 99 |     this.initConnections();
100 |     this.sortOrder();
101 |   },
102 | };
103 | </script>
104 | 
105 | <style type="text/css">
106 |   .connections-wrap {
107 |     height: calc(100vh - 59px);
108 |     overflow-y: auto;
109 |     margin-top: 11px;
110 |   }
111 |   .connections-wrap .filter-input {
112 |     padding-right: 13px;
113 |     margin-bottom: 4px;
114 |   }
115 |   /* set drag area min height, target to the end will be correct */
116 |   .connections-wrap .connections-list {
117 |     min-height: calc(100vh - 110px);
118 |   }
119 | </style>
120 | 


--------------------------------------------------------------------------------
/src/components/CustomFormatter.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <el-dialog :title="$t('message.custom_formatter')" :visible.sync="visible" append-to-body width='60%'>
  3 |     <!-- new formatter btn -->
  4 |     <el-button size="mini" @click="addDialog=true">+ {{ $t('message.new') }}</el-button>
  5 |     <!-- formatter list -->
  6 |     <el-table :data='formatters'>
  7 |       <el-table-column
  8 |         label="Name"
  9 |         prop="name"
 10 |         width="120">
 11 |       </el-table-column>
 12 |       <el-table-column
 13 |         label="Formatter">
 14 |         <template slot-scope="scope">
 15 |           {{ formatterPreview(scope.row) }}
 16 |         </template>
 17 |       </el-table-column>
 18 |       <el-table-column
 19 |         label="Operation"
 20 |         width="90">
 21 |         <template slot-scope="scope">
 22 |           <el-button icon="el-icon-delete" type="text" @click="removeFormatter(scope.$index)"></el-button>
 23 |           <el-button icon="el-icon-edit-outline" type="text" @click="showEditDialog(scope.row)"></el-button>
 24 |         </template>
 25 |       </el-table-column>
 26 |     </el-table>
 27 | 
 28 |     <!-- new formatter dialog -->
 29 |     <el-dialog :close-on-click-modal='false' :title="!editMode ? $t('message.new') : $t('message.edit')"
 30 |                :visible.sync="addDialog" append-to-body
 31 |                @closed='reset'>
 32 |       <el-form label-position="top" size="mini">
 33 |         <el-form-item label="Name" required>
 34 |           <el-input v-model='formatter.name'></el-input>
 35 |         </el-form-item>
 36 | 
 37 |         <el-form-item label="Command" required>
 38 |           <span slot="label">
 39 |             Command
 40 |             <el-popover
 41 |               placement="top-start"
 42 |               title="Command"
 43 |               trigger="hover">
 44 |               <i slot="reference" class="el-icon-question"></i>
 45 |               <p>Executable file, such as  <el-tag>/bin/bash</el-tag>, <el-tag>/bin/node</el-tag>, <el-tag>xxx.sh</el-tag>, <el-tag>xxx.php</el-tag>, make sure it is executable</p>
 46 |             </el-popover>
 47 |           </span>
 48 |           <FileInput
 49 |             :file.sync='formatter.command'
 50 |             placeholder='/bin/bash'>
 51 |           </FileInput>
 52 |         </el-form-item>
 53 | 
 54 |         <el-form-item label="Params">
 55 |           <span slot="label">
 56 |             Params
 57 |             <el-popover
 58 |               placement="top-start"
 59 |               title="Params"
 60 |               trigger="hover">
 61 |               <i slot="reference" class="el-icon-question"></i>
 62 |               <p>
 63 |                 Command params, such as "--key
 64 |                 <el-tag>{KEY}</el-tag> --value <el-tag>{VALUE}</el-tag>"<hr>
 65 |                 <b>Template variables to be replaced:</b>
 66 |                 <table>
 67 |                   <tr>
 68 |                     <td>[String]</td>
 69 |                     <td><el-tag>{VALUE}</el-tag></td>
 70 |                   </tr>
 71 |                   <tr>
 72 |                     <td>[Hash]</td>
 73 |                     <td><el-tag>{FIELD}</el-tag> <el-tag>{VALUE}</el-tag></td>
 74 |                   </tr>
 75 |                   <tr>
 76 |                     <td>[List]</td>
 77 |                     <td><el-tag>{VALUE}</el-tag></td>
 78 |                   </tr>
 79 |                   <tr>
 80 |                     <td>[Set]</td>
 81 |                     <td><el-tag>{VALUE}</el-tag></td>
 82 |                   </tr>
 83 |                   <tr>
 84 |                     <td>[Zset]</td>
 85 |                     <td><el-tag>{SCORE}</el-tag> <el-tag>{MEMBER}</el-tag></td>
 86 |                   </tr>
 87 |                 </table>
 88 |                 <hr>
 89 |                 If your value is unvisible, you can pass <el-tag>{HEX}</el-tag> instead of <el-tag>{VALUE}</el-tag><br>
 90 |                 then hex such as <i>68656c6c6f20776f726c64</i> will be passed
 91 |                 <hr>
 92 |                 If your value is too long(>8000), it will be writen to a file,<br> you can use <el-tag>{HEX_FILE}</el-tag> to get the path and read in your script,<br>
 93 |                 the content in this file is same with <el-tag>{HEX}</el-tag>
 94 |               </p>
 95 |             </el-popover>
 96 |           </span>
 97 |           <el-input v-model='formatter.params' placeholder='--value "{VALUE}"'></el-input>
 98 |         </el-form-item>
 99 | 
100 |         <el-form-item label="">
101 |           <p>{{ formatterPreview(formatter) }}</p>
102 |         </el-form-item>
103 |       </el-form>
104 | 
105 |       <div slot="footer" class="dialog-footer">
106 |         <el-button @click="addDialog=false">{{ $t('el.messagebox.cancel') }}</el-button>
107 |         <el-button type="primary" @click="editFormatter">{{ $t('el.messagebox.confirm') }}</el-button>
108 |       </div>
109 |     </el-dialog>
110 |   </el-dialog>
111 | </template>
112 | 
113 | <script type="text/javascript">
114 | import storage from '@/storage';
115 | import FileInput from '@/components/FileInput';
116 | 
117 | export default {
118 |   data() {
119 |     return {
120 |       visible: false,
121 |       addDialog: false,
122 |       editMode: false,
123 |       formatter: { name: '', command: '', params: '' },
124 |     };
125 |   },
126 |   components: { FileInput },
127 |   computed: {
128 |     formatters() {
129 |       return storage.getCustomFormatter();
130 |     },
131 |   },
132 |   created() {
133 |     this.$bus.$on('addCustomFormatter', () => {
134 |       this.show();
135 |     });
136 |   },
137 |   methods: {
138 |     show() {
139 |       this.visible = true;
140 |     },
141 |     reset() {
142 |       this.editMode = false;
143 |       this.formatter = { name: '', command: '', params: '' };
144 |     },
145 |     formatterPreview(row) {
146 |       return `${row.command} ${row.params}`;
147 |     },
148 |     showEditDialog(row) {
149 |       this.formatter = row;
150 |       this.addDialog = true;
151 |       this.editMode = true;
152 |     },
153 |     editFormatter() {
154 |       if (!this.formatter.name || !this.formatter.command) {
155 |         return false;
156 |       }
157 | 
158 |       // add mode
159 |       if (!this.editMode) {
160 |         this.formatters.push(this.formatter);
161 |       }
162 | 
163 |       this.saveSetting();
164 |       this.addDialog = false;
165 |     },
166 |     removeFormatter(index) {
167 |       this.formatters.splice(index, 1);
168 |       this.saveSetting();
169 |     },
170 |     saveSetting() {
171 |       storage.saveCustomFormatters(this.formatters);
172 |       this.$bus.$emit('refreshViewers');
173 |     },
174 |   },
175 | };
176 | </script>
177 | 


--------------------------------------------------------------------------------
/src/components/DeleteBatch.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 | <div>
  3 |   <el-card class="box-card del-batch-card">
  4 |     <!-- card title -->
  5 |     <div slot="header" class="clearfix">
  6 |       <span class="del-title"><i class="fa fa-exclamation-triangle"></i> {{ $t('message.keys_to_be_deleted') }}</span>
  7 |       <i v-if="loadingScan||loadingDelete" class='el-icon-loading'></i>
  8 |       <el-tag size="mini">
  9 |         <span v-if="loadingScan">Scanning... </span>
 10 |         <span v-if="loadingDelete">Deleting... </span>
 11 |         Total: {{ allKeysList.length }}
 12 |       </el-tag>
 13 | 
 14 |       <!-- del btn -->
 15 |       <el-button @click="confirmDelete" :disabled="loadingScan||loadingDelete||allKeysList.length == 0" style="float: right;" type="danger">{{ $t('message.delete_all') }}</el-button>
 16 |       <!-- toggle scanning btn -->
 17 |       <el-button v-if="rule.pattern.length && !scanningEnd" @click="toggleScanning()" type="text" style="float: right;">{{loadingScan ? $t('message.pause') : $t('message.begin')}}&nbsp;</el-button>
 18 |     </div>
 19 | 
 20 |     <!-- scan pattern -->
 21 |     <el-tag v-if="rule.pattern && rule.pattern.length" size="mini" style="margin-left: 10px;">
 22 |       <i class="fa fa-search"></i> {{rule.pattern.join(' ')}}
 23 |     </el-tag>
 24 | 
 25 |     <!-- key list -->
 26 |     <RecycleScroller
 27 |       class="del-batch-key-list"
 28 |       :items="allKeysList"
 29 |       :item-size="20"
 30 |       key-field="str"
 31 |       v-slot="{ item, index }"
 32 |     >
 33 |       <li>
 34 |         <span class="list-index">{{ index + 1 }}.</span>
 35 |         <span class="key-name" :title="item.str">{{ item.str }}</span>
 36 |       </li>
 37 |     </RecycleScroller>
 38 |   </el-card>
 39 | </div>
 40 | </template>
 41 | 
 42 | <script type="text/javascript">
 43 | import { RecycleScroller } from 'vue-virtual-scroller';
 44 | 
 45 | export default {
 46 |   data() {
 47 |     return {
 48 |       loadingScan: false,
 49 |       loadingDelete: false,
 50 |       scanStreams: [],
 51 |       allKeysList: [],
 52 |       scanningEnd: false,
 53 |     };
 54 |   },
 55 |   props: ['client', 'rule', 'hotKeyScope'],
 56 |   components: { RecycleScroller },
 57 |   methods: {
 58 |     initKeys() {
 59 |       this.allKeysList = [];
 60 |       this.rule.key && this.rule.key.length && this.addToList(this.rule.key);
 61 | 
 62 |       if (this.rule.pattern && this.rule.pattern.length) {
 63 |         this.loadingScan = true;
 64 | 
 65 |         for (const pattern of this.rule.pattern) {
 66 |           this.initScanStreamsAndScan(pattern);
 67 |         }
 68 |       }
 69 |     },
 70 |     initScanStreamsAndScan(pattern) {
 71 |       const nodes = this.client.nodes ? this.client.nodes('master') : [this.client];
 72 |       this.scanningCount = nodes.length;
 73 | 
 74 |       nodes.map((node) => {
 75 |         const scanOption = {
 76 |           match: `${pattern}*`,
 77 |           count: 20000,
 78 |         };
 79 | 
 80 |         const stream = node.scanBufferStream(scanOption);
 81 |         this.scanStreams.push(stream);
 82 | 
 83 |         stream.on('data', (keys) => {
 84 |           this.addToList(keys.sort());
 85 | 
 86 |           // pause for dom rendering
 87 |           stream.pause();
 88 |           setTimeout(() => {
 89 |             this.loadingScan && stream.resume();
 90 |           }, 100);
 91 |         });
 92 | 
 93 |         stream.on('error', (e) => {
 94 |           this.loadingScan = false;
 95 |           this.$message.error({
 96 |             message: `Delete Batch Stream On Error: ${e.message}`,
 97 |             duration: 1500,
 98 |           });
 99 |         });
100 | 
101 |         stream.on('end', () => {
102 |           // all nodes scan finished(cusor back to 0)
103 |           if (--this.scanningCount <= 0) {
104 |             this.loadingScan = false;
105 |             this.scanningEnd = true;
106 |           }
107 |         });
108 |       });
109 |     },
110 |     addToList(keys) {
111 |       const list = [];
112 |       for (const key of keys) {
113 |         list.push({ key, str: this.$util.bufToString(key) });
114 |       }
115 | 
116 |       this.allKeysList = this.allKeysList.concat(list);
117 |     },
118 |     toggleScanning(forcePause = null) {
119 |       this.loadingScan = (forcePause === null ? !this.loadingScan : !forcePause);
120 | 
121 |       if (this.scanStreams.length) {
122 |         for (const stream of this.scanStreams) {
123 |           this.loadingScan ? stream.resume() : stream.pause();
124 |         }
125 |       }
126 |     },
127 |     confirmDelete() {
128 |       const keys = this.allKeysList;
129 |       const total = keys.length;
130 | 
131 |       if (total <= 0) {
132 |         return;
133 |       }
134 | 
135 |       this.loadingDelete = true;
136 |       let delPromise = null;
137 | 
138 |       // standalone Redis, batch delete
139 |       if (!this.client.nodes) {
140 |         let chunked = [];
141 |         for (let i = 0; i < total; i++) {
142 |           chunked.push(keys[i].key);
143 | 
144 |           // del 5000 keys one time
145 |           if (chunked.length >= 5000) {
146 |             delPromise = this.client.del(chunked);
147 |             chunked = [];
148 |           }
149 |         }
150 | 
151 |         if (chunked.length) {
152 |           delPromise = this.client.del(chunked);
153 |         }
154 |         // use final promise
155 |         delPromise.then((reply) => {
156 |           if (reply > 0) {
157 |             this.afterDelete();
158 |           } else {
159 |             this.deleteFailed(this.$t('message.delete_failed'));
160 |           }
161 |         }).catch((e) => {
162 |           this.deleteFailed(e.message);
163 |         });
164 |       }
165 | 
166 |       // cluster, one key per time instead of batch
167 |       else {
168 |         for (let i = 0; i < total; i++) {
169 |           delPromise = this.client.del(keys[i].key);
170 |           delPromise.catch((e) => {});
171 |         }
172 | 
173 |         // use final promise
174 |         delPromise.then((reply) => {
175 |           if (reply == 1) {
176 |             this.afterDelete();
177 |           } else {
178 |             this.deleteFailed(this.$t('message.delete_failed'));
179 |           }
180 |         }).catch((e) => {
181 |           this.deleteFailed(e.message);
182 |         });
183 |       }
184 |     },
185 |     afterDelete() {
186 |       this.loadingDelete = false;
187 |       this.allKeysList = [];
188 | 
189 |       // empty the specified keys
190 |       // this.rule.key = [];
191 | 
192 |       this.$message.success(this.$t('message.delete_success'));
193 |       this.$bus.$emit('refreshKeyList', this.client);
194 | 
195 |       // except pattern mode scanning not to end, close pre tab
196 |       if (!this.rule.pattern.length || this.scanningEnd) {
197 |         this.$bus.$emit('removePreTab');
198 |       }
199 |     },
200 |     deleteFailed(msg = '') {
201 |       msg && this.$message.error(msg);
202 | 
203 |       this.loadingScan = false;
204 |       this.loadingDelete = false;
205 |     },
206 |     initShortcut() {
207 |       this.$shortcut.bind('ctrl+r, ⌘+r, f5', this.hotKeyScope, () => {
208 |         this.initKeys();
209 |         return false;
210 |       });
211 |     },
212 |   },
213 |   mounted() {
214 |     this.initKeys();
215 |     // disable f5 for streams on event cannot stop
216 |     // this.initShortcut();
217 |   },
218 |   beforeDestroy() {
219 |     // this.$shortcut.deleteScope(this.hotKeyScope);
220 |     // cancel scanning
221 |     this.toggleScanning(true);
222 |   },
223 | };
224 | </script>
225 | 
226 | <style type="text/css" >
227 |   .del-title {
228 |     color: #f56c6c;
229 |     font-weight: bold;
230 |     font-size: 120%;
231 |   }
232 |   .del-batch-card {
233 |     /*margin-top: 10px;*/
234 |   }
235 |   .del-batch-key-list {
236 |     height: calc(100vh - 204px);
237 |     overflow: auto;
238 |     padding-left: 10px;
239 |     list-style: none;
240 |     margin-top: 10px;
241 |   }
242 |   .del-batch-key-list li {
243 |     color: #333;
244 |     font-size: 92%;
245 |     display: flex;
246 |   }
247 |   .dark-mode .del-batch-key-list li {
248 |     color: #f7f7f7;
249 |   }
250 |   .del-batch-key-list li .key-name {
251 |     flex: 1;
252 |     overflow: hidden;
253 |     text-overflow: ellipsis;
254 |     white-space: nowrap;
255 |   }
256 | </style>
257 | 


--------------------------------------------------------------------------------
/src/components/FileInput.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <el-input
 3 |     :value='file'
 4 |     clearable
 5 |     @clear='clearFile'
 6 |     @focus='focus'
 7 |     :placeholder='placeholder'>
 8 |     <template slot="append">
 9 |       <el-button @click='showFileSelector'>...</el-button>
10 |     </template>
11 |   </el-input>
12 | </template>
13 | 
14 | <script type="text/javascript">
15 | import { remote } from 'electron';
16 | 
17 | export default {
18 |   props: {
19 |     file: { default: '' },
20 |     bookmark: { default: '' },
21 |     placeholder: { default: 'Select File' },
22 |   },
23 |   methods: {
24 |     clearFile() {
25 |       this.$emit('update:file', '');
26 |       this.$emit('update:bookmark', '');
27 |     },
28 |     focus(e) {
29 |       // edit is forbidden, input blur
30 |       e.target.blur();
31 |     },
32 |     showFileSelector() {
33 |       remote.dialog.showOpenDialog(remote.getCurrentWindow(), {
34 |         securityScopedBookmarks: true,
35 |         properties: ['openFile', 'showHiddenFiles'],
36 |       }).then((reply) => {
37 |         if (reply.canceled) {
38 |           return;
39 |         }
40 | 
41 |         reply.filePaths && this.$emit('update:file', reply.filePaths[0]);
42 |         reply.bookmarks && this.$emit('update:bookmark', reply.bookmarks[0]);
43 |       }).catch((e) => {
44 |         this.$message.error(`File Input Error: ${e.message}`);
45 |       });
46 |     },
47 |   },
48 | };
49 | </script>
50 | 


--------------------------------------------------------------------------------
/src/components/HotKeys.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 | <el-dialog :title="$t('message.hotkey')" :visible.sync="visible" custom-class='hotkey-tips-dialog' append-to-body>
 3 |   <el-table :data='keys'>
 4 |     <el-table-column
 5 |       prop="key"
 6 |       width="180"
 7 |       label="Key">
 8 |     </el-table-column>
 9 |     <el-table-column
10 |       prop="desc"
11 |       label="Description">
12 |     </el-table-column>
13 |   </el-table>
14 | </el-dialog>
15 | </template>
16 | 
17 | <script type="text/javascript">
18 | export default {
19 |   data() {
20 |     return {
21 |       visible: false,
22 |     };
23 |   },
24 |   computed: {
25 |     keys() {
26 |       return [
27 |         { key: 'Ctrl + n / ⌘ + n', desc: this.$t('message.new_connection') },
28 |         { key: 'Ctrl + , / ⌘ + ,', desc: this.$t('message.settings') },
29 |         { key: 'Ctrl + g / ⌘ + g', desc: this.$t('message.command_log') },
30 |         { key: 'Ctrl + w / ⌘ + w', desc: `${this.$t('message.close')} Tab` },
31 |         { key: '⌘ + h', desc: this.$t('message.hide_window') },
32 |         { key: 'Ctrl + [h/m] / ⌘ + m', desc: this.$t('message.minimize_window') },
33 |         { key: 'Ctrl + Enter / ⌘ + Enter', desc: this.$t('message.maximize_window') },
34 |         { key: 'Ctrl + r / ⌘ + r / F5', desc: `${this.$t('message.refresh_connection')} [Key tab, Info tab]` },
35 |         { key: 'Ctrl + d / ⌘ + d', desc: `${this.$t('el.upload.delete')} [Key tab]` },
36 |         { key: 'Ctrl + s / ⌘ + s', desc: `${this.$t('message.save')} [Key tab]` },
37 |         // {key: 'Ctrl + c / ⌘ + c', desc: 'Ctrl + c [Console tab]'},
38 |         { key: 'Ctrl + l / ⌘ + l', desc: `${this.$t('message.clean_up')} [Console tab]` },
39 |         { key: 'Ctrl / ⌘ + click key', desc: this.$t('message.open_new_tab') },
40 |         { key: 'Ctrl + ? / ⌘ + ?', desc: `${this.$t('message.hotkey')} Tips` },
41 |       ];
42 |     },
43 |   },
44 |   methods: {
45 |     show() {
46 |       this.visible = true;
47 |     },
48 |   },
49 |   mounted() {
50 |     this.$shortcut.bind('ctrl+/, ⌘+/', () => {
51 |       this.show();
52 |       return false;
53 |     });
54 |   },
55 | };
56 | </script>
57 | 


--------------------------------------------------------------------------------
/src/components/InputBinary.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div>
 3 |     <!-- <el-tag v-if="!buffVisible" class='input-binary-tag' size="mini">[Hex]</el-tag> -->
 4 |     <el-input :disabled='disabled' :value='contentDisplay' @change="updateContent($event)" :placeholder="placeholder">
 5 |       <template v-if="!buffVisible" slot="prefix">Hex</template>
 6 |     </el-input>
 7 |   </div>
 8 | </template>
 9 | 
10 | <script type="text/javascript">
11 | export default {
12 |   props: {
13 |     content: { default: () => Buffer.from('') },
14 |     disabled: { type: Boolean, default: false },
15 |     placeholder: { type: String, default: '' },
16 |   },
17 |   computed: {
18 |     contentDisplay() {
19 |       if (!this.content) {
20 |         return '';
21 |       }
22 | 
23 |       return this.$util.bufToString(this.content);
24 |     },
25 |     buffVisible() {
26 |       if (!this.content) {
27 |         return true;
28 |       }
29 | 
30 |       return this.$util.bufVisible(this.content);
31 |     },
32 |   },
33 |   methods: {
34 |     updateContent(value) {
35 |       const newContent = this.buffVisible ? Buffer.from(value) : this.$util.xToBuffer(value);
36 |       this.$emit('update:content', newContent);
37 |     },
38 |   },
39 | };
40 | </script>
41 | 
42 | <style type="text/css">
43 |   .input-binary-tag {
44 |     font-size: 80%;
45 |   }
46 | </style>
47 | 


--------------------------------------------------------------------------------
/src/components/InputPassword.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <el-input :value="value" @input="handleInput" :type="inputType" :placeholder="placeholder">
 3 |     <i v-if="!hidepass" ref="toggler" slot="suffix" class="toggler el-icon-view" @click="togglePassword"></i>
 4 |   </el-input>
 5 | </template>
 6 | 
 7 | <script type="text/javascript">
 8 | export default {
 9 |   data() {
10 |     return {
11 |       inputType: 'password',
12 |       hideTextTime: 6000,
13 |     };
14 |   },
15 |   props: ['value', 'placeholder', 'hidepass'],
16 |   methods: {
17 |     handleInput(newValue) {
18 |       this.$emit('input', newValue);
19 |     },
20 |     togglePassword() {
21 |       clearTimeout(this.recoverTimer);
22 | 
23 |       if (!this.$refs.toggler) {
24 |         return;
25 |       }
26 | 
27 |       // show text
28 |       if (this.inputType == 'password') {
29 |         this.inputType = 'text';
30 |         this.$refs.toggler.classList.add('toggler-text');
31 | 
32 |         // set time to hide text
33 |         this.recoverTimer = setTimeout(() => {
34 |           this.togglePassword();
35 |         }, this.hideTextTime);
36 |       }
37 | 
38 |       // back to password
39 |       else {
40 |         this.inputType = 'password';
41 |         this.$refs.toggler.classList.remove('toggler-text');
42 |       }
43 |     },
44 |   },
45 |   destroyed() {
46 |     clearTimeout(this.recoverTimer);
47 |   },
48 | };
49 | </script>
50 | 
51 | <style type="text/css" scoped>
52 |   .toggler {
53 |     cursor: pointer;
54 |     font-weight: bold;
55 |     margin-right: 4px;
56 |   }
57 |   .toggler:hover {
58 |     color: #6895ee;
59 |   }
60 |   .toggler.toggler-text {
61 |     color: #6895ee;
62 |   }
63 | </style>
64 | 


--------------------------------------------------------------------------------
/src/components/JsonEditor.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div class="text-formated-container">
  3 |     <slot name='default'></slot>
  4 |     <!-- collapse btn -->
  5 |     <div class="collapse-container">
  6 |       <el-button class="collapse-btn"  type="text" @click="toggleCollapse">{{ $t('message.' + collapseText) }}</el-button>
  7 |     </div>
  8 | 
  9 |     <!-- monaco editor div -->
 10 |     <div class="monaco-editor-con" ref="editor"></div>
 11 |   </div>
 12 | </template>
 13 | 
 14 | <script type="text/javascript">
 15 | // import * as monaco from 'monaco-editor';
 16 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
 17 | 
 18 | const JSONbig = require('@qii404/json-bigint')({ useNativeBigInt: false });
 19 | 
 20 | export default {
 21 |   data() {
 22 |     return {
 23 |       collapseText: 'collapse_all',
 24 |     };
 25 |   },
 26 |   props: {
 27 |     content: { type: Array | String, default: () => {} },
 28 |     readOnly: { type: Boolean, default: true },
 29 |   },
 30 |   created() {
 31 |     // listen font family change and reset options
 32 |     // to avoid cursor offset
 33 |     this.$bus.$on('fontInited', this.changeFont);
 34 |   },
 35 |   computed: {
 36 |     newContentStr() {
 37 |       if (typeof this.content === 'string') {
 38 |         return this.content;
 39 |       }
 40 | 
 41 |       return JSONbig.stringify(this.content, null, 4);
 42 |     },
 43 |   },
 44 |   watch: {
 45 |     // refresh
 46 |     content() {
 47 |       this.$nextTick(() => {
 48 |         this.monacoEditor.setValue(this.newContentStr);
 49 |       });
 50 |     },
 51 |   },
 52 |   methods: {
 53 |     getContent() {
 54 |       const content = this.monacoEditor.getValue();
 55 | 
 56 |       if (!this.$util.isJson(content)) {
 57 |         this.$message.error(this.$t('message.json_format_failed'));
 58 |         return false;
 59 |       }
 60 | 
 61 |       return Buffer.from(JSONbig.stringify(JSONbig.parse(content), null, 0));
 62 |     },
 63 |     getRawContent(removeJsonSpace = false) {
 64 |       let content = this.monacoEditor.getValue();
 65 | 
 66 |       if (removeJsonSpace) {
 67 |         if (this.$util.isJson(content)) {
 68 |           content = JSONbig.stringify(JSONbig.parse(content), null, 0);
 69 |         }
 70 |       }
 71 | 
 72 |       return content;
 73 |     },
 74 |     toggleCollapse() {
 75 |       this.collapseText == 'expand_all' ? this.monacoEditor.trigger('fold', 'editor.unfoldAll')
 76 |         : this.monacoEditor.trigger('fold', 'editor.foldAll');
 77 |       this.collapseText = this.collapseText == 'expand_all' ? 'collapse_all' : 'expand_all';
 78 |     },
 79 |     onResize() {
 80 |       // init resizeDebounce
 81 |       if (!this.resizeDebounce) {
 82 |         this.resizeDebounce = this.$util.debounce(() => {
 83 |           this.monacoEditor && this.monacoEditor.layout();
 84 |         }, 200);
 85 |       }
 86 | 
 87 |       this.resizeDebounce();
 88 |     },
 89 |     changeFont(fontFamily) {
 90 |       this.monacoEditor && this.monacoEditor.updateOptions({
 91 |         fontFamily,
 92 |       });
 93 |     },
 94 |   },
 95 | 
 96 |   mounted() {
 97 |     this.monacoEditor = monaco.editor.create(
 98 |       this.$refs.editor,
 99 |       {
100 |         value: this.newContentStr,
101 |         theme: 'vs-dark',
102 |         language: 'json',
103 |         links: false,
104 |         readOnly: this.readOnly,
105 |         cursorStyle: this.readOnly ? 'underline-thin' : 'line',
106 |         lineNumbers: 'off',
107 |         contextmenu: false,
108 |         // set fontsize and family to avoid cursor offset
109 |         fontSize: 14,
110 |         fontFamily: this.$storage.getFontFamily(),
111 |         showFoldingControls: 'always',
112 |         // auto layout, performance cost
113 |         automaticLayout: true,
114 |         wordWrap: 'on',
115 |         // long text indent when wrapped
116 |         wrappingIndent: 'indent',
117 |         // cursor line highlight
118 |         renderLineHighlight: 'none',
119 |         // highlight word when cursor in
120 |         occurrencesHighlight: false,
121 |         // disable scroll one page at last line
122 |         scrollBeyondLastLine: false,
123 |         // hide scroll sign of current line
124 |         hideCursorInOverviewRuler: true,
125 |         // fix #1097 additional vertical cursor
126 |         accessibilitySupport: 'off',
127 |         minimap: {
128 |           enabled: false,
129 |         },
130 |         // vertical line
131 |         guides: {
132 |           indentation: false,
133 |           highlightActiveIndentation: false,
134 |         },
135 |         scrollbar: {
136 |           useShadows: false,
137 |           verticalScrollbarSize: '9px',
138 |           horizontalScrollbarSize: '9px',
139 |         },
140 |       },
141 |     );
142 | 
143 |     // window.addEventListener("resize", this.onResize);
144 |     // this.monacoEditor.getAction('editor.foldLevel3').run();
145 |     // this.monacoEditor.getAction('editor.action.formatDocument').run();
146 |   },
147 |   destroyed() {
148 |     // window.removeEventListener("resize", this.onResize);
149 |     this.monacoEditor.dispose();
150 |     this.$bus.$off('fontInited', this.changeFont);
151 |   },
152 | };
153 | </script>
154 | 
155 | <style type="text/css">
156 |   .text-formated-container .monaco-editor-con {
157 |     min-height: 150px;
158 |     height: calc(100vh - 730px);
159 |     clear: both;
160 |     overflow: hidden;
161 |     background: none;
162 |   }
163 |   /*recovery collapse icon font in monaco*/
164 |   .text-formated-container .monaco-editor .codicon {
165 |     font-family: codicon !important;
166 |   }
167 | 
168 |   /*change default scrollbar style*/
169 |   .text-formated-container .monaco-editor .scrollbar {
170 |     background: #eaeaea;
171 |     border-radius: 4px;
172 |   }
173 |   .dark-mode .text-formated-container .monaco-editor .scrollbar {
174 |     background: #425057;
175 |   }
176 |   .text-formated-container .monaco-editor .scrollbar:hover {
177 |     background: #e0e0dd;
178 |   }
179 |   .dark-mode .text-formated-container .monaco-editor .scrollbar:hover {
180 |     background: #495961;
181 |   }
182 | 
183 |   .text-formated-container .monaco-editor-con .monaco-editor .slider {
184 |     border-radius: 4px;
185 |     background: #c1c1c1;
186 |   }
187 |   .dark-mode .text-formated-container .monaco-editor-con .monaco-editor .slider {
188 |     background: #5a6f7a;
189 |   }
190 |   .text-formated-container .monaco-editor-con .monaco-editor .slider:hover {
191 |     background: #7f7f7f;
192 |   }
193 |   .dark-mode .text-formated-container .monaco-editor-con .monaco-editor .slider:hover {
194 |     background: #6a838f;
195 |   }
196 | 
197 |   /*remove background color*/
198 |   .text-formated-container .monaco-editor .margin {
199 |     background-color: inherit;
200 |   }
201 |   .text-formated-container .monaco-editor-con .monaco-editor,
202 |   .text-formated-container .monaco-editor-con .monaco-editor-background,
203 |   .text-formated-container .monaco-editor-con .monaco-editor .inputarea.ime-input {
204 |     background-color: inherit;
205 |   }
206 | 
207 |   /*json key color*/
208 |   .text-formated-container .monaco-editor-con .mtk4 {
209 |     color: #111111;
210 |   }
211 |   .dark-mode .text-formated-container .monaco-editor-con .mtk4 {
212 |     color: #ebebec;
213 |   }
214 |   /*json val string color*/
215 |   .text-formated-container .monaco-editor-con .mtk5 {
216 |     color: #42b983;
217 |   }
218 |   /*json val number color*/
219 |   .text-formated-container .monaco-editor-con .mtk6 {
220 |     color: #fc1e70;
221 |   }
222 |   /*json bracket color*/
223 |   .text-formated-container .monaco-editor-con .mtk9 {
224 |     color: #111111;
225 |   }
226 |   /*json bracket color*/
227 |   .dark-mode .text-formated-container .monaco-editor-con .mtk9 {
228 |     color: #b6b6b9;
229 |   }
230 | 
231 |   /* common string in json editor*/
232 |   .text-formated-container .monaco-editor-con .mtk1 {
233 |     color: #606266;
234 |   }
235 |   .dark-mode .text-formated-container .monaco-editor-con .mtk1 {
236 |     color: #f3f3f4;
237 |   }
238 | </style>
239 | 


--------------------------------------------------------------------------------
/src/components/KeyDetail.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div>
  3 |     <el-container direction="vertical" class="key-tab-container">
  4 |       <!-- key info -->
  5 |       <KeyHeader
  6 |         ref="keyHeader"
  7 |         :client='client'
  8 |         :redisKey="redisKey"
  9 |         :keyType="keyType"
 10 |         @refreshContent='refreshContent'
 11 |         @dumpCommand='dumpCommand'
 12 |         :hotKeyScope='hotKeyScope'
 13 |         class="key-header-info">
 14 |       </KeyHeader>
 15 | 
 16 |       <!-- key content -->
 17 |       <component
 18 |         ref="keyContent"
 19 |         :is="componentName"
 20 |         :client='client'
 21 |         :redisKey="redisKey"
 22 |         :hotKeyScope='hotKeyScope'
 23 |         class="key-content-container">
 24 |       </component>
 25 |     </el-container>
 26 |   </div>
 27 | </template>
 28 | 
 29 | <script>
 30 | import KeyHeader from '@/components/KeyHeader';
 31 | import KeyContentString from '@/components/contents/KeyContentString';
 32 | import KeyContentHash from '@/components/contents/KeyContentHash';
 33 | import KeyContentSet from '@/components/contents/KeyContentSet';
 34 | import KeyContentZset from '@/components/contents/KeyContentZset';
 35 | import KeyContentList from '@/components/contents/KeyContentList';
 36 | import KeyContentStream from '@/components/contents/KeyContentStream';
 37 | import KeyContentReJson from '@/components/contents/KeyContentReJson';
 38 | 
 39 | export default {
 40 |   data() {
 41 |     return {};
 42 |   },
 43 |   props: ['client', 'redisKey', 'keyType', 'hotKeyScope'],
 44 |   components: {
 45 |     KeyHeader,
 46 |     KeyContentString,
 47 |     KeyContentHash,
 48 |     KeyContentSet,
 49 |     KeyContentZset,
 50 |     KeyContentList,
 51 |     KeyContentStream,
 52 |     KeyContentReJson,
 53 |   },
 54 |   computed: {
 55 |     componentName() {
 56 |       return this.getComponentNameByType(this.keyType);
 57 |     },
 58 |   },
 59 |   methods: {
 60 |     getComponentNameByType(keyType) {
 61 |       const map = {
 62 |         string: 'KeyContentString',
 63 |         hash: 'KeyContentHash',
 64 |         zset: 'KeyContentZset',
 65 |         set: 'KeyContentSet',
 66 |         list: 'KeyContentList',
 67 |         stream: 'KeyContentStream',
 68 |         'ReJSON-RL': 'KeyContentReJson',
 69 |         json: 'KeyContentReJson', // upstash
 70 |         'tair-json': 'KeyContentReJson', // tair
 71 |       };
 72 | 
 73 |       if (map[keyType]) {
 74 |         return map[keyType];
 75 |       }
 76 |       // type not support, such as bf
 77 | 
 78 |       this.$message.error(this.$t('message.key_type_not_support'));
 79 |       return '';
 80 |     },
 81 |     refreshContent() {
 82 |       this.client.exists(this.redisKey).then((reply) => {
 83 |         if (reply == 0) {
 84 |           // clear interval if auto refresh opened
 85 |           // this.$refs.keyHeader.removeInterval();
 86 |           return this.$message.error({
 87 |             message: this.$t('message.key_not_exists'),
 88 |             duration: 1000,
 89 |           });
 90 |         }
 91 | 
 92 |         this.$refs.keyContent && this.$refs.keyContent.initShow();
 93 |       }).catch((e) => {
 94 |         this.$message.error(`Exists Error: ${e.message}`);
 95 |       });
 96 |     },
 97 |     dumpCommand() {
 98 |       this.$refs.keyContent && this.$refs.keyContent.dumpCommand();
 99 |     },
100 |   },
101 | };
102 | </script>
103 | 
104 | <style type="text/css">
105 |   .key-tab-container {
106 |     /*padding-left: 5px;*/
107 |   }
108 |   .key-header-info {
109 |     margin-top: 6px;
110 |   }
111 |   .key-content-container {
112 |     margin-top: 12px;
113 |   }
114 | 
115 |   .content-more-container {
116 |     text-align: center;
117 |     margin-top: 10px;
118 |   }
119 |   .content-more-container .content-more-btn {
120 |     width: 95%;
121 |     padding-top: 5px;
122 |     padding-bottom: 5px;
123 |   }
124 | 
125 |   /*key content table wrapper*/
126 |   .key-content-container .content-table-container {
127 |     height: calc(100vh - 223px);
128 |     margin-top: 10px;
129 |     /*fix vex-table height*/
130 |     overflow-y: hidden;
131 |   }
132 | 
133 |   /* vxe table cell */
134 |   .key-content-container .content-table-container .vxe-cell {
135 |     overflow: hidden !important;
136 |     line-height: 34px;
137 |   }
138 |   /*  vxe table radius*/
139 |   .key-content-container .content-table-container .vxe-table--border-line {
140 |     border-radius: 3px;
141 |   }
142 | 
143 |   /*key-content-string such as String,ReJSON*/
144 |   /*text viewer box*/
145 |   .key-content-string .el-textarea textarea {
146 |     font-size: 14px;
147 |     height: calc(100vh - 231px);
148 |   }
149 |   /*json in monaco editor*/
150 |   .key-content-string .text-formated-container .monaco-editor-con {
151 |     height: calc(100vh - 275px);
152 |   }
153 |   .key-content-string .content-string-save-btn {
154 |     width: 100px;
155 |     float: right;
156 |   }
157 |   /*end of key-content-string*/
158 | </style>
159 | 


--------------------------------------------------------------------------------
/src/components/KeyListNormal.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div>
  3 |     <!-- key list -->
  4 |     <ul class='key-list'>
  5 |       <RightClickMenu
  6 |         :items='rightMenus'
  7 |         :clickValue='key'
  8 |         :key='key.toString()'
  9 |         v-for='key of keyList'>
 10 |         <li class='key-item' :title='key'  @click='clickKey(key, $event)'>{{$util.bufToString(key)}}</li>
 11 |       </RightClickMenu>
 12 |     </ul>
 13 |   </div>
 14 | </template>
 15 | 
 16 | <script type="text/javascript">
 17 | import RightClickMenu from '@/components/RightClickMenu';
 18 | 
 19 | export default {
 20 |   data() {
 21 |     return {
 22 |       rightMenus: [
 23 |         {
 24 |           name: this.$t('message.open'),
 25 |           click: (clickValue, event) => {
 26 |             this.clickKey(clickValue, event, false);
 27 |           },
 28 |         },
 29 |         {
 30 |           name: this.$t('message.open_new_tab'),
 31 |           click: (clickValue, event) => {
 32 |             this.clickKey(clickValue, event, true);
 33 |           },
 34 |         },
 35 |       ],
 36 |     };
 37 |   },
 38 |   props: ['client', 'config', 'keyList'],
 39 |   components: { RightClickMenu },
 40 |   methods: {
 41 |     clickKey(key, event = null, newTab = false) {
 42 |       // highlight clicked key
 43 |       event && this.hightKey(event);
 44 |       event && (event.ctrlKey || event.metaKey) && (newTab = true);
 45 |       this.$bus.$emit('clickedKey', this.client, key, newTab);
 46 |     },
 47 |     hightKey(event) {
 48 |       for (const ele of document.querySelectorAll('.key-select')) {
 49 |         ele.classList.remove('key-select');
 50 |       }
 51 | 
 52 |       if (event) {
 53 |         event.target.classList.add('key-select');
 54 |       }
 55 |     },
 56 |   },
 57 | };
 58 | </script>
 59 | 
 60 | <style type="text/css">
 61 |   .connection-menu .key-list {
 62 |     list-style-type: none;
 63 |     padding-left: 0;
 64 |   }
 65 |   .connection-menu .key-list .key-item {
 66 |     height: 22px;
 67 |     white-space:nowrap;
 68 |     overflow: hidden;
 69 |     text-overflow: ellipsis;
 70 |     cursor: pointer;
 71 |     color: #292f31;
 72 |     font-size: 0.9em;
 73 |     line-height: 1.52;
 74 |     /*margin-right: 3px;*/
 75 |     padding-left: 6px;
 76 |   }
 77 |   .dark-mode .connection-menu .key-list .key-item {
 78 |     color: #f7f7f7;
 79 |   }
 80 |   .connection-menu .key-list .key-item:hover {
 81 |     /*color: #3c3d3e;*/
 82 |     background: #e7ebec;
 83 |   }
 84 |   .dark-mode .connection-menu .key-list .key-item:hover {
 85 |     color: #f7f7f7;
 86 |     background: #50616b;
 87 |   }
 88 |   .connection-menu .key-list .key-item.key-select {
 89 |     color: #0b7ff7;
 90 |     background: #e7ebec;
 91 |     box-sizing: border-box;
 92 |     border-left: 2px solid #68acf3;
 93 |     padding-left: 4px;
 94 |   }
 95 |   .dark-mode .connection-menu .key-list .key-item.key-select {
 96 |     color: #f7f7f7;
 97 |     background: #50616b;
 98 |   }
 99 | </style>
100 | 


--------------------------------------------------------------------------------
/src/components/LanguageSelector.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <!-- language select -->
 3 |   <el-select v-model="selectedLang" @change="changeLang" placeholder="Language">
 4 |     <el-option
 5 |       v-for="item in langItems"
 6 |       :key="item.value"
 7 |       :label="item.label"
 8 |       :value="item.value">
 9 |     </el-option>
10 |   </el-select>
11 | </template>
12 | 
13 | <script type="text/javascript">
14 | export default {
15 |   data() {
16 |     return {
17 |       selectedLang: 'en',
18 |       langItems: [
19 |         { value: 'en', label: 'English' },
20 |         { value: 'cn', label: '简体中文' },
21 |         { value: 'tw', label: '繁體中文' },
22 |         { value: 'tr', label: 'Türkçe' },
23 |         { value: 'ru', label: 'Русский' },
24 |         { value: 'pt', label: 'Português' },
25 |         { value: 'de', label: 'Deutsch' },
26 |         { value: 'fr', label: 'Français' },
27 |         { value: 'ua', label: 'Українською' },
28 |         { value: 'it', label: 'Italiano' },
29 |         { value: 'es', label: 'Español' },
30 |         { value: 'ko', label: '한국어' },
31 |         { value: 'vi', label: 'Tiếng Việt' },
32 |       ],
33 |     };
34 |   },
35 |   methods: {
36 |     changeLang(lang) {
37 |       localStorage.lang = this.selectedLang;
38 |       this.$i18n.locale = this.selectedLang;
39 |     },
40 |   },
41 |   mounted() {
42 |     this.selectedLang = localStorage.lang || this.selectedLang;
43 |   },
44 | };
45 | </script>
46 | 


--------------------------------------------------------------------------------
/src/components/PaginationTable.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div>
 3 |     <el-table
 4 |       :data="pagedData"
 5 |       stripe
 6 |       size="small"
 7 |       border
 8 |       min-height=300
 9 |       >
10 |       <slot name="default"></slot>
11 |     </el-table>
12 | 
13 |     <el-pagination
14 |       class="pagenation-table-page-container"
15 |       v-if="dataAfterFilter.length > pageSize"
16 |       :total="dataAfterFilter.length"
17 |       :page-size="pageSize"
18 |       :current-page.sync="pageIndex"
19 |       layout="total, prev, pager, next"
20 |       background
21 |       >
22 |     </el-pagination>
23 |   </div>
24 | </template>
25 | 
26 | <script type="text/javascript">
27 | export default {
28 |   data() {
29 |     return {
30 |       pageSize: 100,
31 |       pageIndex: 1,
32 |     };
33 |   },
34 |   props: ['data', 'filterKey', 'filterValue'],
35 |   computed: {
36 |     pagedData() {
37 |       const start = (this.pageIndex - 1) * this.pageSize;
38 |       return this.dataAfterFilter.slice(start, start + this.pageSize);
39 |     },
40 |     dataAfterFilter() {
41 |       const { filterKey } = this;
42 |       const { filterValue } = this;
43 | 
44 |       this.$nextTick(() => {
45 |         this.pageIndex = 1;
46 |       });
47 | 
48 |       if (!filterValue || !filterKey) {
49 |         return this.data;
50 |       }
51 | 
52 |       return this.data.filter(line => line[filterKey].toLowerCase().includes(filterValue.toLowerCase()));
53 |     },
54 |   },
55 | };
56 | </script>
57 | 
58 | <style type="text/css">
59 |   .pagenation-table-page-container {
60 |     margin-top: 20px;
61 |   }
62 | </style>
63 | 


--------------------------------------------------------------------------------
/src/components/RightClickMenu.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div @contextmenu.prevent.stop="show($event)">
  3 |     <!-- default slot -->
  4 |     <slot name="default"></slot>
  5 |     <!-- right menu -->
  6 |     <div class="qii404-vue-right-menu" ref="menu">
  7 |       <ul>
  8 |         <li v-for="item of items" @click.stop="clickItem($event, item)">{{ item.name }}</li>
  9 |       </ul>
 10 |     </div>
 11 |   </div>
 12 | </template>
 13 | 
 14 | <script type="text/javascript">
 15 | export default {
 16 |   data() {
 17 |     return {
 18 |       triggerEvent: null,
 19 |     };
 20 |   },
 21 |   props: ['items', 'clickValue'],
 22 |   methods: {
 23 |     show($event) {
 24 |       this.triggerEvent = $event;
 25 |       this.showMenus($event.clientX, $event.clientY);
 26 |       document.addEventListener('click', this.removeMenus);
 27 |     },
 28 |     showMenus(x, y) {
 29 |       this.hideAllMenus();
 30 | 
 31 |       const { menu } = this.$refs;
 32 | 
 33 |       menu.style.left = `${x}px`;
 34 |       menu.style.top = `${y - 5}px`;
 35 |       menu.style.display = 'block';
 36 |     },
 37 |     clickItem($event, item) {
 38 |       if (item.click) {
 39 |         item.click(this.clickValue, this.triggerEvent, $event);
 40 |       }
 41 | 
 42 |       this.removeMenus();
 43 |       this.triggerEvent = null;
 44 |     },
 45 |     removeMenus() {
 46 |       document.removeEventListener('click', this.removeMenus);
 47 |       this.hideAllMenus();
 48 |     },
 49 |     hideAllMenus() {
 50 |       const menus = document.querySelectorAll('.qii404-vue-right-menu');
 51 | 
 52 |       if (menus.length === 0) {
 53 |         return;
 54 |       }
 55 | 
 56 |       for (const menu of menus) {
 57 |         menu.style.display = 'none';
 58 |       }
 59 |     },
 60 |   },
 61 | };
 62 | </script>
 63 | 
 64 | <style type="text/css">
 65 |   .qii404-vue-right-menu {
 66 |     display: none;
 67 |     position: fixed;
 68 |     top: 0;
 69 |     left: 0;
 70 |     padding: 0px;
 71 |     z-index: 99999;
 72 |     overflow: hidden;
 73 |     border-radius: 3px;
 74 |     border: 2px solid lightgrey;
 75 |     background: #fafafa;
 76 |   }
 77 |   .dark-mode .qii404-vue-right-menu {
 78 |     background: #263238;
 79 |   }
 80 | 
 81 |   .qii404-vue-right-menu ul {
 82 |     list-style: none;
 83 |     padding: 0px;
 84 |   }
 85 |   .qii404-vue-right-menu ul li:not(:last-child) {
 86 |     border-bottom: 1px solid lightgrey;
 87 |   }
 88 | 
 89 |   .qii404-vue-right-menu ul li {
 90 |     font-size: 13.4px;
 91 |     padding: 6px 10px;
 92 |     cursor: pointer;
 93 |     color: #263238;
 94 |   }
 95 |   .dark-mode .qii404-vue-right-menu ul li {
 96 |     color: #fff;
 97 |   }
 98 | 
 99 |   .qii404-vue-right-menu ul li:hover {
100 |     background: #e4e2e2;
101 |   }
102 |   .dark-mode .qii404-vue-right-menu ul li:hover {
103 |     background: #344A4E;
104 |   }
105 | </style>
106 | 


--------------------------------------------------------------------------------
/src/components/ScrollToTop.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <transition name="bounce">
  3 |     <div class="to-top-container" :style='style' @click="scrollToTop" v-if="toTopShow">
  4 |       <i class="el-icon-to-top el-icon-arrow-up"></i>
  5 |     </div>
  6 |   </transition>
  7 | </template>
  8 | 
  9 | <script type="text/javascript">
 10 | export default {
 11 |   data() {
 12 |     return {
 13 |       toTopShow: false,
 14 |       scrollTop: 0,
 15 |       realDom: null,
 16 |       minShowHeight: 500,
 17 |     };
 18 |   },
 19 |   computed: {
 20 |     style() {
 21 |       const style = { right: '50px' };
 22 | 
 23 |       if (!this.posRight) {
 24 |         style.right = 'inherit';
 25 |       }
 26 | 
 27 |       return style;
 28 |     },
 29 |   },
 30 |   props: {
 31 |     parentNum: { default: 3 },
 32 |     posRight: { default: true },
 33 |   },
 34 |   methods: {
 35 |     handleScroll() {
 36 |       this.scrollTop = this.realDom.scrollTop;
 37 |       this.toTopShow = (this.scrollTop > this.minShowHeight);
 38 |     },
 39 |     scrollToTop() {
 40 |       let timer = null;
 41 |       const that = this;
 42 | 
 43 |       cancelAnimationFrame(timer);
 44 | 
 45 |       timer = requestAnimationFrame(function fn() {
 46 |         const nowTop = that.realDom.scrollTop;
 47 | 
 48 |         // to top already
 49 |         if (nowTop <= 0) {
 50 |           cancelAnimationFrame(timer);
 51 |           that.toTopShow = false;
 52 |         } else if (nowTop < 50) {
 53 |           that.realDom.scrollTop -= 5;
 54 |           timer = requestAnimationFrame(fn);
 55 |         } else {
 56 |           that.realDom.scrollTop -= nowTop * 0.2;
 57 |           timer = requestAnimationFrame(fn);
 58 |         }
 59 |       });
 60 |     },
 61 |   },
 62 |   mounted() {
 63 |     this.$nextTick(() => {
 64 |       let vueCom = this.$parent;
 65 | 
 66 |       for (let i = 0; i < this.parentNum - 1; i++) {
 67 |         if (!vueCom.$parent) {
 68 |           return;
 69 |         }
 70 | 
 71 |         vueCom = vueCom.$parent;
 72 |       }
 73 | 
 74 |       this.realDom = vueCom.$el;
 75 | 
 76 |       if (!this.realDom) {
 77 |         return;
 78 |       }
 79 |       this.realDom.addEventListener('scroll', this.handleScroll, true);
 80 |     });
 81 |   },
 82 |   destroyed() {
 83 |     this.realDom.removeEventListener('scroll', this.handleScroll, true);
 84 |   },
 85 | };
 86 | </script>
 87 | 
 88 | <style type="text/css">
 89 |   .to-top-container {
 90 |     background-color: #409eff;
 91 |     position: fixed;
 92 |     /*right: 50px;*/
 93 |     bottom: 30px;
 94 |     width: 40px;
 95 |     height: 40px;
 96 |     border-radius: 20px;
 97 |     cursor: pointer;
 98 |     transition: .3s;
 99 |     box-shadow: 0 3px 6px rgba(0, 0, 0, .5);
100 |     opacity: .5;
101 |     z-index: 10000;
102 |   }
103 |   .to-top-container:hover{
104 |     opacity: 1;
105 |   }
106 |   .to-top-container .el-icon-to-top{
107 |     color: #fff;
108 |     display: block;
109 |     line-height: 40px;
110 |     text-align: center;
111 |     font-size: 18px;
112 |   }
113 |   .bounce-enter-active {
114 |     animation: bounce-in .5s;
115 |   }
116 |   .bounce-leave-active {
117 |     animation: bounce-in .5s reverse;
118 |   }
119 |   @keyframes bounce-in {
120 |     0% {
121 |       transform: scale(0);
122 |     }
123 |     50% {
124 |       transform: scale(1.5);
125 |     }
126 |     100% {
127 |       transform: scale(1);
128 |     }
129 |   }
130 | </style>
131 | 


--------------------------------------------------------------------------------
/src/components/SlowLog.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 | <div class="slowlog-container">
  3 |   <el-card class="box-card">
  4 |     <!-- card title -->
  5 |     <div slot="header" class="clearfix">
  6 |       <el-popover trigger="hover">
  7 |         <i slot="reference" class="el-icon-question"></i>
  8 |         Via <b><code>SLOWLOG GET</code></b>, the time threshold is: <b><pre>CONFIG GET slowlog-log-slower-than</pre></b>, and the total number is: <b><pre>CONFIG GET slowlog-max-len</pre></b>
  9 |         Unit: <b>μs, 1000μs = 1ms</b>
 10 |       </el-popover>
 11 | 
 12 |       <span class="card-title">{{ $t('message.slow_log') }}</span>
 13 |       <i v-if="isScanning" class='el-icon-loading'></i>
 14 |     </div>
 15 | 
 16 |     <!-- table header -->
 17 |     <div class="table-header">
 18 |       <span>
 19 |         Command
 20 |       </span>
 21 |       <span @click="toggleOrder" class="reorder-container" title="Sort">
 22 |         <span>Cost</span>
 23 |         <span class="el-icon-d-caret"></span>
 24 |       </span>
 25 |     </div>
 26 | 
 27 |     <!-- content list -->
 28 |     <RecycleScroller
 29 |       v-if="cmdList.length"
 30 |       class="list-body"
 31 |       :items="cmdList"
 32 |       :item-size="24"
 33 |       key-field="id"
 34 |       v-slot="{ item, index }"
 35 |     >
 36 |       <li>
 37 |         <span class="list-index">{{ index + 1 }}.</span>
 38 |         <span class="time" :title="item.timestring">{{ item.timestring.substr(11) }}</span>
 39 |         <span class="cmd" :title="item.cmd">{{ item.cmd }}</span>
 40 |         <!-- <span class="cost" :title="item.cost">{{ item.cost }} ms</span> -->
 41 |         <span class="cost">
 42 |           <el-tag size="mini">{{ item.cost }} ms</el-tag>
 43 |         </span>
 44 |       </li>
 45 |     </RecycleScroller>
 46 | 
 47 |     <!-- empty list -->
 48 |     <div v-else class="list-body empty-list-body">
 49 |       <b class="el-icon-star-on"> No Slow Log</b>
 50 |     </div>
 51 | 
 52 |     <!-- table footer -->
 53 |     <div class="table-footer">
 54 |       <!-- <el-tag>{{ $t('message.max_display', {num: scanMax}) }}</el-tag> -->
 55 |       <el-tag>slowlog-log-slower-than: {{ slowerThan }} &nbsp;&nbsp; slowlog-max-len: {{ maxLen }}</el-tag>
 56 |     </div>
 57 |   </el-card>
 58 | </div>
 59 | </template>
 60 | 
 61 | <script type="text/javascript">
 62 | import { RecycleScroller } from 'vue-virtual-scroller';
 63 | 
 64 | export default {
 65 |   data() {
 66 |     return {
 67 |       cmdList: [],
 68 |       isScanning: false,
 69 |       sortOrder: '',
 70 |       scanMax: 20000,
 71 |       slowerThan: 0,
 72 |       maxLen: 0,
 73 |     };
 74 |   },
 75 |   props: ['client', 'hotKeyScope'],
 76 |   components: { RecycleScroller },
 77 |   methods: {
 78 |     initShow() {
 79 |       this.cmdList = [];
 80 |       this.isScanning = true;
 81 |       this.initCmdList();
 82 |       this.initConfig();
 83 |     },
 84 |     initCmdList() {
 85 |       const nodes = this.client.nodes ? this.client.nodes('master') : [this.client];
 86 | 
 87 |       nodes.map((node) => {
 88 |         const lines = [];
 89 |         node.callBuffer('SLOWLOG', 'GET', this.scanMax).then((reply) => {
 90 |           for (const item of reply) {
 91 |             const line = {
 92 |               id: item[0],
 93 |               timestring: this.toLocalTime(item[1] * 1000),
 94 |               cost: (item[2] / 1000).toFixed(3),
 95 |               cmd: item[3].join(' '),
 96 |               source: item[4],
 97 |               name: item[5],
 98 |             };
 99 | 
100 |             lines.push(line);
101 |           }
102 | 
103 |           this.cmdList = lines;
104 |           this.isScanning = false;
105 |         }).catch((e) => {
106 |           this.isScanning = false;
107 |           this.$message.error(e.message);
108 |         });
109 |       });
110 |     },
111 |     initConfig() {
112 |       this.client.call('CONFIG', 'GET', 'slowlog-log-slower-than').then((reply) => {
113 |         this.slowerThan = reply[1];
114 |       }).catch((e) => {});
115 |       this.client.call('CONFIG', 'GET', 'slowlog-max-len').then((reply) => {
116 |         this.maxLen = reply[1];
117 |       }).catch((e) => {});
118 |     },
119 |     toLocalTime(timestamp) {
120 |       const d = new Date(timestamp);
121 |       const h = `${d.getHours()}`.padStart(2, 0);
122 |       const m = `${d.getMinutes()}`.padStart(2, 0);
123 |       const s = `${d.getSeconds()}`.padStart(2, 0);
124 | 
125 |       const Y = d.getFullYear();
126 |       const M = `${d.getMonth() + 1}`.padStart(2, 0);
127 |       const D = `${d.getDate()}`.padStart(2, 0);
128 | 
129 |       return `${Y}-${M}-${D} ${h}:${m}:${s}`;
130 |     },
131 |     toggleOrder() {
132 |       if (this.isScanning) {
133 |         return;
134 |       }
135 | 
136 |       this.sortOrder = (this.sortOrder == 'desc' ? 'asc' : 'desc');
137 |       this.reOrder();
138 |     },
139 |     reOrder(order = null) {
140 |       if (this.sortOrder == 'asc') {
141 |         this.cmdList.sort((a, b) => a.cost - b.cost);
142 |       } else {
143 |         this.cmdList.sort((a, b) => b.cost - a.cost);
144 |       }
145 |     },
146 |     initShortcut() {
147 |       this.$shortcut.bind('ctrl+r, ⌘+r, f5', this.hotKeyScope, () => {
148 |         // scanning not finished, return
149 |         if (this.isScanning) {
150 |           return false;
151 |         }
152 | 
153 |         this.initShow();
154 |         return false;
155 |       });
156 |     },
157 |   },
158 |   mounted() {
159 |     this.initShow();
160 |     this.initShortcut();
161 |   },
162 |   beforeDestroy() {
163 |     this.$shortcut.deleteScope(this.hotKeyScope);
164 |   },
165 | };
166 | </script>
167 | 
168 | <style type="text/css">
169 |   .slowlog-container .card-title {
170 |     font-weight: bold;
171 |     font-size: 120%;
172 |   }
173 | 
174 |   .slowlog-container .table-header {
175 |     margin: 2px 0 14px 0;
176 |     user-select: none;
177 |     display: flex;
178 |     font-weight: bold;
179 |   }
180 |   .slowlog-container .table-header .reorder-container {
181 |     margin-left: auto;
182 |     cursor: pointer;
183 |   }
184 | 
185 |   /* list body*/
186 |   .slowlog-container .list-body {
187 |     height: calc(100vh - 268px);
188 |     min-height: 100px;
189 |     line-height: 100px;
190 |   }
191 |   .slowlog-container .empty-list-body {
192 |     text-align: center;
193 |   }
194 |   .slowlog-container .list-body li {
195 |     border-bottom: 1px solid #e6e6e6;
196 |     padding: 0 0 0 4px;
197 |     margin-right: 2px;
198 |     font-size: 92%;
199 |     list-style: none;
200 |     display: flex;
201 |     /*same with item-size*/
202 |     line-height: 24px;
203 |   }
204 |   .slowlog-container .list-body .time {
205 |     width: 88px;
206 |   }
207 |   .slowlog-container .list-body .cmd {
208 |     flex: 1;
209 |     overflow: hidden;
210 |     text-overflow: ellipsis;
211 |     white-space: nowrap;
212 |   }
213 |   .slowlog-container .list-body .cost {
214 |     font-size: 90%;
215 |     margin-left: 16px;
216 |     margin-right: 4px;
217 |   }
218 | 
219 |   .dark-mode .slowlog-container .list-body li {
220 |     border-bottom: 1px solid #3b4d57;
221 |   }
222 |   .slowlog-container .list-body li:hover {
223 |     background: #e6e6e6;
224 |   }
225 |   .dark-mode .slowlog-container .list-body li:hover {
226 |     background: #3b4d57;
227 |   }
228 | 
229 |   /* table footer*/
230 |   .slowlog-container .table-footer {
231 |     text-align: center;
232 |     line-height: 40px;
233 |   }
234 | </style>
235 | 


--------------------------------------------------------------------------------
/src/components/UpdateCheck.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 | </template>
  3 | 
  4 | <script type="text/javascript">
  5 | import { ipcRenderer } from 'electron';
  6 | 
  7 | export default {
  8 |   data() {
  9 |     return {
 10 |       manual: false,
 11 |       updateChecking: false,
 12 |       downloadProcessShow: false,
 13 |     };
 14 |   },
 15 |   created() {
 16 |     this.$bus.$on('update-check', (manual = false) => {
 17 |       this.manual = manual;
 18 | 
 19 |       // update checking running...
 20 |       if (this.updateChecking) {
 21 |         return;
 22 |       }
 23 | 
 24 |       this.updateChecking = true;
 25 |       this.$notify.closeAll();
 26 | 
 27 |       ipcRenderer.send('update-check');
 28 |     });
 29 |   },
 30 |   methods: {
 31 |     bindRendererListener() {
 32 |       // already bind listening
 33 |       if (ipcRenderer.binded) {
 34 |         return;
 35 |       }
 36 | 
 37 |       ipcRenderer.binded = true;
 38 | 
 39 |       ipcRenderer.on('update-available', (event, arg) => {
 40 |         this.$notify.closeAll();
 41 | 
 42 |         const ignoreUpdateKey = `IgnoreUpdateVersion_${arg.version}`;
 43 |         // version ignored
 44 |         if (!this.manual && localStorage[ignoreUpdateKey]) {
 45 |           return this.resetDownloadProcess();
 46 |         }
 47 | 
 48 |         this.$confirm(arg.releaseNotes, {
 49 |           title: `${this.$t('message.update_available')}: ${arg.version}`,
 50 |           confirmButtonText: this.$t('message.begin_update'),
 51 |           cancelButtonText: this.$t('message.ignore_this_version'),
 52 |           dangerouslyUseHTMLString: true,
 53 |           duration: 0,
 54 |         }).then(() => {
 55 |           // update btn clicked
 56 |           this.manual = true;
 57 |           ipcRenderer.send('continue-update');
 58 |         }).catch(() => {
 59 |           // ignore this version
 60 |           localStorage[ignoreUpdateKey] = true;
 61 |           this.resetDownloadProcess();
 62 |         });
 63 |       });
 64 | 
 65 |       ipcRenderer.on('update-not-available', (event, arg) => {
 66 |         this.$notify.closeAll();
 67 |         this.resetDownloadProcess();
 68 | 
 69 |         // latest version
 70 |         this.manual && this.$notify.success({
 71 |           title: this.$t('message.update_not_available'),
 72 |           duration: 2000,
 73 |         });
 74 |       });
 75 | 
 76 |       ipcRenderer.on('update-error', (event, arg) => {
 77 |         this.resetDownloadProcess();
 78 | 
 79 |         let message = '';
 80 |         const error = (arg.code ? arg.code : arg.message).toLowerCase();
 81 | 
 82 |         // auto update check at app init
 83 |         if (!this.manual || !error) {
 84 |           return;
 85 |         }
 86 | 
 87 |         // mac not support auto update
 88 |         if (error.includes('zip') && error.includes('file')) {
 89 |           message = this.$t('message.mac_not_support_auto_update');
 90 |         }
 91 | 
 92 |         // err_internet_disconnected err_name_not_resolved err_connection_refused
 93 |         else {
 94 |           message = `${this.$t('message.update_error')}: ${error}`;
 95 |         }
 96 | 
 97 |         this.$notify.error({
 98 |           message,
 99 |           duration: 0,
100 |           dangerouslyUseHTMLString: true,
101 |         });
102 |       });
103 | 
104 |       ipcRenderer.on('download-progress', (event, arg) => {
105 |         if (!this.downloadProcessShow) {
106 |           const h = this.$createElement;
107 | 
108 |           this.$notify({
109 |             message: h('el-progress', {
110 |               props: {
111 |                 percentage: 0,
112 |               },
113 |               ref: 'downloadProgressBar',
114 |             }),
115 |             duration: 0,
116 |             customClass: 'download-progress-container',
117 |           });
118 | 
119 |           this.downloadProcessShow = true;
120 |         }
121 | 
122 |         this.setProgressBar(Math.floor(arg.percent));
123 |       });
124 | 
125 |       ipcRenderer.on('update-downloaded', (event, arg) => {
126 |         // this.$notify.closeAll();
127 |         this.setProgressBar(100);
128 |         this.resetDownloadProcess();
129 |         this.$notify.success({
130 |           title: this.$t('message.update_downloaded'),
131 |           duration: 0,
132 |         });
133 |       });
134 |     },
135 |     setProgressBar(percent) {
136 |       this.downloadProcessShow
137 |       && this.$refs.downloadProgressBar
138 |       && this.$set(this.$refs.downloadProgressBar, 'percentage', percent);
139 |     },
140 |     resetDownloadProcess() {
141 |       this.updateChecking = false;
142 |       this.downloadProcessShow = false;
143 |     },
144 |   },
145 |   mounted() {
146 |     this.bindRendererListener();
147 |   },
148 | };
149 | </script>
150 | 
151 | <style type="text/css">
152 |   .download-progress-container .el-progress {
153 |     width: 280px;
154 |   }
155 | </style>
156 | 


--------------------------------------------------------------------------------
/src/components/contents/KeyContentReJson.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 | <el-form class='key-content-string'>
  3 |   <!-- key content textarea -->
  4 |   <el-form-item>
  5 |     <FormatViewer
  6 |       ref='formatViewer'
  7 |       :content='content'
  8 |       :binary='binary'
  9 |       :redisKey='redisKey'
 10 |       float=''>
 11 |     </FormatViewer>
 12 |   </el-form-item>
 13 | 
 14 |   <!-- save btn -->
 15 |   <el-button ref='saveBtn' type="primary"
 16 |     @click="execSave" title='Ctrl+s' class="content-string-save-btn">
 17 |     {{ $t('message.save') }}
 18 |   </el-button>
 19 | </el-form>
 20 | </template>
 21 | 
 22 | <script>
 23 | import FormatViewer from '@/components/FormatViewer';
 24 | 
 25 | export default {
 26 |   data() {
 27 |     return {
 28 |       content: Buffer.from(''),
 29 |       binary: false,
 30 |     };
 31 |   },
 32 |   props: ['client', 'redisKey', 'hotKeyScope'],
 33 |   components: { FormatViewer },
 34 |   methods: {
 35 |     initShow() {
 36 |       this.client.callBuffer('JSON.GET', [this.redisKey]).then((reply) => {
 37 |         this.content = reply;
 38 |       });
 39 |     },
 40 |     execSave() {
 41 |       const content = this.$refs.formatViewer.getContent();
 42 | 
 43 |       // viewer check failed, do not save
 44 |       if (content === false) {
 45 |         return;
 46 |       }
 47 | 
 48 |       if (!this.$util.isJson(content)) {
 49 |         return this.$message.error(this.$t('message.json_format_failed'));
 50 |       }
 51 | 
 52 |       this.client.call('JSON.SET', [this.redisKey, '
#39;, content]).then((reply) => {
 53 |         if (reply === 'OK') {
 54 |           this.setTTL();
 55 |           this.initShow();
 56 | 
 57 |           this.$message.success({
 58 |             message: this.$t('message.modify_success'),
 59 |             duration: 1000,
 60 |           });
 61 |         } else {
 62 |           this.$message.error({
 63 |             message: this.$t('message.modify_failed'),
 64 |             duration: 1000,
 65 |           });
 66 |         }
 67 |       }).catch((e) => {
 68 |         this.$message.error(e.message);
 69 |       });
 70 |     },
 71 |     setTTL() {
 72 |       const ttl = parseInt(this.$parent.$parent.$refs.keyHeader.keyTTL);
 73 | 
 74 |       if (ttl > 0) {
 75 |         this.client.expire(this.redisKey, ttl).catch((e) => {
 76 |           this.$message.error(`Expire Error: ${e.message}`);
 77 |         }).then((reply) => {});
 78 |       }
 79 |     },
 80 |     initShortcut() {
 81 |       this.$shortcut.bind('ctrl+s, ⌘+s', this.hotKeyScope, () => {
 82 |         // make input blur to fill the new value
 83 |         // this.$refs.saveBtn.$el.focus();
 84 |         this.execSave();
 85 | 
 86 |         return false;
 87 |       });
 88 |     },
 89 |     dumpCommand() {
 90 |       const command = `JSON.SET ${this.$util.bufToQuotation(this.redisKey)} . ${
 91 |         this.$util.bufToQuotation(this.content)}`;
 92 |       this.$util.copyToClipboard(command);
 93 |       this.$message.success({ message: this.$t('message.copy_success'), duration: 800 });
 94 |     },
 95 |   },
 96 |   mounted() {
 97 |     this.initShow();
 98 |     this.initShortcut();
 99 |   },
100 | };
101 | </script>
102 | 
103 | 


--------------------------------------------------------------------------------
/src/components/contents/KeyContentString.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 | <el-form class='key-content-string'>
  3 |   <!-- key content textarea -->
  4 |   <el-form-item>
  5 |     <FormatViewer
  6 |       ref='formatViewer'
  7 |       :content='content'
  8 |       :binary='binary'
  9 |       :redisKey='redisKey'
 10 |       float=''>
 11 |     </FormatViewer>
 12 |   </el-form-item>
 13 | 
 14 |   <!-- save btn -->
 15 |   <el-button ref='saveBtn' type="primary"
 16 |     @click="execSave" title='Ctrl+s' class="content-string-save-btn">
 17 |     {{ $t('message.save') }}
 18 |   </el-button>
 19 | </el-form>
 20 | </template>
 21 | 
 22 | <script>
 23 | import FormatViewer from '@/components/FormatViewer';
 24 | 
 25 | export default {
 26 |   data() {
 27 |     return {
 28 |       content: Buffer.from(''),
 29 |       binary: false,
 30 |     };
 31 |   },
 32 |   props: ['client', 'redisKey', 'hotKeyScope'],
 33 |   components: { FormatViewer },
 34 |   methods: {
 35 |     initShow() {
 36 |       this.client.getBuffer(this.redisKey).then((reply) => {
 37 |         this.content = reply;
 38 |         // this.$refs.formatViewer.autoFormat();
 39 |       });
 40 |     },
 41 |     execSave() {
 42 |       const content = this.$refs.formatViewer.getContent();
 43 | 
 44 |       // viewer check failed, do not save
 45 |       if (content === false) {
 46 |         return;
 47 |       }
 48 | 
 49 |       this.client.set(
 50 |         this.redisKey,
 51 |         content,
 52 |       ).then((reply) => {
 53 |         if (reply === 'OK') {
 54 |           // for compatibility, use expire instead of setex
 55 |           this.setTTL();
 56 |           this.initShow();
 57 | 
 58 |           this.$message.success({
 59 |             message: this.$t('message.modify_success'),
 60 |             duration: 1000,
 61 |           });
 62 |         } else {
 63 |           this.$message.error({
 64 |             message: this.$t('message.modify_failed'),
 65 |             duration: 1000,
 66 |           });
 67 |         }
 68 |       }).catch((e) => {
 69 |         this.$message.error(e.message);
 70 |       });
 71 |     },
 72 |     setTTL() {
 73 |       const ttl = parseInt(this.$parent.$parent.$refs.keyHeader.keyTTL);
 74 | 
 75 |       if (ttl > 0) {
 76 |         this.client.expire(this.redisKey, ttl).catch((e) => {
 77 |           this.$message.error(`Expire Error: ${e.message}`);
 78 |         }).then((reply) => {});
 79 |       }
 80 |     },
 81 |     initShortcut() {
 82 |       this.$shortcut.bind('ctrl+s, ⌘+s', this.hotKeyScope, () => {
 83 |         // make input blur to fill the new value
 84 |         // this.$refs.saveBtn.$el.focus();
 85 |         this.execSave();
 86 | 
 87 |         return false;
 88 |       });
 89 |     },
 90 |     dumpCommand() {
 91 |       const command = `SET ${this.$util.bufToQuotation(this.redisKey)} ${
 92 |         this.$util.bufToQuotation(this.content)}`;
 93 |       this.$util.copyToClipboard(command);
 94 |       this.$message.success({ message: this.$t('message.copy_success'), duration: 800 });
 95 |     },
 96 |   },
 97 |   mounted() {
 98 |     this.initShow();
 99 |     this.initShortcut();
100 |   },
101 | };
102 | </script>
103 | 
104 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerBinary.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div>
 3 |     <!-- </textarea> -->
 4 |     <el-input ref='textInput' :disabled='disabled' type='textarea' v-model='contentDisplay'></el-input>
 5 |   </div>
 6 | </template>
 7 | 
 8 | <script type="text/javascript">
 9 | export default {
10 |   data() {
11 |     return {
12 |       contentDisplay: '',
13 |     };
14 |   },
15 |   props: ['content', 'contentVisible', 'disabled'],
16 |   watch: {
17 |     content(val) {
18 |       // refresh
19 |       this.contentDisplay = this.$util.bufToBinary(val);
20 |     },
21 |   },
22 |   methods: {
23 |     getContent() {
24 |       return this.$util.binaryStringToBuffer(this.contentDisplay);
25 |     },
26 |   },
27 |   mounted() {
28 |     this.contentDisplay = this.$util.bufToBinary(this.content);
29 |   },
30 | };
31 | </script>
32 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerBrotli.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <JsonEditor ref='editor' :content='newContent' :readOnly='false'></JsonEditor>
 3 | </template>
 4 | 
 5 | <script type="text/javascript">
 6 | import JsonEditor from '@/components/JsonEditor';
 7 | const JSONbig = require('@qii404/json-bigint')({ useNativeBigInt: false });
 8 | 
 9 | const zlib = require('zlib');
10 | 
11 | export default {
12 |   components: { JsonEditor },
13 |   props: ['content'],
14 |   computed: {
15 |     newContent() {
16 |       const { formatStr } = this;
17 | 
18 |       if (typeof formatStr === 'string') {
19 |         if (this.$util.isJson(formatStr)) {
20 |           return JSONbig.parse(formatStr);
21 |         }
22 | 
23 |         return formatStr;
24 |       }
25 | 
26 |       return 'Zlib Brotli Parse Failed!';
27 |     },
28 |     formatStr() {
29 |       return this.$util.zippedToString(this.content, 'brotli');
30 |     },
31 |   },
32 |   methods: {
33 |     getContent() {
34 |       const content = this.$refs.editor.getRawContent(true);
35 |       return zlib.brotliCompressSync(content);
36 |     },
37 |     copyContent() {
38 |       return this.formatStr;
39 |     },
40 |   },
41 | };
42 | </script>
43 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerCustom.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <JsonEditor ref='editor' :content='newContent' class='viewer-custom-editor'>
  3 |     <p :title="fullCommand" class="command-preview">
  4 |       <el-button size="mini" class="viewer-custom-copy-raw"
  5 |         :title='$t("message.copy")' icon="el-icon-document" type="text"
  6 |         @click="$util.copyToClipboard(fullCommand)">
  7 |       </el-button>
  8 |       {{ previewCommand }}
  9 |     </p>
 10 |   </JsonEditor>
 11 | </template>
 12 | 
 13 | <script type="text/javascript">
 14 | import storage from '@/storage';
 15 | import shell from 'child_process';
 16 | import JsonEditor from '@/components/JsonEditor';
 17 | import { ipcRenderer } from 'electron';
 18 | 
 19 | export default {
 20 |   data() {
 21 |     return {
 22 |       execResult: '',
 23 |       fullCommand: '',
 24 |       previewCommand: '',
 25 |       previewContentMax: 50,
 26 |       writeHexFileSize: 8000,
 27 |     };
 28 |   },
 29 |   components: { JsonEditor },
 30 |   props: ['content', 'name', 'dataMap', 'redisKey'],
 31 |   computed: {
 32 |     newContent() {
 33 |       if (this.$util.isJson(this.execResult)) {
 34 |         return JSON.parse(this.execResult);
 35 |       }
 36 | 
 37 |       return this.execResult;
 38 |     },
 39 |   },
 40 |   watch: {
 41 |     content() {
 42 |       this.execCommand();
 43 |     },
 44 |   },
 45 |   methods: {
 46 |     getCommand() {
 47 |       const formatter = storage.getCustomFormatter(this.name);
 48 | 
 49 |       if (!formatter) {
 50 |         return false;
 51 |       }
 52 | 
 53 |       const { command } = formatter;
 54 |       const { params } = formatter;
 55 |       const paramsReplaced = this.replaceTemplate(params);
 56 | 
 57 |       return `"${command}" ${paramsReplaced}`;
 58 |     },
 59 |     replaceTemplate(params) {
 60 |       if (!params) {
 61 |         return '';
 62 |       }
 63 | 
 64 |       const dataMap = this.dataMap ? this.dataMap : {};
 65 |       const mapObj = {
 66 |         '{KEY}': this.redisKey,
 67 |         // "{VALUE}": this.content,
 68 |         '{FIELD}': dataMap.key,
 69 |         '{SCORE}': dataMap.score,
 70 |         '{MEMBER}': dataMap.member,
 71 |       };
 72 | 
 73 |       const re = new RegExp(Object.keys(mapObj).join('|'), 'gi');
 74 |       return params.replace(re, matched => mapObj[matched]);
 75 |     },
 76 |     execCommand() {
 77 |       if (!this.content || !this.content.length) {
 78 |         return this.execResult = '';
 79 |       }
 80 | 
 81 |       const command = this.getCommand();
 82 |       const hexStr = this.content.toString('hex');
 83 | 
 84 |       if (!command) {
 85 |         return this.execResult = 'Command Error, Check Config!';
 86 |       }
 87 | 
 88 |       this.fullCommand = command.replace(
 89 |         '{VALUE}',
 90 |         this.content,
 91 |       );
 92 | 
 93 |       // in case of long content in template
 94 |       this.previewCommand = command.replace(
 95 |         '{VALUE}',
 96 |         this.$util.cutString(this.content.toString(), this.previewContentMax),
 97 |       );
 98 | 
 99 |       // if content is too long, write to file simultaneously
100 |       // hex str is about 2 times of real size
101 |       if (hexStr.length > this.writeHexFileSize) {
102 |         ipcRenderer.invoke('getTempPath').then((reply) => {
103 |           // target file name
104 |           const fileName = `ardm_cv_${this.redisKey.toString('hex')}`;
105 |           const filePath = require('path').join(reply, fileName);
106 | 
107 |           require('fs').writeFile(filePath, hexStr, (err) => {
108 |             if (err) {
109 |               return this.$message.error(err);
110 |             }
111 | 
112 |             this.fullCommand = this.fullCommand
113 |               .replace('{HEX_FILE}', filePath)
114 |               .replace('{HEX}', '<Content Too Long, Use {HEX_FILE} Instead!>');
115 |             this.previewCommand = this.previewCommand
116 |               .replace('{HEX_FILE}', filePath)
117 |               .replace('{HEX}', '<Content Too Long, Use {HEX_FILE} Instead!>');
118 | 
119 |             this.exec();
120 |           });
121 |         });
122 |       }
123 |       // common content just exec
124 |       else {
125 |         this.fullCommand = this.fullCommand
126 |           .replace('{HEX}', hexStr)
127 |           .replace('{HEX_FILE}', '<Use {HEX} Instead!>');
128 | 
129 |         this.previewCommand = this.previewCommand
130 |           .replace(
131 |             '{HEX}',
132 |             this.$util.cutString(hexStr, this.previewContentMax),
133 |           )
134 |           .replace('{HEX_FILE}', '<Use {HEX} Instead!>');
135 | 
136 |         this.exec();
137 |       }
138 |     },
139 |     exec() {
140 |       try {
141 |         shell.exec(this.fullCommand, (error, stdout, stderr) => {
142 |           if (error || stderr) {
143 |             this.execResult = error ? error.message : stderr;
144 |           } else {
145 |             this.execResult = stdout.trim();
146 |           }
147 |         });
148 |       } catch (e) {
149 |         return this.execResult = e.message;
150 |       }
151 |     },
152 |   },
153 |   mounted() {
154 |     this.execCommand();
155 |   },
156 | };
157 | </script>
158 | 
159 | <style type="text/css">
160 | .text-formated-container .command-preview {
161 |   color: #9798a7;
162 |   word-break: break-all;
163 |   height: 40px;
164 |   overflow-y: auto;
165 |   line-height: 20px;
166 |   margin-bottom: 2px;
167 | }
168 | /*copy raw command btn*/
169 | .text-formated-container .command-preview .viewer-custom-copy-raw {
170 |   padding: 0;
171 | }
172 | 
173 | /*make monaco less height in custom viewer*/
174 | .key-content-string .text-formated-container.viewer-custom-editor .monaco-editor-con {
175 |   height: calc(100vh - 331px);
176 | /*  min-height: 50px;*/
177 | }
178 | </style>
179 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerDeflate.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <JsonEditor ref='editor' :content='newContent' :readOnly='false'></JsonEditor>
 3 | </template>
 4 | 
 5 | <script type="text/javascript">
 6 | import JsonEditor from '@/components/JsonEditor';
 7 | const JSONbig = require('@qii404/json-bigint')({ useNativeBigInt: false });
 8 | 
 9 | const zlib = require('zlib');
10 | 
11 | export default {
12 |   components: { JsonEditor },
13 |   props: ['content'],
14 |   computed: {
15 |     newContent() {
16 |       const { formatStr } = this;
17 | 
18 |       if (typeof formatStr === 'string') {
19 |         if (this.$util.isJson(formatStr)) {
20 |           return JSONbig.parse(formatStr);
21 |         }
22 | 
23 |         return formatStr;
24 |       }
25 | 
26 |       return 'Zlib Deflate Parse Failed!';
27 |     },
28 |     formatStr() {
29 |       return this.$util.zippedToString(this.content, 'deflate');
30 |     },
31 |   },
32 |   methods: {
33 |     getContent() {
34 |       const content = this.$refs.editor.getRawContent(true);
35 |       return zlib.deflateSync(content);
36 |     },
37 |     copyContent() {
38 |       return this.formatStr;
39 |     },
40 |   },
41 | };
42 | </script>
43 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerDeflateRaw.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <JsonEditor ref='editor' :content='newContent' :readOnly='false'></JsonEditor>
 3 | </template>
 4 | 
 5 | <script type="text/javascript">
 6 | import JsonEditor from '@/components/JsonEditor';
 7 | const JSONbig = require('@qii404/json-bigint')({ useNativeBigInt: false });
 8 | 
 9 | const zlib = require('zlib');
10 | 
11 | export default {
12 |   components: { JsonEditor },
13 |   props: ['content'],
14 |   computed: {
15 |     newContent() {
16 |       const { formatStr } = this;
17 | 
18 |       if (typeof formatStr === 'string') {
19 |         if (this.$util.isJson(formatStr)) {
20 |           return JSONbig.parse(formatStr);
21 |         }
22 | 
23 |         return formatStr;
24 |       }
25 | 
26 |       return 'Zlib DeflateRaw Parse Failed!';
27 |     },
28 |     formatStr() {
29 |       return this.$util.zippedToString(this.content, 'deflateRaw');
30 |     },
31 |   },
32 |   methods: {
33 |     getContent() {
34 |       const content = this.$refs.editor.getRawContent(true);
35 |       return zlib.deflateRawSync(content);
36 |     },
37 |     copyContent() {
38 |       return this.formatStr;
39 |     },
40 |   },
41 | };
42 | </script>
43 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerGzip.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <JsonEditor ref='editor' :content='newContent' :readOnly='false'></JsonEditor>
 3 | </template>
 4 | 
 5 | <script type="text/javascript">
 6 | import JsonEditor from '@/components/JsonEditor';
 7 | const JSONbig = require('@qii404/json-bigint')({ useNativeBigInt: false });
 8 | 
 9 | const zlib = require('zlib');
10 | 
11 | export default {
12 |   components: { JsonEditor },
13 |   props: ['content'],
14 |   computed: {
15 |     newContent() {
16 |       const { formatStr } = this;
17 | 
18 |       if (typeof formatStr === 'string') {
19 |         if (this.$util.isJson(formatStr)) {
20 |           return JSONbig.parse(formatStr);
21 |         }
22 | 
23 |         return formatStr;
24 |       }
25 | 
26 |       return 'Zlib Gzip Parse Failed!';
27 |     },
28 |     formatStr() {
29 |       return this.$util.zippedToString(this.content, 'gzip');
30 |     },
31 |   },
32 |   methods: {
33 |     getContent() {
34 |       const content = this.$refs.editor.getRawContent(true);
35 |       return zlib.gzipSync(content);
36 |     },
37 |     copyContent() {
38 |       return this.formatStr;
39 |     },
40 |   },
41 | };
42 | </script>
43 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerHex.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div>
 3 |     <!-- </textarea> -->
 4 |     <el-input ref='textInput' :disabled='disabled' type='textarea' v-model='contentDisplay'></el-input>
 5 |   </div>
 6 | </template>
 7 | 
 8 | <script type="text/javascript">
 9 | export default {
10 |   data() {
11 |     return {
12 |       contentDisplay: '',
13 |     };
14 |   },
15 |   props: ['content', 'contentVisible', 'disabled'],
16 |   watch: {
17 |     content(val) {
18 |       // refresh
19 |       this.contentDisplay = this.$util.bufToString(val);
20 |     },
21 |   },
22 |   methods: {
23 |     getContent() {
24 |       return this.$util.xToBuffer(this.contentDisplay);
25 |     },
26 |   },
27 |   mounted() {
28 |     this.contentDisplay = this.$util.bufToString(this.content);
29 |   },
30 | };
31 | </script>
32 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerJavaSerialize.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <JsonEditor ref='editor' :content='newContent' :readOnly='true'></JsonEditor>
 3 | </template>
 4 | 
 5 | <script type="text/javascript">
 6 | import JsonEditor from '@/components/JsonEditor';
 7 | import { ObjectInputStream } from 'java-object-serialization';
 8 | 
 9 | export default {
10 |   props: ['content'],
11 |   components: { JsonEditor },
12 |   computed: {
13 |     newContent() {
14 |       try {
15 |         // ref RedisInsight
16 |         const result = (new ObjectInputStream(this.content)).readObject();
17 | 
18 |         if (typeof result !== 'object') {
19 |           return result;
20 |         }
21 | 
22 |         const fields = Array.from(result.fields, ([key, value]) => ({ [key]: value }));
23 |         return { ...result, fields };
24 |       } catch (e) {
25 |         return 'Java unserialize failed!';
26 |       }
27 |     },
28 |   },
29 |   methods: {
30 |     getContent() {
31 |       this.$message.error('Java unserialization is readonly now!');
32 |       return false;
33 |     },
34 |     copyContent() {
35 |       return this.$refs.editor.getRawContent();
36 |     },
37 |   },
38 | };
39 | </script>
40 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerJson.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <JsonEditor ref='editor' :content='newContent' :readOnly='disabled||false'></JsonEditor>
 3 | </template>
 4 | 
 5 | <script type="text/javascript">
 6 | import JsonEditor from '@/components/JsonEditor';
 7 | 
 8 | const JSONbig = require('@qii404/json-bigint')({ useNativeBigInt: false });
 9 | 
10 | export default {
11 |   props: ['content', 'disabled'],
12 |   components: { JsonEditor },
13 |   computed: {
14 |     newContent() {
15 |       try {
16 |         const parsedObj = JSONbig.parse(this.content);
17 |         // if JSON.parse returns string, means raw content like "{\"name\":\"age\"}"
18 |         // (JSON string wrapped with quotation) issue #909
19 |         if (typeof parsedObj === 'string') {
20 |           this.jsonIsString = true;
21 |         }
22 |         return parsedObj;
23 |       } catch (e) {
24 |         // parse failed, return raw content to edit instead of error
25 |         return this.content.toString();
26 |       }
27 |     },
28 |   },
29 |   methods: {
30 |     getContent() {
31 |       const content = this.$refs.editor.getContent();
32 | 
33 |       if (!content) {
34 |         return false;
35 |       }
36 | 
37 |       // json in string, quotation wrapped and escaped,
38 |       if (this.jsonIsString) {
39 |         return JSONbig.stringify(content.toString());
40 |       }
41 | 
42 |       return content;
43 |     },
44 |   },
45 | };
46 | </script>
47 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerMsgpack.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <JsonEditor ref='editor' :content='newContent' :readOnly='false'></JsonEditor>
 3 | </template>
 4 | 
 5 | <script type="text/javascript">
 6 | import JsonEditor from '@/components/JsonEditor';
 7 | import { decode, encode } from 'algo-msgpack-with-bigint';
 8 | 
 9 | const JSONbig = require('@qii404/json-bigint')({ useNativeBigInt: true });
10 | 
11 | 
12 | export default {
13 |   props: ['content'],
14 |   components: { JsonEditor },
15 |   computed: {
16 |     newContent() {
17 |       try {
18 |         return decode(this.content);
19 |       } catch (e) {
20 |         return this.$t('message.msgpack_format_failed');
21 |       }
22 |     },
23 |   },
24 |   methods: {
25 |     getContent() {
26 |       let content = this.$refs.editor.getRawContent();
27 | 
28 |       // raw content is an object
29 |       if (typeof this.newContent !== 'string') {
30 |         try {
31 |           content = JSONbig.parse(content);
32 |         } catch (e) {
33 |           // object parse failed
34 |           this.$message.error({
35 |             message: `Raw content is an object, but now parse object failed: ${e.message}`,
36 |             duration: 6000,
37 |           });
38 | 
39 |           return false;
40 |         }
41 |       }
42 | 
43 |       // encode returns Uint8Array
44 |       return Buffer.from(encode(content));
45 |     },
46 |     copyContent() {
47 |       const content = decode(this.content);
48 |       return (typeof content === 'object') ? JSONbig.stringify(content) : content;
49 |     },
50 |   },
51 | };
52 | </script>
53 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerOverSize.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="size-too-large-viewer">
 3 |     <el-alert
 4 |       :closable='false'
 5 |       :title='alertTitle'
 6 |       type="error">
 7 |     </el-alert>
 8 |     <el-input :disabled='true' type='textarea' :value='contentDisplay'></el-input>
 9 |   </div>
10 | </template>
11 | 
12 | <script type="text/javascript">
13 | export default {
14 |   data() {
15 |     return {
16 |       firstChars: 20000,
17 |     };
18 |   },
19 |   props: ['content', 'contentVisible', 'disabled'],
20 |   computed: {
21 |     contentDisplay() {
22 |       return `${this.$util.bufToString(this.content.slice(0, this.firstChars), false)
23 |       }...Show only the first ${this.firstChars} characters, the rest has been hidden...`;
24 |     },
25 |     alertTitle() {
26 |       return `Size too large, show only the first ${this.firstChars} characters and you cannot edit it.`;
27 |     },
28 |   },
29 | };
30 | </script>
31 | 
32 | <style type="text/css">
33 |   .size-too-large-viewer .el-alert {
34 |     margin: 3px 0 8px 0;
35 |     color: #f56c6c;
36 |     background-color: #f9dbdb;
37 |   }
38 | 
39 |   /*text viewer box*/
40 |   .key-content-string .size-too-large-viewer .el-textarea textarea {
41 |     height: calc(100vh - 290px);
42 |   }
43 | </style>
44 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerPHPSerialize.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <JsonEditor ref='editor' :content='newContent' :readOnly='isPHPClass'></JsonEditor>
 3 | </template>
 4 | 
 5 | <script type="text/javascript">
 6 | import JsonEditor from '@/components/JsonEditor';
 7 | import { unserialize, serialize } from 'php-serialize';
 8 | 
 9 | 
10 | export default {
11 |   props: ['content'],
12 |   data() {
13 |     return {
14 |       isPHPClass: false,
15 |     };
16 |   },
17 |   components: { JsonEditor },
18 |   computed: {
19 |     newContent() {
20 |       try {
21 |         const content = unserialize(this.content, {}, { strict: false });
22 | 
23 |         if (content && content['__PHP_Incomplete_Class_Name']) {
24 |           this.isPHPClass = true;
25 |         }
26 | 
27 |         return content;
28 |       } catch (e) {
29 |         return this.$t('message.php_unserialize_format_failed');
30 |       }
31 |     },
32 |   },
33 |   methods: {
34 |     getContent() {
35 |       let content = this.$refs.editor.getRawContent();
36 | 
37 |       // raw content is an object
38 |       if (typeof this.newContent !== 'string') {
39 |         try {
40 |           content = JSON.parse(content);
41 |         } catch (e) {
42 |           // object parse failed
43 |           this.$message.error({
44 |             message: `Raw content is an object, but now parse object failed: ${e.message}`,
45 |             duration: 6000,
46 |           });
47 | 
48 |           return false;
49 |         }
50 |       }
51 | 
52 |       return serialize(content);
53 |     },
54 |   },
55 | };
56 | </script>
57 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerPickle.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <JsonEditor ref='editor' :content='newContent' :readOnly='true'></JsonEditor>
 3 | </template>
 4 | 
 5 | <script type="text/javascript">
 6 | import JsonEditor from '@/components/JsonEditor';
 7 | import { Parser } from 'pickleparser';
 8 | 
 9 | export default {
10 |   props: ['content'],
11 |   components: { JsonEditor },
12 |   computed: {
13 |     newContent() {
14 |       try {
15 |         return (new Parser()).parse(this.content);
16 |       } catch (e) {
17 |         return 'Pickle parsed failed!';
18 |       }
19 |     },
20 |   },
21 |   methods: {
22 |     getContent() {
23 |       this.$message.error('Pickle is readonly now!');
24 |       return false;
25 |     },
26 |     copyContent() {
27 |       return this.$refs.editor.getRawContent();
28 |     },
29 |   },
30 | };
31 | </script>
32 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerProtobuf.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <JsonEditor ref='editor' :content='newContent' :readOnly='false' class='protobuf-viewer'>
  3 |     <div class="viewer-protobuf-header">
  4 |       <!-- type selector -->
  5 |       <el-select v-model="selectedType" filterable placeholder="Select Type" size="mini" class="type-selector">
  6 |         <el-option
  7 |           v-for="t of types"
  8 |           :key="t"
  9 |           :label="t"
 10 |           :value="t">
 11 |         </el-option>
 12 |       </el-select>
 13 |       <!-- select proto file -->
 14 |       <el-button class="select-proto-btn" type='primary' size="mini" icon="el-icon-upload2" @click="selectProto">Select Proto Files</el-button>
 15 |     </div>
 16 |     <!-- selected files -->
 17 |     <!-- <el-tag v-for="p of proto" :key="p" class="selected-proto-file-tag">{{ p }}</el-tag> -->
 18 |     <hr>
 19 |   </JsonEditor>
 20 | </template>
 21 | 
 22 | <script type="text/javascript">
 23 | import JsonEditor from '@/components/JsonEditor';
 24 | import { getData } from 'rawproto';
 25 | // import * as protobuf from 'protobufjs';
 26 | const protobuf = require('protobufjs/minimal');
 27 | const { dialog } = require('electron').remote;
 28 | 
 29 | export default {
 30 |   data() {
 31 |     return {
 32 |       proto: [],
 33 |       protoRoot: null,
 34 |       types: ['Rawproto'],
 35 |       selectedType: 'Rawproto',
 36 |     };
 37 |   },
 38 |   components: { JsonEditor },
 39 |   props: ['content'],
 40 |   computed: {
 41 |     newContent() {
 42 |       try {
 43 |         if (this.selectedType === 'Rawproto') {
 44 |           return getData(this.content);
 45 |         }
 46 |         const type = this.protoRoot.lookupType(this.selectedType);
 47 |         const message = type.decode(this.content);
 48 |         return message.toJSON();
 49 |       } catch (e) {
 50 |         return 'Protobuf Decode Failed!';
 51 |       }
 52 |     },
 53 |   },
 54 |   methods: {
 55 |     traverseTypes(current) {
 56 |       if (current instanceof protobuf.Type) {
 57 |         this.types.push(current.fullName);
 58 |       }
 59 |       if (current.nestedArray) {
 60 |         current.nestedArray.forEach((nested) => {
 61 |           this.traverseTypes(nested);
 62 |         });
 63 |       }
 64 |     },
 65 |     selectProto() {
 66 |       dialog.showOpenDialog({
 67 |         properties: ['openFile', 'multiSelections'],
 68 |         filters: [
 69 |           {
 70 |             name: '.proto',
 71 |             extensions: ['proto'],
 72 |           },
 73 |         ],
 74 |       }).then((result) => {
 75 |         if (result.canceled) return;
 76 |         this.proto = result.filePaths;
 77 |         this.types = ['Rawproto'];
 78 |         this.selectedType = 'Rawproto';
 79 | 
 80 |         protobuf.load(this.proto).then((root) => {
 81 |           this.protoRoot = root;
 82 |           // init types
 83 |           this.traverseTypes(root);
 84 |           // first type as default
 85 |           if (this.types.length > 0) {
 86 |             this.selectedType = this.types[1];
 87 |           }
 88 |         }).catch((e) => {
 89 |           this.$message.error(e.message);
 90 |         });
 91 |       }).catch((e) => {
 92 |         this.$message.error(e.message);
 93 |       });
 94 |     },
 95 |     getContent() {
 96 |       if (!this.protoRoot) {
 97 |         this.$message.error('Select a correct .proto file');
 98 |         return false;
 99 |       }
100 | 
101 |       if (!this.selectedType || this.selectedType === 'Rawproto') {
102 |         this.$message.error('Select a correct Type to encode');
103 |         return false;
104 |       }
105 | 
106 |       let content = this.$refs.editor.getRawContent();
107 |       const type = this.protoRoot.lookupType(this.selectedType);
108 | 
109 |       try {
110 |         content = JSON.parse(content);
111 |         const err = type.verify(content);
112 | 
113 |         if (err) {
114 |           this.$message.error(`Proto Verify Failed: ${err}`);
115 |           return false;
116 |         }
117 | 
118 |         const message = type.create(content);
119 |         return type.encode(message).finish();
120 |       } catch (e) {
121 |         this.$message.error(this.$t('message.json_format_failed'));
122 |         return false;
123 |       }
124 |     },
125 |     copyContent() {
126 |       return JSON.stringify(this.newContent);
127 |     },
128 |   },
129 | };
130 | </script>
131 | 
132 | <style type="text/css">
133 |   .viewer-protobuf-header {
134 |     display: flex;
135 |     margin-top: 8px;
136 |   }
137 |   .viewer-protobuf-header .type-selector {
138 |     flex: 1;
139 |     margin-right: 10px;
140 |   }
141 |   .viewer-protobuf-header .select-proto-btn {
142 |     margin-top: 2px;
143 |     height: 27px;
144 |   }
145 |   .selected-proto-file-tag {
146 |     margin-right: 4px;
147 |   }
148 | 
149 |   /*text viewer box*/
150 |   .key-content-string .text-formated-container.protobuf-viewer .monaco-editor-con {
151 |     height: calc(100vh - 331px);
152 |   }
153 | </style>
154 | 


--------------------------------------------------------------------------------
/src/components/viewers/ViewerText.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div>
 3 |     <!-- </textarea> -->
 4 |     <el-input ref='textInput' :disabled='disabled' type='textarea' v-model='contentDisplay' @input='inputContent'>
 5 |     </el-input>
 6 |   </div>
 7 | </template>
 8 | 
 9 | <script type="text/javascript">
10 | export default {
11 |   data() {
12 |     return {
13 |       confirmChange: false,
14 |       contentDisplay: '',
15 |       oldContentDisplay: '',
16 |     };
17 |   },
18 |   props: ['content', 'contentVisible', 'disabled'],
19 |   watch: {
20 |     content(val) {
21 |       // refresh
22 |       this.contentDisplay = val.toString();
23 |       this.oldContentDisplay = this.contentDisplay;
24 |     },
25 |   },
26 |   methods: {
27 |     getContent() {
28 |       // not changed
29 |       if (!this.contentVisible && !this.confirmChange) {
30 |         return this.content;
31 |       }
32 | 
33 |       return Buffer.from(this.contentDisplay);
34 |     },
35 |     inputContent(value) {
36 |       // visible content do nothing
37 |       if (this.contentVisible) {
38 |         return;
39 |       }
40 | 
41 |       // confirmed change content
42 |       if (this.confirmChange) {
43 |         return;
44 |       }
45 | 
46 |       this.$confirm(this.$t('message.confirm_modify_unvisible_content')).then(_ => this.confirmChange = true).catch((_) => {
47 |         // recovery the input value
48 |         this.contentDisplay = this.oldContentDisplay;
49 |       });
50 |     },
51 |   },
52 |   mounted() {
53 |     this.contentDisplay = this.content.toString();
54 |     this.oldContentDisplay = this.contentDisplay;
55 |   },
56 | };
57 | </script>
58 | 


--------------------------------------------------------------------------------
/src/i18n/i18n.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue';
 2 | import VueI18n from 'vue-i18n';
 3 | import locale from 'element-ui/lib/locale';
 4 | 
 5 | import enLocale from 'element-ui/lib/locale/lang/en';
 6 | import zhLocale from 'element-ui/lib/locale/lang/zh-CN';
 7 | import zhTwLocale from 'element-ui/lib/locale/lang/zh-TW';
 8 | import trTrLocale from 'element-ui/lib/locale/lang/tr-TR';
 9 | import ruLocale from 'element-ui/lib/locale/lang/ru-RU';
10 | import ptBrLocale from 'element-ui/lib/locale/lang/pt-br';
11 | import deLocale from 'element-ui/lib/locale/lang/de';
12 | import frLocale from 'element-ui/lib/locale/lang/fr';
13 | import uaLocale from 'element-ui/lib/locale/lang/ua';
14 | import itLocale from 'element-ui/lib/locale/lang/it';
15 | import esLocale from 'element-ui/lib/locale/lang/es';
16 | import koLocale from 'element-ui/lib/locale/lang/ko';
17 | import viLocale from 'element-ui/lib/locale/lang/vi';
18 | 
19 | import en from './langs/en';
20 | import cn from './langs/cn';
21 | import tw from './langs/tw';
22 | import tr from './langs/tr';
23 | import ru from './langs/ru';
24 | import pt from './langs/pt';
25 | import de from './langs/de';
26 | import fr from './langs/fr';
27 | import ua from './langs/ua';
28 | import it from './langs/it';
29 | import es from './langs/es';
30 | import ko from './langs/ko';
31 | import vi from './langs/vi';
32 | 
33 | Vue.use(VueI18n);
34 | 
35 | const messages = {
36 |   en: {
37 |     ...en,
38 |     ...enLocale,
39 |   },
40 |   cn: {
41 |     ...cn,
42 |     ...zhLocale,
43 |   },
44 |   tw: {
45 |     ...tw,
46 |     ...zhTwLocale,
47 |   },
48 |   tr: {
49 |     ...tr,
50 |     ...trTrLocale,
51 |   },
52 |   ru: {
53 |     ...ru,
54 |     ...ruLocale,
55 |   },
56 |   pt: {
57 |     ...pt,
58 |     ...ptBrLocale,
59 |   },
60 |   de: {
61 |     ...de,
62 |     ...deLocale,
63 |   },
64 |   fr: {
65 |     ...fr,
66 |     ...frLocale,
67 |   },
68 |   ua: {
69 |     ...ua,
70 |     ...uaLocale,
71 |   },
72 |   it: {
73 |     ...it,
74 |     ...itLocale,
75 |   },
76 |   es: {
77 |     ...es,
78 |     ...esLocale,
79 |   },
80 |   ko: {
81 |     ...ko,
82 |     ...koLocale,
83 |   },
84 |   vi: {
85 |     ...vi,
86 |     ...viLocale,
87 |   },
88 | };
89 | 
90 | const i18n = new VueI18n({
91 |   locale: localStorage.lang || 'en',
92 |   messages,
93 | });
94 | 
95 | locale.i18n((key, value) => i18n.t(key, value));
96 | 
97 | export default i18n;
98 | 


--------------------------------------------------------------------------------
/src/i18n/langs/cn.js:
--------------------------------------------------------------------------------
  1 | const cn = {
  2 |   message: {
  3 |     new_connection: '新建连接',
  4 |     refresh_connection: '刷新',
  5 |     edit_connection: '编辑连接',
  6 |     duplicate_connection: '复制连接',
  7 |     del_connection: '删除连接',
  8 |     close_connection: '关闭连接',
  9 |     add_new_line: '添加新行',
 10 |     dump_to_clipboard: '复制为命令',
 11 |     redis_version: 'Redis版本',
 12 |     process_id: '进程ID',
 13 |     used_memory: '已用内存',
 14 |     used_memory_peak: '内存占用峰值',
 15 |     used_memory_lua: 'Lua占用内存',
 16 |     connected_clients: '客户端连接数',
 17 |     total_connections_received: '历史连接数',
 18 |     total_commands_processed: '历史命令数',
 19 |     key_statistics: '键值统计',
 20 |     all_redis_info: 'Redis信息全集',
 21 |     server: '服务器',
 22 |     memory: '内存',
 23 |     stats: '状态',
 24 |     settings: '基础设置',
 25 |     ui_settings: '外观',
 26 |     feature_settings: '功能',
 27 |     common_settings: '通用',
 28 |     confirm_to_delete_row_data: '确认删除该行数据?',
 29 |     delete_success: '删除成功',
 30 |     delete_failed: '删除失败',
 31 |     modify_success: '修改成功',
 32 |     modify_failed: '修改失败',
 33 |     add_success: '添加成功',
 34 |     add_failed: '添加失败',
 35 |     value_exists: '值已存在',
 36 |     value_not_exists: '该值不存在',
 37 |     refresh_success: '刷新成功',
 38 |     click_enter_to_rename: '点击或者按Enter键来重命名',
 39 |     click_enter_to_ttl: '点击或者按Enter键来修改过期时间',
 40 |     confirm_to_delete_key: '确认删除 {key} ?',
 41 |     confirm_to_rename_key: '确认重命名 {old} -> {new} ?',
 42 |     edit_line: '修改行',
 43 |     auto_refresh: '自动刷新',
 44 |     auto_refresh_tip: '自动刷新开关,每{interval}秒刷新一次',
 45 |     key_not_exists: '键不存在',
 46 |     collapse_all: '全部折叠',
 47 |     expand_all: '全部展开',
 48 |     json_format_failed: 'Json 格式化失败',
 49 |     msgpack_format_failed: 'Msgpack 格式化失败',
 50 |     php_unserialize_format_failed: 'PHP Unserialize 格式化失败',
 51 |     clean_up: '清空',
 52 |     redis_console: 'Redis 控制台',
 53 |     confirm_to_delete_connection: '确认删除连接?',
 54 |     connection_exists: '连接配置已存在',
 55 |     close_to_edit_connection: '编辑前必须关闭连接,要继续么',
 56 |     close_to_connection: '确认关闭连接?',
 57 |     ttl_delete: '设置TTL<=0将删除该key,是否确认?',
 58 |     max_page_reached: '已到达最大页码',
 59 |     add_new_key: '新增Key',
 60 |     enter_new_key: '请先输入新的key名称',
 61 |     key_type: '类型',
 62 |     save: '保存',
 63 |     enter_to_search: 'Enter 键进行搜索',
 64 |     export_success: '导出成功',
 65 |     select_import_file: '选择配置文件',
 66 |     import_success: '导入成功',
 67 |     put_file_here: '将文件拖到此处,或点击选择',
 68 |     config_connections: '连接配置',
 69 |     import: '导入',
 70 |     export: '导出',
 71 |     open: '打开',
 72 |     close: '关闭',
 73 |     open_new_tab: '新窗口打开',
 74 |     exact_search: '精确搜索',
 75 |     enter_to_exec: '输入Redis命令后,按Enter键执行,上下键切换历史',
 76 |     pre_version: '当前版本',
 77 |     manual_update: '手动下载',
 78 |     retry_too_many_times: '尝试重连次数过多,请检查Server状态',
 79 |     key_to_search: '输入关键字搜索',
 80 |     search_connection: '搜索链接',
 81 |     begin_update: '更新',
 82 |     ignore_this_version: '忽略该版本',
 83 |     check_update: '检查更新',
 84 |     update_checking: '检查更新中, 请稍后...',
 85 |     update_available: '发现新版本',
 86 |     update_not_available: '当前为最新版本',
 87 |     update_error: '更新失败',
 88 |     update_downloading: '下载中...',
 89 |     update_download_progress: '下载进度',
 90 |     update_downloaded: '更新下载完成,重启客户端生效.\
 91 |     [Tips]: 如果您使用的是Windows,关闭软件后,请等待桌面图标刷新到正常状态(约10秒),然后再重新打开即可',
 92 |     mac_not_support_auto_update: 'Mac暂时不支持自动更新,请手动<a href="https://github.com/qishibo/AnotherRedisDesktopManager/releases">下载</a>后重新安装,\
 93 |     或者执行<br><code>brew reinstall --cask another-redis-desktop-manager </code>\
 94 |     <br><hr><br>❤️如果您觉得好用,可以通过<a href="https://apps.apple.com/app/id1516451072">AppStore</a>赞助,并由AppStore帮您自动更新',
 95 |     font_family: '字体选择',
 96 |     font_faq_title: '字体设置说明',
 97 |     font_faq: '1. 可以设置多个字体<br>2. 字体选择是有序的,建议首先选择英文字体,然后再选择中文字体<br>\
 98 |     3. 某些异常情况无法加载系统字体列表时,可以手动输入已安装字体名称',
 99 |     private_key_faq: '目前支持RSA格式私钥,即以<pre>-----BEGIN RSA PRIVATE KEY-----</pre>开头,\
100 |     以<pre>-----BEGIN OPENSSH PRIVATE KEY-----</pre>开头的,需要执行\
101 |     <pre>ssh-keygen -p -m pem -f ~/.ssh/id_rsa</pre>进行格式转换后再使用,该操作不会影响以前的私钥登陆',
102 |     dark_mode: '深色模式',
103 |     load_more_keys: '加载更多',
104 |     key_name: '键名',
105 |     project_home: '项目主页',
106 |     cluster_faq: '选择集群中任一节点配置填入即可,会自动识别其它节点',
107 |     redis_status: 'Redis信息',
108 |     confirm_flush_db: '确认删除db{db}中的所有键值么?',
109 |     flushdb: '删除所有键',
110 |     flushdb_prompt: '请输入 "{txt}"',
111 |     info_disabled: 'Info命令执行异常(可能已被禁用),无法显示Redis信息',
112 |     page_zoom: '页面缩放',
113 |     scan_disabled: 'Scan命令执行异常(可能已被禁用),无法显示Key列表',
114 |     key_type_not_support: '该类型暂不支持可视化展示,请使用命令行进行操作',
115 |     delete_folder: '扫描并删除整个文件夹',
116 |     multiple_select: '多项选择',
117 |     copy: '复制',
118 |     copy_success: '复制成功',
119 |     keys_to_be_deleted: '即将删除的键值',
120 |     delete_all: '全部删除',
121 |     clear_cache: '清除缓存',
122 |     mark_color: '标记颜色',
123 |     key_no_permission: '文件读取权限已过期,请手动重新选择密钥文件',
124 |     toggle_check_all: '全选 | 取消全选',
125 |     select_lang: '选择语言',
126 |     clear_cache_tip: '当客户端出现问题时,该操作会删除所有连接和配置,用于恢复客户端',
127 |     detail: '详情',
128 |     separator_tip: '树状显示的分隔符,设置为空可以禁用树状图,直接以列表展示',
129 |     confirm_modify_unvisible_content: '内容中包含不可见字符,你可以在Hex视图中进行安全编辑。如果继续在Text视图中编辑可能会导致编码错误,确定继续么?',
130 |     keys_per_loading: '加载数量',
131 |     keys_per_loading_tip: '每次加载的key数量, 设置过大可能会影响性能',
132 |     host: '地址',
133 |     port: '端口',
134 |     username: '用户名',
135 |     password: '密码',
136 |     connection_name: '连接名称',
137 |     separator: '分隔符',
138 |     timeout: '超时',
139 |     private_key: '私钥',
140 |     public_key: '公钥',
141 |     authority: '授权',
142 |     redis_node_password: 'Redis节点密码',
143 |     master_group_name: 'Master组名称',
144 |     command_log: '日志',
145 |     sentinel_faq: '多个哨兵任选其一即可,地址、端口、密码请填写哨兵配置,Redis节点密码为哨兵监听的Master节点密码',
146 |     hotkey: '快捷键',
147 |     persist: '持久化',
148 |     custom_formatter: '自定义格式化',
149 |     edit: '编辑',
150 |     new: '新增',
151 |     custom: '自定义',
152 |     hide_window: '隐藏窗口',
153 |     minimize_window: '最小化窗口',
154 |     maximize_window: '最大化窗口',
155 |     load_all_keys: '加载所有',
156 |     show_load_all_keys: '启用按钮以加载所有键',
157 |     load_all_keys_tip: '一次性加载所有key,当key的数量过多时,有可能会导致客户端卡顿,请酌情使用',
158 |     tree_node_overflow: 'key或者文件夹数量过多,仅保留{num}个进行展示。如未找到所需key,建议使用模糊搜索,或者设置分隔符来将key分散到文件夹中',
159 |     connection_readonly: '只读模式,禁止新增、编辑和删除',
160 |     memory_analysis: '内存分析',
161 |     begin: '开始',
162 |     pause: '暂停',
163 |     restart: '重新开始',
164 |     max_display: '最大显示数量: {num}',
165 |     max_scan: '最大扫描数量: {num}',
166 |     close_left: '关闭左侧标签',
167 |     close_right: '关闭右侧标签',
168 |     close_other: '关闭其他标签',
169 |     slow_log: '慢查询',
170 |     load_current_folder: '只加载该文件夹',
171 |     custom_name: '自定义名称',
172 |     theme_select: '主题模式',
173 |     theme_system: '跟随系统',
174 |     theme_light: '亮色模式',
175 |     theme_dark: '暗色模式',
176 |   },
177 | };
178 | 
179 | export default cn;
180 | 


--------------------------------------------------------------------------------
/src/i18n/langs/ko.js:
--------------------------------------------------------------------------------
  1 | const ko = {
  2 |   message: {
  3 |     new_connection: '새 연결',
  4 |     refresh_connection: '새로고침',
  5 |     edit_connection: '연결 편집',
  6 |     duplicate_connection: '연결 복제',
  7 |     del_connection: '연결 삭제',
  8 |     close_connection: '연결 해제',
  9 |     add_new_line: '새 행 추가',
 10 |     dump_to_clipboard: '명령어 복사',
 11 |     redis_version: '레디스 버전',
 12 |     process_id: '프로세스 ID',
 13 |     used_memory: '사용중인 메모리',
 14 |     used_memory_peak: '최대 메모리 사용율',
 15 |     used_memory_lua: 'Lua 메모리 사용율',
 16 |     connected_clients: '클라이언트 연결',
 17 |     total_connections_received: '총 연결수',
 18 |     total_commands_processed: '총 명령어수',
 19 |     key_statistics: '키 통계',
 20 |     all_redis_info: '모든 레디스 정보',
 21 |     server: '서버',
 22 |     memory: '메모리',
 23 |     stats: '상태',
 24 |     settings: '설정',
 25 |     ui_settings: '모양',
 26 |     feature_settings: 'Function',
 27 |     common_settings: '일반',
 28 |     confirm_to_delete_row_data: '데이터 행을 삭제하시겠습니까?',
 29 |     delete_success: '삭제하였습니다.',
 30 |     delete_failed: '삭제 실패',
 31 |     modify_success: '수정하였습니다.',
 32 |     modify_failed: '수정 실패',
 33 |     add_success: '추가하였습니다.',
 34 |     add_failed: '추가 실패',
 35 |     value_exists: '값이 존재합니다',
 36 |     value_not_exists: '값이 존재하지 않습니다',
 37 |     refresh_success: '새로고침 성공',
 38 |     click_enter_to_rename: '클릭 또는 엔터로 이름 변경',
 39 |     click_enter_to_ttl: '클릭 또는 엔터로 TTL 변경',
 40 |     confirm_to_delete_key: '{key} 키를 삭제하시겠습니까?',
 41 |     confirm_to_rename_key: '{old} -> {new} 이름으로 변경하시겠습니까?',
 42 |     edit_line: '행 편집',
 43 |     auto_refresh: '자동 새로고침',
 44 |     auto_refresh_tip: '매 {interval}초마다 새로고침',
 45 |     key_not_exists: '존재하지 않는 키',
 46 |     collapse_all: '모두 접기',
 47 |     expand_all: '모두 펴기',
 48 |     json_format_failed: 'Json 변환 실패',
 49 |     msgpack_format_failed: 'Msgpack 변환 실패',
 50 |     php_unserialize_format_failed: 'PHP Unserialize 실패',
 51 |     clean_up: '비우기',
 52 |     redis_console: '레디스 콘솔',
 53 |     confirm_to_delete_connection: '연결을 삭제하시겠습니까?',
 54 |     connection_exists: '연결 설정이 이미 존재합니다',
 55 |     close_to_edit_connection: '편집하기 전에 연결을 종료합니다.',
 56 |     close_to_connection: '연결을 종료하시겠습니까?',
 57 |     ttl_delete: 'TTL<=0 으로 설정하면 키가 바로 삭제됩니다.',
 58 |     max_page_reached: '마지막 페이지',
 59 |     add_new_key: '새 키',
 60 |     enter_new_key: '새 키의 이름을 먼저 입력해주세요',
 61 |     key_type: '키 유형',
 62 |     save: '저장',
 63 |     enter_to_search: '엔터로 검색',
 64 |     export_success: '내보내기를 완료하였습니다.',
 65 |     select_import_file: '파일을 선택해주세요',
 66 |     import_success: '불러오기를 완료하였습니다.',
 67 |     put_file_here: '파일을 이 곳으로 드래그하거나 클릭하여 선택해주세요',
 68 |     config_connections: '연결',
 69 |     import: '불러오기',
 70 |     export: '내보내기',
 71 |     open: '열기',
 72 |     close: '닫기',
 73 |     open_new_tab: '새 탭에서 열기',
 74 |     exact_search: '완전일치 검색',
 75 |     enter_to_exec: '엔터를 통해 명령을 실행하고, 방향키 위아래로 실행기록을 전환할 수 있습니다.',
 76 |     pre_version: '버전',
 77 |     manual_update: '직접 다운로드',
 78 |     retry_too_many_times: '너무 많이 재연결을 시도하였습니다. 서버 상태를 확인해주세요.',
 79 |     key_to_search: '키워드 검색',
 80 |     search_connection: '검색 연결',
 81 |     begin_update: '업데이트',
 82 |     ignore_this_version: '이 버전 무시',
 83 |     check_update: '업데이트 확인',
 84 |     update_checking: '업데이트를 확인중입니다, 잠시만 기다려주세요...',
 85 |     update_available: '새 버전을 찾았습니다.',
 86 |     update_not_available: '최신버전입니다.',
 87 |     update_error: '업데이트 실패',
 88 |     update_downloading: '다운로드 중...',
 89 |     update_download_progress: '다운로드 진행률',
 90 |     update_downloaded: '업데이트가 완료되었습니다. 앱을 재시작해주세요.\
 91 |     [도움말]: Windows를 사용하는 경우 앱을 종료한 후 바탕 화면 아이콘이 정상 상태(약 10초)로 새로 고쳐질 때까지 기다린 후 다시 열 수 있습니다.',
 92 |     mac_not_support_auto_update: 'Mac은 자동 업데이트를 지원하지 않습니다. <a href="https://github.com/qishibo/AnotherRedisDesktopManager/releases">직접 다운로드</a>하여 재설치해주세요.\
 93 |     또는 <br><code>brew reinstall --cask another-redis-desktop-manager</code> 명령을 실행하십시오.\
 94 |     <br><hr><br>❤️만약 이 프로그램이 당신에게 유용하다면 <a href="https://apps.apple.com/app/id1516451072">앱스토어</a>를 통하여 후원하실 수 있습니다. 또한, 앱스토어는 자동으로 업데이트됩니다.',
 95 |     font_family: '폰트 패밀리',
 96 |     font_faq_title: '폰트 설정 지침',
 97 |     font_faq: '1. 여러 폰트를 선택할 수 있습니다.<br>\
 98 |     2. 폰트는 선택된 순서대로 로드됩니다. 영문 폰트를 먼저 선택하고 당신의 언어 폰트를 선택하는 것을 추천합니다.<br>\
 99 |     3. 시스템 폰트가 목록에 없는 경우, 설치된 폰트명을 직접 입력할 수 있습니다.',
100 |     private_key_faq: '다음으로 시작하는 RSA 유형의 개인키를 지원합니다, <pre>-----BEGIN RSA PRIVATE KEY-----</pre>\
101 |     또는 <pre>-----BEGIN OPENSSH PRIVATE KEY-----</pre>형식 변경이 필요하다면 <pre>ssh-keygen -p -m pem -f ~/.ssh/id_rsa</pre>이 명령은 이전 개인키 로그인에 영향을 미치지 않습니다.',
102 |     dark_mode: '다크 모드',
103 |     load_more_keys: '더 불러오기',
104 |     key_name: '키 명',
105 |     project_home: '프로젝트 홈',
106 |     cluster_faq: '클러스터의 노드 중 아무거나 하나에 대하여 입력하면 다른 노드들은 자동으로 인식합니다.',
107 |     redis_status: '레디스 정보',
108 |     confirm_flush_db: '{db} 데이터베이스의 모든 키를 삭제하시겠습니까?',
109 |     flushdb: '데이터베이스 비우기',
110 |     flushdb_prompt: '진행하려면 "{txt}"를 입력해주세요.',
111 |     info_disabled: 'Info 명령 실행 예외(비활성화되었을 수 있음), Redis 정보를 표시할 수 없습니다.',
112 |     page_zoom: '화면 확대',
113 |     scan_disabled: '스캔 명령 실행 예외(비활성화되었을 수 있음), 키 목록을 표시할 수 없습니다.',
114 |     key_type_not_support: '이 유형에는 시각화 표현이 지원되지 않습니다. 콘솔을 이용해주세요.',
115 |     delete_folder: '모든 폴더를 스캔하고 삭제',
116 |     multiple_select: '다중 선택',
117 |     copy: '복사',
118 |     copy_success: '복사 되었습니다.',
119 |     keys_to_be_deleted: '삭제할 키',
120 |     delete_all: '모두 삭제',
121 |     clear_cache: '캐시 비우기',
122 |     mark_color: '라벨 색상',
123 |     key_no_permission: '파일 읽기 권한이 만료되었습니다, 파일을 직접 재선택해주세요.',
124 |     toggle_check_all: '모두 선택 | 모두 해제',
125 |     select_lang: '언어 선택',
126 |     clear_cache_tip: '클라이언트에 문제가 있을 때, 이 작업을 통해 모든 연결과 환경설정을 삭제하여 클라이언트를 복구할 수 있습니다.',
127 |     detail: '상세정보',
128 |     separator_tip: '트리뷰를 위한 구분자로, 비워두면 트리가 비활성화되고 목록으로 키를 표시합니다.',
129 |     confirm_modify_unvisible_content: '보이지않는 문자가 내용에 포함되어 있습니다. "Hex View"를 통해 안전하게 편집할 수 있습니다. "Text View"로 편집하면 오류가 발생할 수 있습니다. 정말로 계속 진행하시겠습니까?',
130 |     keys_per_loading: '한 번에 로드할 개수',
131 |     keys_per_loading_tip: '설정한 수 만큼 키를 불러옵니다. 너무 크게 설정하면 성능에 영향을 미칠 수 있습니다.',
132 |     host: '호스트',
133 |     port: '포트',
134 |     username: '사용자명',
135 |     password: '비밀번호',
136 |     connection_name: '이름',
137 |     separator: '구분자',
138 |     timeout: '연결시도 시간제한',
139 |     private_key: '개인키',
140 |     public_key: '공개키',
141 |     authority: '인증기관',
142 |     redis_node_password: '레디스 노드 비밀번호',
143 |     master_group_name: '마스터 그룹명',
144 |     command_log: '로그',
145 |     sentinel_faq: '여러 센티널 중 하나를 선택할 수 있습니다. 센티널의 주소, 포트 및 비밀번호를 입력하십시오. 레디스 노드 비밀번호는 센티널이 모니터링하는 마스터 노드의 비밀번호입니다.',
146 |     hotkey: '단축키',
147 |     persist: '만료시간 삭제',
148 |     custom_formatter: '사용자정의 형식',
149 |     edit: '편집',
150 |     new: '생성',
151 |     custom: '사용자정의',
152 |     hide_window: '창 숨기기',
153 |     minimize_window: '최소화',
154 |     maximize_window: '최대화',
155 |     load_all_keys: '모두 불러오기',
156 |     show_load_all_keys: '모두 불러오기 버튼 활성화',
157 |     load_all_keys_tip: '모든 키를 한 번에 불러옵니다. 만약 키가 너무 많다면 클라이언트가 멈출 수 있습니다. 사용에 주의 하십시오.',
158 |     tree_node_overflow: '키나 폴더가 너무 많으면 {num}개만 표시합니다. 찾으려는 키가 보이지 않으면 퍼지 검색을 권장합니다. 또는 구분자를 설정하여 키들을 폴더로 분류할 수 있습니다.',
159 |     connection_readonly: '읽기전용 모드입니다. 추가, 편집, 삭제가 제한됩니다.',
160 |     memory_analysis: '메모리 분석',
161 |     begin: '시작',
162 |     pause: '일시정지',
163 |     restart: '재시작',
164 |     max_display: '최대 표시제한:  {num}',
165 |     max_scan: '최대 스캔제한:  {num}',
166 |     close_left: '좌측 탭 닫기',
167 |     close_right: '우측 탭 닫기',
168 |     close_other: '다른 탭 닫기',
169 |     slow_log: '저성능 쿼리',
170 |     load_current_folder: '현재 폴더만 로드',
171 |     custom_name: '맞춤 이름',
172 |     theme_select: '색상 테마',
173 |     theme_system: '시스템',
174 |     theme_light: '밝은 색',
175 |     theme_dark: '어두운 색',
176 |   },
177 | };
178 | 
179 | export default ko;
180 | 


--------------------------------------------------------------------------------
/src/i18n/langs/tw.js:
--------------------------------------------------------------------------------
  1 | const tw = {
  2 |   message: {
  3 |     new_connection: '新增連線',
  4 |     refresh_connection: '重新整理',
  5 |     edit_connection: '編輯連線',
  6 |     duplicate_connection: '複製連接',
  7 |     del_connection: '刪除連線',
  8 |     close_connection: '關閉連線',
  9 |     add_new_line: '新增行',
 10 |     dump_to_clipboard: '複製為命令',
 11 |     redis_version: 'Redis 版本',
 12 |     process_id: '處理程序 ID',
 13 |     used_memory: '已使用記憶體',
 14 |     used_memory_peak: '記憶體佔用峰值',
 15 |     used_memory_lua: 'Lua 佔用記憶體',
 16 |     connected_clients: '用戶端連線數',
 17 |     total_connections_received: '歷史連線數',
 18 |     total_commands_processed: '歷史命令數',
 19 |     key_statistics: '鍵值統計',
 20 |     all_redis_info: 'Redis 資訊總覽',
 21 |     server: '伺服器',
 22 |     memory: '記憶體',
 23 |     stats: '狀態',
 24 |     settings: '基本設定',
 25 |     ui_settings: '外觀',
 26 |     feature_settings: '功能',
 27 |     common_settings: '通用',
 28 |     confirm_to_delete_row_data: '確認刪除該行資料?',
 29 |     delete_success: '刪除成功',
 30 |     delete_failed: '刪除失敗',
 31 |     modify_success: '修改成功',
 32 |     modify_failed: '修改失敗',
 33 |     add_success: '新增成功',
 34 |     add_failed: '新增失敗',
 35 |     value_exists: '值已存在',
 36 |     value_not_exists: '該值不存在',
 37 |     refresh_success: '重新整理成功',
 38 |     click_enter_to_rename: '點擊或者按 Enter 鍵來重新命名',
 39 |     click_enter_to_ttl: '點擊或者按 Enter 鍵來修改過期時間',
 40 |     confirm_to_delete_key: '確認刪除 {key} ?',
 41 |     confirm_to_rename_key: '確認重命名 {old} -> {new} ?',
 42 |     edit_line: '修改行',
 43 |     auto_refresh: '自動重新整理',
 44 |     auto_refresh_tip: '自動重新整理開關,每 {interval} 秒重新整理一次',
 45 |     key_not_exists: '鍵不存在',
 46 |     collapse_all: '全部摺疊',
 47 |     expand_all: '全部展開',
 48 |     json_format_failed: 'JSON 格式化失敗',
 49 |     msgpack_format_failed: 'Msgpack 格式化失敗',
 50 |     php_unserialize_format_failed: 'PHP Unserialize 格式化失敗',
 51 |     clean_up: '清空',
 52 |     redis_console: 'Redis 控制台',
 53 |     confirm_to_delete_connection: '確認刪除連線?',
 54 |     connection_exists: '連線設定已存在',
 55 |     close_to_edit_connection: '編輯前必須關閉連線,確定要繼續嗎',
 56 |     close_to_connection: '確認關閉連線?',
 57 |     ttl_delete: '設定 TTL<=0 將刪除該鍵,是否確認?',
 58 |     max_page_reached: '已到達最大頁碼',
 59 |     add_new_key: '新增鍵',
 60 |     enter_new_key: '請先輸入新的鍵名',
 61 |     key_type: '類型',
 62 |     save: '儲存',
 63 |     enter_to_search: 'Enter 鍵進行搜尋',
 64 |     export_success: '匯出成功',
 65 |     select_import_file: '選擇設定檔',
 66 |     import_success: '匯入成功',
 67 |     put_file_here: '將檔案拖到此處,或點擊選擇',
 68 |     config_connections: '連線設定',
 69 |     import: '匯入',
 70 |     export: '匯出',
 71 |     open: '打開',
 72 |     close: '關閉',
 73 |     open_new_tab: '以新視窗打開',
 74 |     exact_search: '精確搜尋',
 75 |     enter_to_exec: '輸入 Redis 指令後,按 Enter 鍵執行,上下鍵切換指令歷史紀錄',
 76 |     pre_version: '目前版本',
 77 |     manual_update: '手動下載',
 78 |     retry_too_many_times: '嘗試重連次數過多,請檢查伺服器狀態',
 79 |     key_to_search: '輸入關鍵字搜尋',
 80 |     search_connection: '搜尋連接',
 81 |     begin_update: '更新',
 82 |     ignore_this_version: '忽略此版本',
 83 |     check_update: '檢查更新',
 84 |     update_checking: '檢查更新中, 請稍後...',
 85 |     update_available: '發現新版本',
 86 |     update_not_available: '目前為最新版本',
 87 |     update_error: '更新失敗',
 88 |     update_downloading: '下載中...',
 89 |     update_download_progress: '下載進度',
 90 |     update_downloaded: '更新下載完成,重啟用戶端生效.\
 91 |     [Tips]: 如果您使用的是Windows,請在關閉應用程序後等待桌面圖標刷新到正常狀態(大約10秒),然後重新打開',
 92 |     mac_not_support_auto_update: 'Mac 暫時不支援自動更新,請手動<a href="https://github.com/qishibo/AnotherRedisDesktopManager/releases">下載</a>後重新安裝,\
 93 |     或者執行<br><code>brew reinstall --cask another-redis-desktop-manager </code>\
 94 |     <br><hr><br>❤️如果對您有用,您可以通過<a href="https://apps.apple.com/app/id1516451072">AppStore</a>贊助,AppStore會自動為您更新。',
 95 |     font_family: '字體選擇',
 96 |     font_faq_title: '字體設定說明',
 97 |     font_faq: '1. 可以設定多個字體<br>2. 字體選擇是有分先後順序的,建議首先選擇英文字體,然後再選擇中文字體<br>\
 98 |     3. 某些異常情況無法載入系統字體列表時,可以手動輸入已安裝的字體名稱',
 99 |     private_key_faq: '目前支持RSA格式私鑰,即以<pre>-----BEGIN RSA PRIVATE KEY-----</pre>開頭,\
100 |     以<pre>-----BEGIN OPENSSH PRIVATE KEY-----</pre>開頭的,需要執行\
101 |     <pre>ssh-keygen -p -m pem -f ~/.ssh/id_rsa</pre>進行格式轉換後再使用,該操作不會影響以前的私鑰登入',
102 |     dark_mode: '深色模式',
103 |     load_more_keys: '載入更多',
104 |     key_name: '鍵名',
105 |     project_home: '專案主頁',
106 |     cluster_faq: '選擇叢集中任一節點設定填入即可,會自動識別其它節點',
107 |     redis_status: 'Redis訊息',
108 |     confirm_flush_db: '確認刪除db{db}中的所有鍵值嗎?',
109 |     flushdb: '刪除所有鍵',
110 |     flushdb_prompt: '輸入 "{txt}"',
111 |     info_disabled: 'Info命令執行異常(可能已被禁用),無法顯示Redis訊息',
112 |     page_zoom: '頁面縮放',
113 |     scan_disabled: 'Scan命令執行異常(可能已被禁用),無法顯示key列表',
114 |     key_type_not_support: '該類型暫不支持視覺化展示,請使用Console',
115 |     delete_folder: '掃描並刪除整個資料夾',
116 |     multiple_select: '多項選擇',
117 |     copy: '複製',
118 |     copy_success: '複製成功',
119 |     keys_to_be_deleted: '即將刪除的鍵',
120 |     delete_all: '全部删除',
121 |     clear_cache: '清除緩存',
122 |     mark_color: '標記顏色',
123 |     key_no_permission: '文件讀取權限已過期,請手動重新選擇密鑰文件',
124 |     toggle_check_all: '全選 | 取消全選',
125 |     select_lang: '選擇語言',
126 |     clear_cache_tip: '如果客戶端出現問題,此操作將刪除所有連接和配置以恢復客戶端',
127 |     detail: '詳情',
128 |     separator_tip: '樹視圖的分隔符,設置為空可禁用樹並將鍵顯示為列表',
129 |     confirm_modify_unvisible_content: '內容包含不可見的字符,您可以在Hex視圖中進行安全編輯。如果繼續在Text視圖中進行編輯可能會導致編碼錯誤,確定要繼續嗎?',
130 |     keys_per_loading: '加載數量',
131 |     keys_per_loading_tip: '每次加載的key數量, 設置太大的話可能會影響使用性能',
132 |     host: '地址',
133 |     port: '端口',
134 |     username: '用戶名',
135 |     password: '密碼',
136 |     connection_name: '連接名稱',
137 |     separator: '分隔符',
138 |     timeout: '超時',
139 |     private_key: '私鑰',
140 |     public_key: '公鑰',
141 |     authority: '授權',
142 |     redis_node_password: 'Redis節點密碼',
143 |     master_group_name: 'Master組名',
144 |     command_log: '日誌',
145 |     sentinel_faq: '您可以選擇多個哨兵之一。 地址、端口、密碼請填寫哨兵配置。 Redis節點密碼是sentinel監控的Master節點的密碼。',
146 |     hotkey: '熱鍵',
147 |     persist: '刪除過期時間',
148 |     custom_formatter: '自定義格式化',
149 |     edit: '編輯',
150 |     new: '新增',
151 |     custom: '自定義',
152 |     hide_window: '隱藏窗口',
153 |     minimize_window: '最小化窗口',
154 |     maximize_window: '最大化窗口',
155 |     load_all_keys: '載入所有',
156 |     show_load_all_keys: '啟用按鈕以加載所有鍵',
157 |     load_all_keys_tip: '一次載入所有密鑰。如果密鑰數量過多,客戶端可能會卡住,請正確使用',
158 |     tree_node_overflow: '鍵或資料夾太多,僅保留{num}個以顯示。 如果您的鍵不在此處,建議使用模糊搜索,或設置分隔符以將密鑰分散到文件夾中',
159 |     connection_readonly: '只讀模式,禁止添加、編輯和刪除',
160 |     memory_analysis: '內存分析',
161 |     begin: '開始',
162 |     pause: '暫停',
163 |     restart: '重新開始',
164 |     max_display: '最大顯示數量:{num}',
165 |     max_scan: '最大掃描數量:{num}',
166 |     close_left: '關閉左側標籤',
167 |     close_right: '關閉右側標籤',
168 |     close_other: '關閉其他標籤',
169 |     slow_log: '慢查詢',
170 |     load_current_folder: '僅載入目前資料夾',
171 |     custom_name: '自訂名稱',
172 |     theme_select: '主題模式',
173 |     theme_system: '跟隨系統',
174 |     theme_light: '亮色模式',
175 |     theme_dark: '暗色模式',
176 |   },
177 | };
178 | 
179 | export default tw;
180 | 


--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue';
 2 | import ElementUI from 'element-ui';
 3 | import 'font-awesome/css/font-awesome.css';
 4 | import App from './App';
 5 | import i18n from './i18n/i18n';
 6 | import bus from './bus';
 7 | import util from './util';
 8 | import storage from './storage';
 9 | import shortcut from './shortcut';
10 | 
11 | // vxe-table
12 | // import VxeUITable from 'vxe-table';
13 | import 'vxe-table/lib/style.css';
14 | // Vue.use(VxeUITable);
15 | 
16 | Vue.prototype.$bus = bus;
17 | Vue.prototype.$util = util;
18 | Vue.prototype.$storage = storage;
19 | Vue.prototype.$shortcut = shortcut;
20 | 
21 | Vue.use(ElementUI, { size: 'small' });
22 | Vue.config.productionTip = false;
23 | 
24 | /* eslint-disable no-new */
25 | const vue = new Vue({
26 |   el: '#app',
27 |   i18n,
28 |   components: { App },
29 |   template: '<App/>',
30 | });
31 | 
32 | // handle uncaught exception
33 | process.on('uncaughtException', (err, origin) => {
34 |   if (!err) {
35 |     return;
36 |   }
37 | 
38 |   vue.$message.error({
39 |     message: `Uncaught Exception: ${err}`,
40 |     duration: 5000,
41 |   });
42 | 
43 |   vue.$bus.$emit('closeConnection');
44 | });
45 | 
46 | export default vue;
47 | 


--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue';
 2 | import Router from 'vue-router';
 3 | import Tabs from '@/components/Tabs';
 4 | 
 5 | Vue.use(Router);
 6 | 
 7 | export default new Router({
 8 |   routes: [
 9 |     {
10 |       path: '/',
11 |       name: 'Tabs',
12 |       component: Tabs,
13 |     },
14 |   ],
15 | });
16 | 


--------------------------------------------------------------------------------
/src/shortcut.js:
--------------------------------------------------------------------------------
 1 | import keymaster from 'keymaster';
 2 | import { ipcRenderer } from 'electron';
 3 | 
 4 | // enable shortcut in input, textarea, select
 5 | keymaster.filter = e => true;
 6 | 
 7 | // prevent ctrl+r
 8 | keymaster('ctrl+r, ⌘+r', e => false);
 9 | 
10 | // minimize window
11 | keymaster('ctrl+h, ctrl+m, ⌘+m', (e) => {
12 |   ipcRenderer.send('minimizeWindow');
13 |   return false;
14 | });
15 | 
16 | // hide window on mac
17 | // (process.platform === 'darwin') && keymaster('⌘+h', e => {
18 | //   ipcRenderer.send('hideWindow');
19 | //   return false;
20 | // });
21 | 
22 | // toggle maximize
23 | keymaster('ctrl+enter, ⌘+enter', (e) => {
24 |   ipcRenderer.send('toggleMaximize');
25 |   return false;
26 | });
27 | 
28 | export default {
29 |   bind: (...args) => keymaster(...args),
30 |   ...keymaster,
31 | };
32 | 


--------------------------------------------------------------------------------
/src/storage.js:
--------------------------------------------------------------------------------
  1 | import utils from './util';
  2 | 
  3 | const { randomString } = utils;
  4 | 
  5 | export default {
  6 |   getSetting(key) {
  7 |     let settings = localStorage.getItem('settings');
  8 |     settings = settings ? JSON.parse(settings) : {};
  9 | 
 10 |     return key ? settings[key] : settings;
 11 |   },
 12 |   saveSettings(settings) {
 13 |     settings = JSON.stringify(settings);
 14 |     return localStorage.setItem('settings', settings);
 15 |   },
 16 |   getFontFamily() {
 17 |     let fontFamily = this.getSetting('fontFamily');
 18 | 
 19 |     // set to default font-family
 20 |     if (
 21 |       !fontFamily || !fontFamily.length
 22 |       || fontFamily.toString() === 'Default Initial'
 23 |     ) {
 24 |       fontFamily = ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica',
 25 |         'Arial', 'sans-serif', 'Microsoft YaHei', 'Apple Color Emoji', 'Segoe UI Emoji'];
 26 |     }
 27 | 
 28 |     return fontFamily.map(line => `"${line}"`).join(',');
 29 |   },
 30 |   getCustomFormatter(name = '') {
 31 |     let formatters = localStorage.getItem('customFormatters');
 32 |     formatters = formatters ? JSON.parse(formatters) : [];
 33 | 
 34 |     if (!name) {
 35 |       return formatters;
 36 |     }
 37 | 
 38 |     for (const line of formatters) {
 39 |       if (line.name === name) {
 40 |         return line;
 41 |       }
 42 |     }
 43 |   },
 44 |   saveCustomFormatters(formatters = []) {
 45 |     return localStorage.setItem('customFormatters', JSON.stringify(formatters));
 46 |   },
 47 |   addConnection(connection) {
 48 |     this.editConnectionByKey(connection, '');
 49 |   },
 50 |   getConnections(returnList = false) {
 51 |     let connections = localStorage.connections || '{}';
 52 | 
 53 |     connections = JSON.parse(connections);
 54 | 
 55 |     if (returnList) {
 56 |       connections = Object.keys(connections).map(key => connections[key]);
 57 |       this.sortConnections(connections);
 58 |     }
 59 | 
 60 |     return connections;
 61 |   },
 62 |   editConnectionByKey(connection, oldKey = '') {
 63 |     oldKey = connection.key || oldKey;
 64 | 
 65 |     const connections = this.getConnections();
 66 |     delete connections[oldKey];
 67 | 
 68 |     this.updateConnectionName(connection, connections);
 69 |     const newKey = this.getConnectionKey(connection, true);
 70 |     connection.key = newKey;
 71 | 
 72 |     // new added has no order, add it. do not add when edit mode
 73 |     if (!oldKey && isNaN(connection.order)) {
 74 |       // connection.order = Object.keys(connections).length;
 75 |       const maxOrder = Math.max(...Object.values(connections).map(item => (!isNaN(item.order) ? item.order : 0)));
 76 |       connection.order = (maxOrder > 0 ? maxOrder : 0) + 1;
 77 |     }
 78 | 
 79 |     connections[newKey] = connection;
 80 |     this.setConnections(connections);
 81 |   },
 82 |   editConnectionItem(connection, items = {}) {
 83 |     const key = this.getConnectionKey(connection);
 84 |     const connections = this.getConnections();
 85 | 
 86 |     if (!connections[key]) {
 87 |       return;
 88 |     }
 89 | 
 90 |     Object.assign(connection, items);
 91 |     Object.assign(connections[key], items);
 92 |     this.setConnections(connections);
 93 |   },
 94 |   updateConnectionName(connection, connections) {
 95 |     let name = this.getConnectionName(connection);
 96 | 
 97 |     for (const key in connections) {
 98 |       // if 'name' same with others, add random suffix
 99 |       if (this.getConnectionName(connections[key]) == name) {
100 |         name += ` (${randomString(3)})`;
101 |         break;
102 |       }
103 |     }
104 | 
105 |     connection.name = name;
106 |   },
107 |   getConnectionName(connection) {
108 |     return connection.name || `${connection.host}@${connection.port}`;
109 |   },
110 |   setConnections(connections) {
111 |     localStorage.connections = JSON.stringify(connections);
112 |   },
113 |   deleteConnection(connection) {
114 |     const connections = this.getConnections();
115 |     const key = this.getConnectionKey(connection);
116 | 
117 |     delete connections[key];
118 | 
119 |     this.hookAfterDelConnection(connection);
120 |     this.setConnections(connections);
121 |   },
122 |   getConnectionKey(connection, forceUnique = false) {
123 |     if (Object.keys(connection).length === 0) {
124 |       return '';
125 |     }
126 | 
127 |     if (connection.key) {
128 |       return connection.key;
129 |     }
130 | 
131 |     if (forceUnique) {
132 |       return `${new Date().getTime()}_${randomString(5)}`;
133 |     }
134 | 
135 |     return connection.host + connection.port + connection.name;
136 |   },
137 |   sortConnections(connections) {
138 |     connections.sort((a, b) => {
139 |       // drag ordered
140 |       if (!isNaN(a.order) && !isNaN(b.order)) {
141 |         return parseInt(a.order) <= parseInt(b.order) ? -1 : 1;
142 |       }
143 | 
144 |       // no ordered, by key
145 |       if (a.key && b.key) {
146 |         return a.key < b.key ? -1 : 1;
147 |       }
148 | 
149 |       return a.key ? 1 : (b.key ? -1 : 0);
150 |     });
151 |   },
152 |   reOrderAndStore(connections = []) {
153 |     const newConnections = {};
154 | 
155 |     for (const index in connections) {
156 |       const connection = connections[index];
157 |       connection.order = parseInt(index);
158 |       newConnections[this.getConnectionKey(connection, true)] = connection;
159 |     }
160 | 
161 |     this.setConnections(newConnections);
162 | 
163 |     return newConnections;
164 |   },
165 |   getStorageKeyMap(type) {
166 |     const typeMap = {
167 |       cli_tip: 'cliTips',
168 |       last_db: 'lastSelectedDb',
169 |       custom_db: 'customDbName',
170 |       search_tip: 'searchTips',
171 |     };
172 | 
173 |     return type ? typeMap[type] : typeMap;
174 |   },
175 |   initStorageKey(prefix, connectionName) {
176 |     return `${prefix}_${connectionName}`;
177 |   },
178 |   getStorageKeyByName(type = 'cli_tip', connectionName = '') {
179 |     return this.initStorageKey(this.getStorageKeyMap(type), connectionName);
180 |   },
181 |   hookAfterDelConnection(connection) {
182 |     const connectionName = this.getConnectionName(connection);
183 |     const types = Object.keys(this.getStorageKeyMap());
184 | 
185 |     const willRemovedKeys = [];
186 | 
187 |     for (const type of types) {
188 |       willRemovedKeys.push(this.getStorageKeyByName(type, connectionName));
189 |     }
190 | 
191 |     willRemovedKeys.forEach(k => localStorage.removeItem(k));
192 |   },
193 | };
194 | 


--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qishibo/AnotherRedisDesktopManager/ae576b362b37d20385447f27c3ec3698914ad168/static/.gitkeep


--------------------------------------------------------------------------------
/static/theme/dark/fonts/element-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qishibo/AnotherRedisDesktopManager/ae576b362b37d20385447f27c3ec3698914ad168/static/theme/dark/fonts/element-icons.ttf


--------------------------------------------------------------------------------
/static/theme/dark/fonts/element-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qishibo/AnotherRedisDesktopManager/ae576b362b37d20385447f27c3ec3698914ad168/static/theme/dark/fonts/element-icons.woff


--------------------------------------------------------------------------------
/static/theme/light/fonts/element-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qishibo/AnotherRedisDesktopManager/ae576b362b37d20385447f27c3ec3698914ad168/static/theme/light/fonts/element-icons.ttf


--------------------------------------------------------------------------------
/static/theme/light/fonts/element-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qishibo/AnotherRedisDesktopManager/ae576b362b37d20385447f27c3ec3698914ad168/static/theme/light/fonts/element-icons.woff


--------------------------------------------------------------------------------