diff --git a/.babelrc b/.babelrc index 43484cf8..0f12df17 100644 --- a/.babelrc +++ b/.babelrc @@ -16,7 +16,8 @@ "polished", "transform-object-rest-spread", "transform-strict-mode", - "transform-class-properties" + "transform-class-properties", + "jsx-control-statements" ], "env": { "production": { @@ -24,7 +25,8 @@ "plugins": [ "transform-remove-console", "transform-remove-debugger", - "dev-expression" + "dev-expression", + "jsx-control-statements" ] }, "node": { @@ -35,7 +37,7 @@ }, "development": { "plugins": [ - "react-hot-loader/babel" + // "react-hot-loader/babel" ] } } diff --git a/.eslintrc b/.eslintrc index 734aa19d..99b30ab2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,8 +1,12 @@ { "parser": "babel-eslint", + "plugins": [ + "jsx-control-statements" + ], "extends": [ "standard", - "standard-react" + "standard-react", + "plugin:jsx-control-statements/recommended" ], "env": { "node": true, diff --git a/app/package-lock.json b/app/package-lock.json index 0b099738..589ea1c1 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,6 +1,6 @@ { "name": "buttercup", - "version": "0.18.2", + "version": "0.19.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -74,6 +74,17 @@ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" }, + "conf": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-1.0.0.tgz", + "integrity": "sha1-NUcXDTObtNOL9eDobC5a+Xa0f/o=", + "requires": { + "dot-prop": "4.1.1", + "env-paths": "1.0.0", + "mkdirp": "0.5.1", + "pkg-up": "1.0.0" + } + }, "cookiejar": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.0.6.tgz", @@ -141,6 +152,14 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, + "dot-prop": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.1.1.tgz", + "integrity": "sha1-qEk/C3te7sglJbXHWH+n3nyoWcE=", + "requires": { + "is-obj": "1.0.1" + } + }, "dropbox": { "version": "2.5.4", "resolved": "https://registry.npmjs.org/dropbox/-/dropbox-2.5.4.tgz", @@ -158,6 +177,11 @@ "iconv-lite": "0.4.18" } }, + "env-paths": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", + "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=" + }, "es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -173,6 +197,15 @@ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, "form-data": { "version": "1.0.0-rc3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc3.tgz", @@ -235,6 +268,11 @@ "resolved": "https://registry.npmjs.org/is-dir/-/is-dir-1.0.0.tgz", "integrity": "sha1-QdN/SV/MrMBaR3jWboMCTCkro/8=" }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -302,6 +340,19 @@ "mime-db": "1.27.0" } }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, "ms": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", @@ -316,6 +367,14 @@ "is-stream": "1.1.0" } }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "2.0.1" + } + }, "pbkdf2": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.12.tgz", @@ -333,6 +392,27 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-up": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-1.0.0.tgz", + "integrity": "sha1-Pgj7RhUlxEIWJKM7n35tCvWwWiY=", + "requires": { + "find-up": "1.1.2" + } + }, "qs": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz", diff --git a/app/package.json b/app/package.json index 05a63592..fb065d70 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "buttercup", - "version": "0.18.2", + "version": "0.20.0", "productName": "Buttercup", "main": "./dist/app.js", "description": "Free and Open Source password vault", @@ -8,6 +8,7 @@ "homepage": "https://buttercup.pw", "author": "Buttercup ", "dependencies": { + "conf": "~1.0.0", "buttercup-importer": "~0.9.2", "dropbox": "^2.3.0", "webdav": "~0.10.0", diff --git a/config/webpack.config.base.js b/config/webpack.config.base.js index ce5ba3c9..4b3d2c7f 100644 --- a/config/webpack.config.base.js +++ b/config/webpack.config.base.js @@ -33,6 +33,6 @@ module.exports = { plugins: [ ], externals: [ - 'buttercup-importer', 'zxcvbn', 'dropbox', 'webdav' + 'buttercup-importer', 'zxcvbn', 'dropbox', 'webdav', 'conf' ] }; diff --git a/config/webpack.config.development.js b/config/webpack.config.development.js index 43b28de8..a6f9c5e9 100644 --- a/config/webpack.config.development.js +++ b/config/webpack.config.development.js @@ -9,8 +9,6 @@ module.exports = merge(baseConfig, { entry: { main: [ 'react-hot-loader/patch', - 'webpack-dev-server/client?http://localhost:3000/', - 'webpack/hot/only-dev-server', resolve(__dirname, '../src/renderer/index') ], fileManager: resolve(__dirname, '../src/renderer/file-manager') diff --git a/package-lock.json b/package-lock.json index b1aefd77..aeafb5b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "buttercup", - "version": "0.18.2", + "version": "0.19.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -10,17 +10,6 @@ "integrity": "sha1-nK+xca+CMpSQNTtIFvAzR6oVCjA=", "dev": true }, - "7zip-bin": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-2.1.0.tgz", - "integrity": "sha512-jgBTCcJ0gQedE9o8Jw+H/Gyq//EnQxmVpha7CdprIwzRSC81Uj37inHvPzv6jaZgZwkCcfho52rAaIFBrdbO7w==", - "dev": true, - "requires": { - "7zip-bin-linux": "1.1.0", - "7zip-bin-mac": "1.0.1", - "7zip-bin-win": "2.1.0" - } - }, "7zip-bin-linux": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/7zip-bin-linux/-/7zip-bin-linux-1.1.0.tgz", @@ -562,16 +551,6 @@ "integrity": "sha1-UidltQw1EEkOUtfc/ghe+bqWlY8=", "dev": true }, - "asar-integrity": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/asar-integrity/-/asar-integrity-0.1.1.tgz", - "integrity": "sha512-Nt9p2sWWNFkgqaioFCjsjTQTBAu0YFy2UyW0cWqNr1UBs9vV0j1kG0GI3r1lEJ6XxV4jiz1/AwCJnCDj5DLJUg==", - "dev": true, - "requires": { - "bluebird-lst": "1.0.2", - "fs-extra-p": "4.3.0" - } - }, "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", @@ -616,6 +595,12 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", "dev": true }, + "async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true + }, "async-foreach": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", @@ -2707,12 +2692,12 @@ } }, "buttercup-web": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/buttercup-web/-/buttercup-web-0.32.0.tgz", - "integrity": "sha1-5tlmB3fIvc1QiUAs2HR/T/TUA3I=", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/buttercup-web/-/buttercup-web-0.32.2.tgz", + "integrity": "sha1-E1nw8poPlRyo2o/lkBUU0VGk7pg=", "dev": true, "requires": { - "buttercup": "0.41.0" + "buttercup": "0.41.2" }, "dependencies": { "assert-plus": { @@ -2722,9 +2707,9 @@ "dev": true }, "buttercup": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/buttercup/-/buttercup-0.41.0.tgz", - "integrity": "sha1-KeNwUogtjyXA0XJ/7CI/RNI6nNA=", + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/buttercup/-/buttercup-0.41.2.tgz", + "integrity": "sha1-j8BfVwGC/g9Qohx1mxsFEcgrIYY=", "dev": true, "requires": { "clone": "1.0.2", @@ -2780,26 +2765,6 @@ "core-util-is": "1.0.2", "extsprintf": "1.3.0" } - }, - "webdav": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/webdav/-/webdav-0.10.0.tgz", - "integrity": "sha1-3n9pNRQB7KsjufUHwjXhpHyZ9nc=", - "dev": true, - "requires": { - "deepmerge": "1.3.2", - "node-fetch": "1.6.3", - "xml2js": "0.4.17" - } - }, - "webdav-fs": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/webdav-fs/-/webdav-fs-1.3.0.tgz", - "integrity": "sha1-2xROSLIqWzGDCL4fNExK7Xnx+0w=", - "dev": true, - "requires": { - "webdav": "0.10.0" - } } } }, @@ -3562,6 +3527,47 @@ } } }, + "conf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/conf/-/conf-1.1.2.tgz", + "integrity": "sha512-0ZvmcIhd7IGdB2Kx4Uz9/dRibqLb2c9q2uaMQJmq54m1qSZfULL0RzU+APThtAxiXM/iNbZ346EeX7BtukJZOg==", + "dev": true, + "requires": { + "dot-prop": "4.2.0", + "env-paths": "1.0.0", + "make-dir": "1.0.0", + "pkg-up": "2.0.0" + }, + "dependencies": { + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "1.0.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", + "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "dev": true, + "requires": { + "find-up": "2.1.0" + } + } + } + }, "configstore": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/configstore/-/configstore-2.1.0.tgz", @@ -3996,16 +4002,6 @@ "which": "1.2.14" } }, - "cross-spawn-async": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz", - "integrity": "sha1-hF/wwINKPe2dFg2sptOQkGuyiMw=", - "dev": true, - "requires": { - "lru-cache": "4.1.0", - "which": "1.2.14" - } - }, "cross-unzip": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/cross-unzip/-/cross-unzip-0.0.2.tgz", @@ -4574,6 +4570,18 @@ "is-obj": "1.0.1" } }, + "dotenv": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-4.0.0.tgz", + "integrity": "sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=", + "dev": true + }, + "dotenv-expand": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-4.0.1.tgz", + "integrity": "sha1-aP3cFWGBTgoQlkERBX/xOM7X16g=", + "dev": true + }, "dropbox": { "version": "2.5.4", "resolved": "https://registry.npmjs.org/dropbox/-/dropbox-2.5.4.tgz", @@ -4660,6 +4668,12 @@ } } }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, "eastasianwidth": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.1.1.tgz", @@ -4689,86 +4703,87 @@ "dev": true }, "electron": { - "version": "1.6.10", - "resolved": "https://registry.npmjs.org/electron/-/electron-1.6.10.tgz", - "integrity": "sha1-Twuc1ZbjVwC1cSj5iMwdLOZ+VnE=", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/electron/-/electron-1.7.5.tgz", + "integrity": "sha1-BloxAr+LhxAt9QxQmF/v5sVpBFs=", "dev": true, "requires": { - "@types/node": "7.0.29", + "@types/node": "7.0.42", "electron-download": "3.3.0", "extract-zip": "1.6.5" }, "dependencies": { "@types/node": { - "version": "7.0.29", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.29.tgz", - "integrity": "sha512-+8JrLZny/uR+d/jLK9eaV63buRM7X/gNzQk57q76NS4KNKLSKOmxJYFIlwuP2zDvA7wqZj05POPhSd9Z1hYQpQ==", + "version": "7.0.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.42.tgz", + "integrity": "sha512-cF/2SHIITu6Xen1DqBobqsx63Bdui37ZnID90G/vkuF1T7orBijcgyYcgkRpChCRwoRaf4LV/jXjrfVtFL/Y8Q==", "dev": true } } }, "electron-builder": { - "version": "19.4.2", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-19.4.2.tgz", - "integrity": "sha512-2QOYo9U/vCZ7ueNjdGLUeqnc8el1+S0Q8IWvYxVIWXIASk0V+9SIuV8GrtU0rxdOhKk/c/KKe2iuL0A6XvNMMg==", + "version": "19.24.1", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-19.24.1.tgz", + "integrity": "sha512-3ieCWar8nN08kug107NmRHwR7pmge5jbZMYfps0wcYIWmASVFgcJtLpdegFA4cbxt1Ak71h/+Zk3QJvGtfupRQ==", "dev": true, "requires": { - "7zip-bin": "2.1.0", - "ajv": "5.2.0", + "7zip-bin": "2.2.3", + "ajv": "5.2.2", "ajv-keywords": "2.1.0", - "asar-integrity": "0.1.1", - "bluebird-lst": "1.0.2", - "chalk": "1.1.3", + "asar-integrity": "0.1.2", + "bluebird-lst": "1.0.3", + "chalk": "2.1.0", "chromium-pickle-js": "0.2.0", "cuint": "0.2.2", - "debug": "2.6.8", - "electron-builder-http": "19.4.2", - "electron-builder-util": "19.4.2", + "debug": "3.0.0", + "dotenv": "4.0.0", + "dotenv-expand": "4.0.1", + "ejs": "2.5.7", + "electron-builder-http": "19.23.0", + "electron-builder-util": "19.24.0", "electron-download-tf": "4.3.1", - "electron-osx-sign": "0.4.6", - "electron-publish": "19.4.2", - "fs-extra-p": "4.3.0", - "hosted-git-info": "2.4.2", + "electron-osx-sign": "0.4.7", + "electron-publish": "19.24.0", + "fs-extra-p": "4.4.0", + "hosted-git-info": "2.5.0", "is-ci": "1.0.10", "isbinaryfile": "3.0.2", - "js-yaml": "3.8.4", - "json5": "0.5.1", + "js-yaml": "3.9.1", + "lazy-val": "1.0.2", "minimatch": "3.0.4", - "node-forge": "0.7.1", - "normalize-package-data": "2.3.8", + "normalize-package-data": "2.4.0", "parse-color": "1.0.0", "plist": "2.1.0", + "read-config-file": "1.0.5", "sanitize-filename": "1.6.1", - "semver": "5.3.0", + "semver": "5.4.1", + "temp-file": "2.0.2", "update-notifier": "2.2.0", "uuid-1345": "0.99.6", "yargs": "8.0.2" }, "dependencies": { + "7zip-bin": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-2.2.3.tgz", + "integrity": "sha512-S2f7InK2SwceVFly0tx/+1xakOWhSZQeY5hOXFl/sZ9orfRE4i4Z9edsWonT5lyYTowBN73RwBbLqZaVrtSEuw==", + "dev": true, + "requires": { + "7zip-bin-linux": "1.1.0", + "7zip-bin-mac": "1.0.1", + "7zip-bin-win": "2.1.0" + } + }, "ajv": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.0.tgz", - "integrity": "sha1-wXNQJMXaLvdcwZBxMHPUTwmL9IY=", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.2.tgz", + "integrity": "sha1-R8aNaehvXZUxA7AHSpQw3GPaXjk=", "dev": true, "requires": { "co": "4.6.0", - "fast-deep-equal": "0.1.0", + "fast-deep-equal": "1.0.0", "json-schema-traverse": "0.3.1", "json-stable-stringify": "1.0.1" - }, - "dependencies": { - "fast-deep-equal": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-0.1.0.tgz", - "integrity": "sha1-XG9FmaumszPuM0Li7ZeGcvEAH40=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - } } }, "ansi-align": { @@ -4777,33 +4792,56 @@ "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", "dev": true, "requires": { - "string-width": "2.0.0" + "string-width": "2.1.1" + } + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "asar-integrity": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/asar-integrity/-/asar-integrity-0.1.2.tgz", + "integrity": "sha512-I5ZfmdG03WnC141nVuyHvbNO55kSS+uxEbC11p54YFXE1fBFlC5XV0KHGjCX3y1+KSnr53JHJf22ev4qi4LHXQ==", + "dev": true, + "requires": { + "bluebird-lst": "1.0.3", + "fs-extra-p": "4.4.0" + } + }, + "bluebird-lst": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.3.tgz", + "integrity": "sha512-NKk/GQk5fXcLKt4USI1htGuMwXHhKLa2a32FCNBFAOcpL0k8U5yFpusr3+NKc6RjytL8umW5pSQmtJCWWhiLrQ==", + "dev": true, + "requires": { + "bluebird": "3.5.0" } }, "boxen": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.1.0.tgz", - "integrity": "sha1-sbad1SIwXoB6md7ud329blFnsQI=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.2.1.tgz", + "integrity": "sha1-DxHn/jRO25OXl3/BPt5/ZNlWSB0=", "dev": true, "requires": { "ansi-align": "2.0.0", "camelcase": "4.1.0", - "chalk": "1.1.3", + "chalk": "2.1.0", "cli-boxes": "1.0.0", - "string-width": "2.0.0", - "term-size": "0.1.1", + "string-width": "2.1.1", + "term-size": "1.2.0", "widest-line": "1.0.0" - }, - "dependencies": { - "term-size": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-0.1.1.tgz", - "integrity": "sha1-hzYLljlsq1dgljcUzaDQy+7K2co=", - "dev": true, - "requires": { - "execa": "0.4.0" - } - } } }, "camelcase": { @@ -4812,79 +4850,54 @@ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "dev": true }, + "chalk": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", + "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.2.1" + } + }, "configstore": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.0.tgz", - "integrity": "sha1-Rd+QcHPibfoc9LLVL1tgVF6qEdE=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.1.tgz", + "integrity": "sha512-5oNkD/L++l0O6xGXxb1EWS7SivtjfGQlRyxJsYgE0Z495/L81e2h4/d3r969hoPXuFItzNOKMtsXgYG4c7dYvw==", "dev": true, "requires": { - "dot-prop": "4.1.1", + "dot-prop": "4.2.0", "graceful-fs": "4.1.11", "make-dir": "1.0.0", "unique-string": "1.0.0", - "write-file-atomic": "2.1.0", + "write-file-atomic": "2.3.0", "xdg-basedir": "3.0.0" - }, - "dependencies": { - "make-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.0.0.tgz", - "integrity": "sha1-l6ARdR6R3YfPre9Ygy67BJNt6Xg=", - "dev": true, - "requires": { - "pify": "2.3.0" - } - }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "dev": true, - "requires": { - "crypto-random-string": "1.0.0" - } - } } }, - "dot-prop": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.1.1.tgz", - "integrity": "sha1-qEk/C3te7sglJbXHWH+n3nyoWcE=", + "debug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.0.0.tgz", + "integrity": "sha512-XQkHxxqbsCb+zFurCHbotmJZl5jXsxvkRt952pT6Hpo7LmjWAJF12d9/kqBg5owjbLADbBDli1olravjSiSg8g==", "dev": true, "requires": { - "is-obj": "1.0.1" + "ms": "2.0.0" } }, - "electron-builder-http": { - "version": "19.4.2", - "resolved": "https://registry.npmjs.org/electron-builder-http/-/electron-builder-http-19.4.2.tgz", - "integrity": "sha512-9t0W0ynQT3Aul+XAzwg16bfBzJ8GS6aQ9oPVMR4IiBu0Z7DTpWeorrtlrhr6XJzs0c0mkK8/YL18Pjn9696iNg==", + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", "dev": true, "requires": { - "debug": "2.6.8", - "fs-extra-p": "4.3.0" + "is-obj": "1.0.1" } }, - "electron-builder-util": { - "version": "19.4.2", - "resolved": "https://registry.npmjs.org/electron-builder-util/-/electron-builder-util-19.4.2.tgz", - "integrity": "sha512-khgF/4VdsdW4sY2nZLjGgeAqVgueW3+s5OFSFY7EuU0LqZ81ZZz7hR0MvcLf1xXxbPswkHFJWASMVseWXGwmDg==", - "dev": true, - "requires": { - "7zip-bin": "2.1.0", - "bluebird-lst": "1.0.2", - "chalk": "1.1.3", - "debug": "2.6.8", - "electron-builder-http": "19.4.2", - "fcopy-pre-bundled": "0.3.4", - "fs-extra-p": "4.3.0", - "ini": "1.3.4", - "is-ci": "1.0.10", - "node-emoji": "1.5.1", - "source-map-support": "0.4.15", - "stat-mode": "0.2.2", - "tunnel-agent": "0.6.0" - } + "ejs": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.7.tgz", + "integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo=", + "dev": true }, "electron-download-tf": { "version": "4.3.1", @@ -4899,51 +4912,36 @@ "nugget": "2.0.1", "path-exists": "3.0.0", "rc": "1.2.1", - "semver": "5.3.0", + "semver": "5.4.1", "sumchecker": "2.0.2" }, "dependencies": { - "env-paths": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", - "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=", - "dev": true + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "3.0.1", + "universalify": "0.1.0" + } } } }, - "electron-osx-sign": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.6.tgz", - "integrity": "sha1-I5ji18q1wdjD7quxzUkDdlKOw5o=", - "dev": true, - "requires": { - "bluebird": "3.5.0", - "compare-version": "0.1.2", - "debug": "2.6.8", - "isbinaryfile": "3.0.2", - "minimist": "1.2.0", - "plist": "2.1.0", - "tempfile": "1.1.1" - } - }, - "electron-publish": { - "version": "19.4.2", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-19.4.2.tgz", - "integrity": "sha512-WSJHY+F4IHq4HtWftz0uPhpFX7DIRzgjtrtd1VwMctckVg3H3SzxEgfGUAE8xolKHsErGXqBubHDHd+66szSZg==", - "dev": true, - "requires": { - "bluebird-lst": "1.0.2", - "chalk": "1.1.3", - "electron-builder-http": "19.4.2", - "electron-builder-util": "19.4.2", - "fs-extra-p": "4.3.0", - "mime": "1.3.6" - } - }, "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", "dev": true }, "find-up": { @@ -4956,16 +4954,26 @@ } }, "fs-extra": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", - "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.1.tgz", + "integrity": "sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=", "dev": true, "requires": { "graceful-fs": "4.1.11", - "jsonfile": "3.0.0", + "jsonfile": "3.0.1", "universalify": "0.1.0" } }, + "fs-extra-p": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.4.0.tgz", + "integrity": "sha512-SDAF7Ma08/ERKmbNHBvoaxxox33/xiomZGhJlxoSaGYGn7jHCwLTFRnJ82wxrylZa+h0TtkBrrtXzRO79p3AHQ==", + "dev": true, + "requires": { + "bluebird-lst": "1.0.3", + "fs-extra": "4.0.1" + } + }, "got": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", @@ -4983,22 +4991,20 @@ "timed-out": "4.0.1", "unzip-response": "2.0.1", "url-parse-lax": "1.0.0" - }, - "dependencies": { - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - } } }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", + "dev": true + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -5006,19 +5012,19 @@ "dev": true }, "js-yaml": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.4.tgz", - "integrity": "sha1-UgtFZPhlc7qWZir4Woyvp7S1pvY=", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.9.1.tgz", + "integrity": "sha512-CbcG379L1e+mWBnLvHWWeLs8GyV/EMw862uLI3c+GxVyDHWZcjZinwuBd3iW2pgxgIlksW/1vNJa4to+RvDOww==", "dev": true, "requires": { "argparse": "1.0.9", - "esprima": "3.1.3" + "esprima": "4.0.0" } }, "jsonfile": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.0.tgz", - "integrity": "sha1-kufHRE5f/V+jLmqa6LhQNN+DR9A=", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", "dev": true, "requires": { "graceful-fs": "4.1.11" @@ -5045,12 +5051,24 @@ "strip-bom": "3.0.0" } }, - "mime": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.6.tgz", - "integrity": "sha1-WR2E02U6awtKO5343lqoEI5y5eA=", + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.4.1", + "validate-npm-package-license": "3.0.1" + } + }, "package-json": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", @@ -5060,7 +5078,7 @@ "got": "6.7.1", "registry-auth-token": "3.3.1", "registry-url": "3.1.0", - "semver": "5.3.0" + "semver": "5.4.1" } }, "path-exists": { @@ -5085,7 +5103,7 @@ "dev": true, "requires": { "load-json-file": "2.0.0", - "normalize-package-data": "2.3.8", + "normalize-package-data": "2.4.0", "path-type": "2.0.0" } }, @@ -5099,14 +5117,29 @@ "read-pkg": "2.0.0" } }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "dev": true + }, "string-width": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.0.0.tgz", - "integrity": "sha1-Y1xUNsxypuDDh87KJ41OLuxSaH4=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "dev": true, "requires": { "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "3.0.1" + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" } }, "strip-bom": { @@ -5122,6 +5155,26 @@ "dev": true, "requires": { "debug": "2.6.8" + }, + "dependencies": { + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "supports-color": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz", + "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==", + "dev": true, + "requires": { + "has-flag": "2.0.0" } }, "timed-out": { @@ -5142,9 +5195,9 @@ "integrity": "sha1-G1g3z5DAc22IYncytmHBOPht5y8=", "dev": true, "requires": { - "boxen": "1.1.0", + "boxen": "1.2.1", "chalk": "1.1.3", - "configstore": "3.1.0", + "configstore": "3.1.1", "import-lazy": "2.1.0", "is-npm": "1.0.0", "latest-version": "3.1.0", @@ -5152,23 +5205,57 @@ "xdg-basedir": "3.0.0" }, "dependencies": { - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", "dev": true } } }, "write-file-atomic": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.1.0.tgz", - "integrity": "sha512-0TZ20a+xcIl4u0+Mj5xDH2yOWdmQiXlKf9Hm+TgDXjTMsEYb+gDrmb8e8UNAzMCitX8NBqG4Z/FUQIyzv/R1JQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", "dev": true, "requires": { "graceful-fs": "4.1.11", "imurmurhash": "0.1.4", - "slide": "1.1.6" + "signal-exit": "3.0.2" } }, "xdg-basedir": { @@ -5192,7 +5279,7 @@ "require-directory": "2.1.1", "require-main-filename": "1.0.1", "set-blocking": "2.0.0", - "string-width": "2.0.0", + "string-width": "2.1.1", "which-module": "2.0.0", "y18n": "3.2.1", "yargs-parser": "7.0.0" @@ -5200,6 +5287,223 @@ } } }, + "electron-builder-http": { + "version": "19.23.0", + "resolved": "https://registry.npmjs.org/electron-builder-http/-/electron-builder-http-19.23.0.tgz", + "integrity": "sha512-cZ6+KR0ciSzlh10Jkwy2jH+sUfxX3dItu4bLciGIU9KW0oDTkFh1tzJMPUZzszyunZy41Zga+5A7k3F5TiQSgQ==", + "dev": true, + "requires": { + "bluebird-lst": "1.0.3", + "debug": "3.0.0", + "fs-extra-p": "4.4.0" + }, + "dependencies": { + "bluebird-lst": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.3.tgz", + "integrity": "sha512-NKk/GQk5fXcLKt4USI1htGuMwXHhKLa2a32FCNBFAOcpL0k8U5yFpusr3+NKc6RjytL8umW5pSQmtJCWWhiLrQ==", + "dev": true, + "requires": { + "bluebird": "3.5.0" + } + }, + "debug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.0.0.tgz", + "integrity": "sha512-XQkHxxqbsCb+zFurCHbotmJZl5jXsxvkRt952pT6Hpo7LmjWAJF12d9/kqBg5owjbLADbBDli1olravjSiSg8g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "fs-extra": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.1.tgz", + "integrity": "sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "3.0.1", + "universalify": "0.1.0" + } + }, + "fs-extra-p": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.4.0.tgz", + "integrity": "sha512-SDAF7Ma08/ERKmbNHBvoaxxox33/xiomZGhJlxoSaGYGn7jHCwLTFRnJ82wxrylZa+h0TtkBrrtXzRO79p3AHQ==", + "dev": true, + "requires": { + "bluebird-lst": "1.0.3", + "fs-extra": "4.0.1" + } + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "electron-builder-util": { + "version": "19.24.0", + "resolved": "https://registry.npmjs.org/electron-builder-util/-/electron-builder-util-19.24.0.tgz", + "integrity": "sha512-DbdsDz55383/rp5UJmpKYTIhmw0F84EZbzwiM4tlorTS/9GYFdLBs7P12ORtxy1d8ajpDxtMD/0f/Hsn25w1sg==", + "dev": true, + "requires": { + "7zip-bin": "2.2.3", + "bluebird-lst": "1.0.3", + "chalk": "2.1.0", + "debug": "3.0.0", + "electron-builder-http": "19.23.0", + "fcopy-pre-bundled": "0.3.4", + "fs-extra-p": "4.4.0", + "ini": "1.3.4", + "is-ci": "1.0.10", + "lazy-val": "1.0.2", + "node-emoji": "1.8.1", + "semver": "5.4.1", + "source-map-support": "0.4.16", + "stat-mode": "0.2.2", + "temp-file": "2.0.2", + "tunnel-agent": "0.6.0" + }, + "dependencies": { + "7zip-bin": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-2.2.3.tgz", + "integrity": "sha512-S2f7InK2SwceVFly0tx/+1xakOWhSZQeY5hOXFl/sZ9orfRE4i4Z9edsWonT5lyYTowBN73RwBbLqZaVrtSEuw==", + "dev": true, + "requires": { + "7zip-bin-linux": "1.1.0", + "7zip-bin-mac": "1.0.1", + "7zip-bin-win": "2.1.0" + } + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "bluebird-lst": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.3.tgz", + "integrity": "sha512-NKk/GQk5fXcLKt4USI1htGuMwXHhKLa2a32FCNBFAOcpL0k8U5yFpusr3+NKc6RjytL8umW5pSQmtJCWWhiLrQ==", + "dev": true, + "requires": { + "bluebird": "3.5.0" + } + }, + "chalk": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", + "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.2.1" + } + }, + "debug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.0.0.tgz", + "integrity": "sha512-XQkHxxqbsCb+zFurCHbotmJZl5jXsxvkRt952pT6Hpo7LmjWAJF12d9/kqBg5owjbLADbBDli1olravjSiSg8g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "fs-extra": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.1.tgz", + "integrity": "sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "3.0.1", + "universalify": "0.1.0" + } + }, + "fs-extra-p": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.4.0.tgz", + "integrity": "sha512-SDAF7Ma08/ERKmbNHBvoaxxox33/xiomZGhJlxoSaGYGn7jHCwLTFRnJ82wxrylZa+h0TtkBrrtXzRO79p3AHQ==", + "dev": true, + "requires": { + "bluebird-lst": "1.0.3", + "fs-extra": "4.0.1" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node-emoji": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.8.1.tgz", + "integrity": "sha512-+ktMAh1Jwas+TnGodfCfjUbJKoANqPaJFN0z0iqh41eqD8dvguNzcitVSBSVK1pidz0AqGbLKcoVuVLRVZ/aVg==", + "dev": true, + "requires": { + "lodash.toarray": "4.4.0" + } + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "dev": true + }, + "source-map-support": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.16.tgz", + "integrity": "sha512-A6vlydY7H/ljr4L2UOhDSajQdZQ6dMD7cLH0pzwcmwLyc9u8PNI4WGtnfDDzX7uzGL6c/T+ORL97Zlh+S4iOrg==", + "dev": true, + "requires": { + "source-map": "0.5.6" + } + }, + "supports-color": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz", + "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, "electron-chromedriver": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/electron-chromedriver/-/electron-chromedriver-1.6.0.tgz", @@ -5221,14 +5525,13 @@ } }, "electron-devtools-installer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/electron-devtools-installer/-/electron-devtools-installer-2.0.1.tgz", - "integrity": "sha1-1S3LOkMKlbdx3AHY3rE/y9qo0Tw=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/electron-devtools-installer/-/electron-devtools-installer-2.2.0.tgz", + "integrity": "sha1-mBPmgRr81p3co8rlQW23Lqfs+2o=", "dev": true, "requires": { "7zip": "0.0.6", "cross-unzip": "0.0.2", - "request": "2.81.0", "rimraf": "2.6.1", "semver": "5.3.0" } @@ -5268,61 +5571,181 @@ "dev": true }, "electron-json-storage": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/electron-json-storage/-/electron-json-storage-3.0.5.tgz", - "integrity": "sha1-UXAtLCXcXHUsjUylBBLugKV2g6Q=", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/electron-json-storage/-/electron-json-storage-3.0.7.tgz", + "integrity": "sha1-ohN3VtBngv5WWUi40HJC0/ViTCc=", "dev": true, "requires": { - "async": "2.4.1", + "async": "2.5.0", "lodash": "4.17.4", "mkdirp": "0.5.1", - "rimraf": "2.6.1" + "rimraf": "2.6.1", + "rwlock": "5.0.0" }, "dependencies": { "async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.4.1.tgz", - "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c=", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", + "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, + "electron-localshortcut": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/electron-localshortcut/-/electron-localshortcut-0.6.1.tgz", + "integrity": "sha1-xOJow4puQvQN5WGPyQbR7WCPEao=", + "dev": true + }, + "electron-log": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.6.tgz", + "integrity": "sha1-zPo+CbOfMhRoyQpkJwE4CjQHnxo=", + "dev": true + }, + "electron-osx-sign": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.7.tgz", + "integrity": "sha1-HXVkeoJ0jqzUi+pwYW7IP/rePuU=", + "dev": true, + "requires": { + "bluebird": "3.5.0", + "compare-version": "0.1.2", + "debug": "2.6.8", + "isbinaryfile": "3.0.2", + "minimist": "1.2.0", + "plist": "2.1.0" + } + }, + "electron-publish": { + "version": "19.24.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-19.24.0.tgz", + "integrity": "sha512-xxwKHen9OGVqC0QmWdmt2WKlmIdvdbLxvFRvpB0jICSjRDR06nzNJYNwqICN92vtQ1c5M4gE1SW09X+CV3rnjA==", + "dev": true, + "requires": { + "bluebird-lst": "1.0.3", + "chalk": "2.1.0", + "electron-builder-http": "19.23.0", + "electron-builder-util": "19.24.0", + "fs-extra-p": "4.4.0", + "mime": "1.3.6" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "bluebird-lst": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.3.tgz", + "integrity": "sha512-NKk/GQk5fXcLKt4USI1htGuMwXHhKLa2a32FCNBFAOcpL0k8U5yFpusr3+NKc6RjytL8umW5pSQmtJCWWhiLrQ==", + "dev": true, + "requires": { + "bluebird": "3.5.0" + } + }, + "chalk": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", + "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.2.1" + } + }, + "fs-extra": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.1.tgz", + "integrity": "sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "3.0.1", + "universalify": "0.1.0" + } + }, + "fs-extra-p": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.4.0.tgz", + "integrity": "sha512-SDAF7Ma08/ERKmbNHBvoaxxox33/xiomZGhJlxoSaGYGn7jHCwLTFRnJ82wxrylZa+h0TtkBrrtXzRO79p3AHQ==", + "dev": true, + "requires": { + "bluebird-lst": "1.0.3", + "fs-extra": "4.0.1" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", "dev": true, "requires": { - "lodash": "4.17.4" + "graceful-fs": "4.1.11" } }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "mime": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.6.tgz", + "integrity": "sha1-WR2E02U6awtKO5343lqoEI5y5eA=", "dev": true }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "supports-color": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz", + "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==", "dev": true, "requires": { - "minimist": "0.0.8" + "has-flag": "2.0.0" } } } }, - "electron-localshortcut": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/electron-localshortcut/-/electron-localshortcut-0.6.1.tgz", - "integrity": "sha1-xOJow4puQvQN5WGPyQbR7WCPEao=", - "dev": true - }, - "electron-log": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.6.tgz", - "integrity": "sha1-zPo+CbOfMhRoyQpkJwE4CjQHnxo=", - "dev": true - }, "electron-rpc": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/electron-rpc/-/electron-rpc-2.0.1.tgz", "integrity": "sha1-BJXx3PMllzIPhmEZDP8jBKB73cQ=", "dev": true }, + "electron-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-1.2.0.tgz", + "integrity": "sha1-lvcI8BiMdwiQfp+bfLp5C+1rmJ8=", + "dev": true, + "requires": { + "conf": "1.1.2" + } + }, "electron-to-chromium": { "version": "1.3.14", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.14.tgz", @@ -5330,12 +5753,13 @@ "dev": true }, "electron-unhandled": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/electron-unhandled/-/electron-unhandled-0.1.1.tgz", - "integrity": "sha1-/ybseBzMsTCyldQ79AbB68Bf4dU=", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/electron-unhandled/-/electron-unhandled-0.2.0.tgz", + "integrity": "sha1-5mvaW5PIQ5Knt9e34mfZDC0MGuM=", "dev": true, "requires": { - "clean-stack": "1.3.0" + "clean-stack": "1.3.0", + "ensure-error": "0.1.0" } }, "electron-updater": { @@ -5465,6 +5889,18 @@ "tapable": "0.2.6" } }, + "ensure-error": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ensure-error/-/ensure-error-0.1.0.tgz", + "integrity": "sha1-zz/TNlv3/YMkRzHL1PMABtmd8lk=", + "dev": true + }, + "env-paths": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", + "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=", + "dev": true + }, "errno": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", @@ -5899,6 +6335,12 @@ } } }, + "eslint-plugin-jsx-control-statements": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-control-statements/-/eslint-plugin-jsx-control-statements-2.2.0.tgz", + "integrity": "sha512-c1a9pwrx93KBX2GJmkpMaGZW+yR2O4wRCpndnnXIb+mesH3DOrmDejIA+81kVvaMijheSDUAs74gdTZxJJh9tA==", + "dev": true + }, "eslint-plugin-node": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-4.2.2.tgz", @@ -6105,20 +6547,6 @@ "create-hash": "1.1.3" } }, - "execa": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.4.0.tgz", - "integrity": "sha1-TrZGejaglfq7KXD/nV4/t7zm68M=", - "dev": true, - "requires": { - "cross-spawn-async": "2.2.5", - "is-stream": "1.1.0", - "npm-run-path": "1.0.0", - "object-assign": "4.1.1", - "path-key": "1.0.0", - "strip-eof": "1.0.0" - } - }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -6291,6 +6719,12 @@ "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", "dev": true }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", + "dev": true + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -7770,6 +8204,12 @@ "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", "dev": true }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, "getobject": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", @@ -8694,6 +9134,12 @@ "dev": true, "optional": true }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, "imul": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/imul/-/imul-1.0.1.tgz", @@ -9342,6 +9788,12 @@ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", "dev": true }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, "json-stable-stringify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", @@ -9443,6 +9895,16 @@ "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=", "dev": true }, + "jsx-control-statements": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/jsx-control-statements/-/jsx-control-statements-3.2.6.tgz", + "integrity": "sha512-FW+Yu0eYs/rXfVj9OsCATr0LE7rwFvwvIrlnE+esfCr4Smi+fIkHRH3sEPgoMeCbVcpuIjnAzzdvF1by0gbmKg==", + "dev": true, + "requires": { + "babel-core": "6.25.0", + "babel-plugin-syntax-jsx": "6.18.0" + } + }, "kdbxweb": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/kdbxweb/-/kdbxweb-1.0.1.tgz", @@ -12680,6 +13142,12 @@ "integrity": "sha1-va6+rTD42CQDnODOFJ1Nqge6H6w=", "dev": true }, + "lazy-val": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.2.tgz", + "integrity": "sha512-2BaSu6qVnicKdWQPysrffZVFAKcPcZQ/q2YyeSjAxWaJlvCvKSrkcvsSHlleeIfA//fW2goTcYDTy2cBLN7+PQ==", + "dev": true + }, "lazystream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", @@ -13047,6 +13515,12 @@ "lodash.debounce": "3.1.1" } }, + "lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=", + "dev": true + }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -13125,6 +13599,15 @@ "nan": "2.6.2" } }, + "make-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.0.0.tgz", + "integrity": "sha1-l6ARdR6R3YfPre9Ygy67BJNt6Xg=", + "dev": true, + "requires": { + "pify": "2.3.0" + } + }, "makedeb": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/makedeb/-/makedeb-0.0.4.tgz", @@ -13173,6 +13656,12 @@ "escape-string-regexp": "1.0.5" } }, + "material-colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.5.tgz", + "integrity": "sha1-UpJZPmdUyxvMK5gDDk4Najr8nqE=", + "dev": true + }, "math-expression-evaluator": { "version": "1.2.17", "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", @@ -13518,15 +14007,6 @@ "inherits": "2.0.3" } }, - "node-emoji": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.5.1.tgz", - "integrity": "sha1-/ZGOQSdpv4xEgFEjgjOECyr/FqE=", - "dev": true, - "requires": { - "string.prototype.codepointat": "0.2.0" - } - }, "node-fetch": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.1.tgz", @@ -13537,12 +14017,6 @@ "is-stream": "1.1.0" } }, - "node-forge": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", - "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=", - "dev": true - }, "node-gyp": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz", @@ -13835,21 +14309,18 @@ "sort-keys": "1.1.2" } }, + "normalizr": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/normalizr/-/normalizr-3.2.3.tgz", + "integrity": "sha1-iHVcZN5BiwQPpq0TKbLeXDJQrEk=", + "dev": true + }, "npm-install-package": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/npm-install-package/-/npm-install-package-2.1.0.tgz", "integrity": "sha1-1+/jz816sAYUuJbqUxGdyaslkSU=", "dev": true }, - "npm-run-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-1.0.0.tgz", - "integrity": "sha1-9cMr9ZX+ga6Sfa7FLoL4sACsPI8=", - "dev": true, - "requires": { - "path-key": "1.0.0" - } - }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -15729,12 +16200,6 @@ "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", "dev": true }, - "path-key": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-1.0.0.tgz", - "integrity": "sha1-XVPVeAGWRsDWiADbThRua9wqx68=", - "dev": true - }, "path-parse": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", @@ -16791,6 +17256,18 @@ "object-assign": "4.1.1" } }, + "react-color": { + "version": "2.13.5", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.13.5.tgz", + "integrity": "sha512-YR6fH/lLa3/eR1c+OhfXEFdGq2yX812UqOrRY4iiEF/9znwekg4mbn8FoUVgxX2vob6PPRcv0wO85SbU3qtsGg==", + "dev": true, + "requires": { + "lodash": "4.17.4", + "material-colors": "1.2.5", + "reactcss": "1.2.2", + "tinycolor2": "1.4.1" + } + }, "react-custom-scrollbars": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/react-custom-scrollbars/-/react-custom-scrollbars-4.1.2.tgz", @@ -16896,6 +17373,15 @@ "prop-types": "15.5.10" } }, + "react-portal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-portal/-/react-portal-3.1.0.tgz", + "integrity": "sha1-hlxE+3Kh2hBsZJIGk2VZzoke6Jk=", + "dev": true, + "requires": { + "prop-types": "15.5.10" + } + }, "react-proxy": { "version": "3.0.0-alpha.1", "resolved": "https://registry.npmjs.org/react-proxy/-/react-proxy-3.0.0-alpha.1.tgz", @@ -16979,6 +17465,15 @@ "prop-types": "15.5.10" } }, + "reactcss": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.2.tgz", + "integrity": "sha1-QbDvQ+AdVIgDV8NLEawVMSCTUO8=", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, "read-all-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", @@ -17027,6 +17522,76 @@ } } }, + "read-config-file": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-1.0.5.tgz", + "integrity": "sha512-hI5Z0kO8tHI58DhK2DeBjf7wfKYaGlpxMZWU1KAwyYefm6IgHGfUeFYNlG4x4dDxBXLUiWGsmoWn9qUiQcUkmQ==", + "dev": true, + "requires": { + "bluebird-lst": "1.0.3", + "fs-extra-p": "4.4.0", + "js-yaml": "3.9.1", + "json5": "0.5.1", + "lazy-val": "1.0.2" + }, + "dependencies": { + "bluebird-lst": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.3.tgz", + "integrity": "sha512-NKk/GQk5fXcLKt4USI1htGuMwXHhKLa2a32FCNBFAOcpL0k8U5yFpusr3+NKc6RjytL8umW5pSQmtJCWWhiLrQ==", + "dev": true, + "requires": { + "bluebird": "3.5.0" + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "fs-extra": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.1.tgz", + "integrity": "sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "3.0.1", + "universalify": "0.1.0" + } + }, + "fs-extra-p": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.4.0.tgz", + "integrity": "sha512-SDAF7Ma08/ERKmbNHBvoaxxox33/xiomZGhJlxoSaGYGn7jHCwLTFRnJ82wxrylZa+h0TtkBrrtXzRO79p3AHQ==", + "dev": true, + "requires": { + "bluebird-lst": "1.0.3", + "fs-extra": "4.0.1" + } + }, + "js-yaml": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.9.1.tgz", + "integrity": "sha512-CbcG379L1e+mWBnLvHWWeLs8GyV/EMw862uLI3c+GxVyDHWZcjZinwuBd3iW2pgxgIlksW/1vNJa4to+RvDOww==", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + } + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + } + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -17569,6 +18134,12 @@ "once": "1.4.0" } }, + "rwlock": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rwlock/-/rwlock-5.0.0.tgz", + "integrity": "sha1-iI1qd6M1HMGiCSBO8u4XIgk4Ns8=", + "dev": true + }, "rx": { "version": "2.3.24", "resolved": "https://registry.npmjs.org/rx/-/rx-2.3.24.tgz", @@ -17833,6 +18404,21 @@ "lodash.keys": "3.1.2" } }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, "shelljs": { "version": "0.7.8", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", @@ -18363,12 +18949,6 @@ "strip-ansi": "3.0.1" } }, - "string.prototype.codepointat": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz", - "integrity": "sha1-aybpvTr8qnvjtCabUm3huCAArHg=", - "dev": true - }, "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", @@ -18940,14 +19520,109 @@ } } }, - "tempfile": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-1.1.1.tgz", - "integrity": "sha1-W8xOrsxKsscH2LwR2ZzMmiyyh/I=", + "temp-file": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-2.0.2.tgz", + "integrity": "sha512-PFNC4wAem2UF2wwyoVM0ozvkk0g2A1c+uAb2zRXr2zHsVoWF796cV2LIFKuBhWWtaHGfZp2dtA1AfWoDXpkrOQ==", "dev": true, "requires": { - "os-tmpdir": "1.0.2", - "uuid": "2.0.3" + "async-exit-hook": "2.0.1", + "bluebird-lst": "1.0.3", + "fs-extra-p": "4.4.0", + "lazy-val": "1.0.2" + }, + "dependencies": { + "bluebird-lst": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.3.tgz", + "integrity": "sha512-NKk/GQk5fXcLKt4USI1htGuMwXHhKLa2a32FCNBFAOcpL0k8U5yFpusr3+NKc6RjytL8umW5pSQmtJCWWhiLrQ==", + "dev": true, + "requires": { + "bluebird": "3.5.0" + } + }, + "fs-extra": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.1.tgz", + "integrity": "sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "3.0.1", + "universalify": "0.1.0" + } + }, + "fs-extra-p": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.4.0.tgz", + "integrity": "sha512-SDAF7Ma08/ERKmbNHBvoaxxox33/xiomZGhJlxoSaGYGn7jHCwLTFRnJ82wxrylZa+h0TtkBrrtXzRO79p3AHQ==", + "dev": true, + "requires": { + "bluebird-lst": "1.0.3", + "fs-extra": "4.0.1" + } + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + } + } + }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "0.7.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.0", + "shebang-command": "1.2.0", + "which": "1.2.14" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + } } }, "text-table": { @@ -19077,6 +19752,12 @@ } } }, + "tinycolor2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", + "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=", + "dev": true + }, "tmatch": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-2.0.1.tgz", @@ -19411,6 +20092,15 @@ "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", "dev": true }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "1.0.0" + } + }, "unique-temp-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz", diff --git a/package.json b/package.json index e3f053fb..5dca2ae7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "buttercup", "productName": "Buttercup", - "version": "0.18.2", + "version": "0.20.0", "description": "Free and Open Source password vault", "main": "app/dist/app.js", "scripts": { @@ -60,7 +60,7 @@ "AppImage", "deb" ], - "packageCategory": "utils", + "category": "Utility", "synopsis": "Free and Open Source Password Vault" }, "fileAssociations": { @@ -126,7 +126,7 @@ "babel-register": "^6.18.0", "babili-webpack-plugin": "^0.1.1", "buttercup-importer": "~0.9.2", - "buttercup-web": "~0.32.0", + "buttercup-web": "~0.32.1", "classnames": "^2.2.5", "concurrently": "^2.1.0", "cross-env": "^1.0.8", @@ -135,21 +135,23 @@ "del": "^2.2.1", "devtron": "^1.4.0", "dropbox-fs": "^0.0.5", - "electron": "^1.6.10", - "electron-builder": "^19.4.2", + "electron": "^1.6.11", + "electron-builder": "^19.11.1", "electron-debug": "^1.1.0", - "electron-devtools-installer": "2.0.1", + "electron-devtools-installer": "^2.2.0", "electron-installer-dmg": "^0.1.2", - "electron-json-storage": "^3.0.2", + "electron-json-storage": "^3.0.6", "electron-log": "^2.2.6", "electron-rpc": "^2.0.1", - "electron-unhandled": "^0.1.1", + "electron-store": "^1.2.0", + "electron-unhandled": "^0.2.0", "electron-updater": "^2.3.0", "eslint": "^3.19.0", "eslint-config-standard": "^10.2.1", "eslint-config-standard-react": "^4.3.0", "eslint-import-resolver-webpack": "^0.8.1", "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jsx-control-statements": "^2.2.0", "eslint-plugin-node": "^4.2.2", "eslint-plugin-promise": "^3.5.0", "eslint-plugin-react": "^6.10.3", @@ -169,6 +171,7 @@ "is-error": "^2.2.1", "jsdom": "^9.8.3", "json-loader": "^0.5.4", + "jsx-control-statements": "^3.2.5", "load-grunt-tasks": "^3.3.0", "lodash": "^4.16.6", "makedeb": "0.0.4", @@ -177,20 +180,23 @@ "ms": "^0.7.2", "node-noop": "^1.0.0", "node-sass": "^4.5.3", + "normalizr": "^3.2.3", "pify": "^2.3.0", "prop-types": "^15.5.10", "raw-loader": "^0.5.1", "rc-tree": "^1.3.9", "react": "^15.5.4", "react-addons-test-utils": "^15.4.0", + "react-color": "^2.13.0", "react-custom-scrollbars": "^4.1.1", "react-dimensions": "^1.3.0", "react-dom": "^15.5.4", "react-hot-loader": "3.0.0-beta.6", - "react-icons": "^2.2.1", + "react-icons": "^2.2.5", + "react-portal": "^3.1.0", "react-redux": "^5.0.5", "react-router-dom": "^4.0.0", - "react-split-pane": "^0.1.61", + "react-split-pane": "^0.1.63", "redux": "^3.5.2", "redux-actions": "^2.0.1", "redux-electron-store": "^0.4.1", diff --git a/src/main/actions.js b/src/main/actions.js index 8df943ab..11601f1b 100644 --- a/src/main/actions.js +++ b/src/main/actions.js @@ -1,8 +1,9 @@ import { ipcMain as ipc, BrowserWindow } from 'electron'; -import fs from 'fs-extra'; import { pushUpdate, updateInstalled } from '../shared/actions/update'; import { getWindowManager } from './lib/window-manager'; import { startAutoUpdate, installUpdates } from './lib/updater'; +import { openFile, newFile, openFileForImporting } from './lib/files'; +import { setupMenu } from './menu'; const windowManager = getWindowManager(); @@ -11,35 +12,38 @@ export function setupActions(store) { store.dispatch(updateInstalled()); if (process.env.NODE_ENV !== 'development') { - try { - startAutoUpdate((releaseNotes, releaseName) => { - store.dispatch(pushUpdate({ - releaseNotes, - releaseName - })); - }); - - ipc.on('quit-and-install', () => { - installUpdates(); - }); - } catch (err) { - console.warn('Auto update failed.'); - } - } - - ipc.on('read-archive', (event, arg) => { - fs.ensureFileSync(arg); - event.returnValue = fs.readFileSync(arg).toString('utf-8'); - }); + startAutoUpdate((releaseNotes, releaseName) => { + store.dispatch(pushUpdate({ + releaseNotes, + releaseName + })); + }); - ipc.on('write-archive', (event, arg) => { - fs.outputFileSync(arg.filename, arg.content); - event.returnValue = true; - }); + ipc.on('quit-and-install', () => { + installUpdates(); + }); + } ipc.on('show-file-manager', () => { windowManager.buildWindowOfType('file-manager', null, { parent: BrowserWindow.getFocusedWindow() }); }); + + ipc.on('open-file-dialog', () => { + openFile(); + }); + + ipc.on('new-file-dialog', () => { + newFile(); + }); + + ipc.on('archive-list-updated', (e, payload) => { + setupMenu(store); + }); + + ipc.on('show-import-dialog', (e, payload) => { + const { type, archiveId } = payload; + openFileForImporting(undefined, type, archiveId); + }); } diff --git a/src/main/app.js b/src/main/app.js index 02fdf3f5..676296c8 100644 --- a/src/main/app.js +++ b/src/main/app.js @@ -1,13 +1,13 @@ -import { app, Menu } from 'electron'; +import { app } from 'electron'; import pify from 'pify'; import log from 'electron-log'; import { throttle } from 'lodash'; import jsonStorage from 'electron-json-storage'; import configureStore from '../shared/store/configure-store'; -import menuTemplate from './config/menu'; +import { setupMenu } from './menu'; import { getWindowManager } from './lib/window-manager'; import { loadFile } from './lib/files'; -import { isWindows } from './lib/platform'; +import { isWindows } from '../shared/utils/platform'; import { setupActions } from './actions'; import { setupWindows } from './windows'; @@ -85,6 +85,15 @@ app.on('ready', async () => { try { state = await storage.get('state'); log.info('Restoring state...', state); + + // Temporary bridge to new format + // @TODO: remove this! + if (state.archives && !Array.isArray(state.archives)) { + storage.set('state.backup', state); + log.info('Updating old state format to new.'); + state.archives = []; + state.settingsByArchiveId = {}; + } } catch (err) { log.error('Unable to read state json file', err); } @@ -98,6 +107,7 @@ app.on('ready', async () => { // Setup Windows & IPC Actions setupWindows(store); setupActions(store); + setupMenu(store); appIsReady = true; @@ -110,11 +120,6 @@ app.on('ready', async () => { initialFile = null; } }); - - // Show standard menu - Menu.setApplicationMenu( - Menu.buildFromTemplate(menuTemplate) - ); }); // When user closes all windows diff --git a/src/main/config/menu.js b/src/main/config/menu.js deleted file mode 100644 index 5016e49e..00000000 --- a/src/main/config/menu.js +++ /dev/null @@ -1,37 +0,0 @@ -import { app } from 'electron'; -import { isOSX } from '../lib/platform'; -import appMenu from './menu/app'; -import fileMenu from './menu/file'; -import editMenu from './menu/edit'; -import viewMenu from './menu/view'; -import windowMenu from './menu/window'; -import helpMenu from './menu/help'; - -export default [ - isOSX() ? { - label: app.getName(), - submenu: appMenu - } : [], - { - label: isOSX() ? 'Archive' : 'File', - submenu: fileMenu - }, - { - label: 'Edit', - submenu: editMenu - }, - { - label: 'View', - submenu: viewMenu - }, - { - label: 'Window', - role: 'window', - submenu: windowMenu - }, - { - label: 'Help', - role: 'help', - submenu: helpMenu - } -]; diff --git a/src/main/config/menu/app.js b/src/main/config/menu/app.js deleted file mode 100644 index b87ad223..00000000 --- a/src/main/config/menu/app.js +++ /dev/null @@ -1,67 +0,0 @@ -import { app } from 'electron'; -import { isOSX } from '../../lib/platform'; - -let menuItems = []; // eslint-disable-line import/no-mutable-exports - -if (isOSX()) { - menuItems = [ - { - label: 'About Buttercup', - role: 'about' - }, - { - type: 'separator' - }, - { - label: 'Services', - role: 'services', - submenu: [] - }, - { - type: 'separator' - }, - { - label: 'Hide Buttercup', - accelerator: 'Command+H', - role: 'hide' - }, - { - label: 'Hide Others', - accelerator: 'Command+Alt+H', - role: 'hideothers' - }, - { - label: 'Show All', - role: 'unhide' - }, - { - type: 'separator' - }, - { - label: 'Quit', - accelerator: 'Command+Q', - click: () => { - app.quit(); - } - } - ]; -} else { - menuItems = [ - { - label: 'About Buttercup', - role: 'about' - }, - { - type: 'separator' - }, - { - label: 'Quit', - accelerator: 'CmdOrCtrl+Q', - click: () => { - app.quit(); - } - } - ]; -} - -export default menuItems; diff --git a/src/main/config/menu/edit.js b/src/main/config/menu/edit.js deleted file mode 100644 index 969dc681..00000000 --- a/src/main/config/menu/edit.js +++ /dev/null @@ -1,35 +0,0 @@ -export default [ - { - label: 'Undo', - accelerator: 'CmdOrCtrl+Z', - role: 'undo' - }, - { - label: 'Redo', - accelerator: 'Shift+CmdOrCtrl+Z', - role: 'redo' - }, - { - type: 'separator' - }, - { - label: 'Cut', - accelerator: 'CmdOrCtrl+X', - role: 'cut' - }, - { - label: 'Copy', - accelerator: 'CmdOrCtrl+C', - role: 'copy' - }, - { - label: 'Paste', - accelerator: 'CmdOrCtrl+V', - role: 'paste' - }, - { - label: 'Select All', - accelerator: 'CmdOrCtrl+A', - role: 'selectall' - } -]; diff --git a/src/main/config/menu/file.js b/src/main/config/menu/file.js deleted file mode 100644 index d7fc47ec..00000000 --- a/src/main/config/menu/file.js +++ /dev/null @@ -1,57 +0,0 @@ -import { getWindowManager } from '../../lib/window-manager'; -import { openFile, openFileForImporting, newFile } from '../../lib/files'; - -const windowManager = getWindowManager(); - -export default [ - { - label: 'New Archive', - accelerator: 'CmdOrCtrl+N', - click: (item, focusedWindow) => newFile(focusedWindow) - }, - { - label: 'Open Archive', - accelerator: 'CmdOrCtrl+O', - click: (item, focusedWindow) => openFile(focusedWindow) - }, - { - type: 'separator' - }, - // @TODO: Gray out this option dynamically - // when target is not available - { - label: 'Import', - submenu: [ - { - label: 'From KeePass archive (.kdbx)', - click: (item, focusedWindow) => openFileForImporting(focusedWindow, 'kdbx') - }, - { - label: 'From 1Password archive (.1pif)', - click: (item, focusedWindow) => openFileForImporting(focusedWindow, '1pif') - }, - { - label: 'From LastPass archive (.csv)', - click: (item, focusedWindow) => openFileForImporting(focusedWindow, 'csv') - } - ] - }, - { - type: 'separator' - }, - { - label: 'Open New Window', - accelerator: 'CmdOrCtrl+Shift+N', - click: () => { - windowManager.buildWindowOfType('main'); - } - }, - { - type: 'separator' - }, - { - label: 'Close Window', - accelerator: 'CmdOrCtrl+W', - role: 'close' - } -]; diff --git a/src/main/config/menu/help.js b/src/main/config/menu/help.js deleted file mode 100644 index 10cf5cae..00000000 --- a/src/main/config/menu/help.js +++ /dev/null @@ -1,23 +0,0 @@ -import { shell } from 'electron'; -import pkg from '../../../../package.json'; - -export default [ - { - label: 'Visit Our Website', - click: () => { - shell.openExternal('https://buttercup.pw'); - } - }, - { - label: 'Privacy Policy', - click: () => { - shell.openExternal('https://buttercup.pw/privacy'); - } - }, - { - label: `View Changelog For v${pkg.version}`, - click: () => { - shell.openExternal(`https://github.com/buttercup/buttercup/releases/tag/v${pkg.version}`); - } - } -]; diff --git a/src/main/config/menu/view.js b/src/main/config/menu/view.js deleted file mode 100644 index a88b961b..00000000 --- a/src/main/config/menu/view.js +++ /dev/null @@ -1,41 +0,0 @@ -import { isOSX } from '../../lib/platform'; - -export default [ - { - label: 'Reload', - accelerator: 'CmdOrCtrl+R', - click(item, focusedWindow) { - if (focusedWindow) { - focusedWindow.reload(); - } - } - }, - { - label: 'Toggle Full Screen', - accelerator: (() => { - if (isOSX()) { - return 'Ctrl+Command+F'; - } - return 'F11'; - })(), - click: (item, focusedWindow) => { - if (focusedWindow) { - focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); - } - } - }, - { - label: 'Toggle Developer Tools', - accelerator: (function() { - if (isOSX()) { - return 'Alt+Command+I'; - } - return 'Ctrl+Shift+I'; - })(), - click: (item, focusedWindow) => { - if (focusedWindow) { - focusedWindow.toggleDevTools(); - } - } - } -]; diff --git a/src/main/config/menu/window.js b/src/main/config/menu/window.js deleted file mode 100644 index 51de7767..00000000 --- a/src/main/config/menu/window.js +++ /dev/null @@ -1,19 +0,0 @@ -export default [ - { - label: 'Minimize', - accelerator: 'CmdOrCtrl+M', - role: 'minimize' - }, - { - label: 'Close', - accelerator: 'CmdOrCtrl+W', - role: 'close' - }, - { - type: 'separator' - }, - { - label: 'Bring All to Front', - role: 'front' - } -]; diff --git a/src/main/lib/buttercup.js b/src/main/lib/buttercup.js index 2512ceba..6e41f060 100644 --- a/src/main/lib/buttercup.js +++ b/src/main/lib/buttercup.js @@ -1,4 +1,5 @@ import { importFromKDBX, importFrom1PIF, importFromLastPass } from 'buttercup-importer'; +import { ImportTypes } from '../../shared/buttercup/types'; /** * Import archive from file @@ -10,11 +11,11 @@ import { importFromKDBX, importFrom1PIF, importFromLastPass } from 'buttercup-im */ export function importArchive(type, filename, password) { switch (type) { - case 'kdbx': + case ImportTypes.KEEPASS: return importFromKDBX(filename, password).then(archive => archive.getHistory()); - case '1pif': + case ImportTypes.ONE_PASSWORD: return importFrom1PIF(filename).then(archive => archive.getHistory()); - case 'csv': + case ImportTypes.LASTPASS: return importFromLastPass(filename).then(archive => archive.getHistory()); default: throw new Error('Wrong import type provided'); diff --git a/src/main/lib/files.js b/src/main/lib/files.js index d2363168..5dd850e4 100644 --- a/src/main/lib/files.js +++ b/src/main/lib/files.js @@ -1,9 +1,10 @@ import path from 'path'; -import { BrowserWindow, dialog } from 'electron'; -import { ArchiveTypes } from '../../shared/buttercup/types'; -import { isWindows } from './platform'; +import { dialog, ipcMain as ipc } from 'electron'; +import { ArchiveTypes, ImportTypeInfo } from '../../shared/buttercup/types'; +import { isWindows } from '../../shared/utils/platform'; import { getWindowManager } from './window-manager'; import { importArchive } from './buttercup'; +import { getMainWindow } from '../utils/window'; const windowManager = getWindowManager(); const dialogOptions = { @@ -27,14 +28,14 @@ function normalizePath(filePath) { * @returns {void} */ function showOpenDialog(focusedWindow) { - const filename = dialog.showOpenDialog(focusedWindow, { + dialog.showOpenDialog(focusedWindow, { ...dialogOptions, title: 'Load a Buttercup Archive' + }, filename => { + if (filename && filename.length > 0) { + loadFile(filename[0], focusedWindow); + } }); - - if (filename && filename.length > 0) { - loadFile(filename[0], focusedWindow); - } } /** @@ -45,14 +46,14 @@ function showOpenDialog(focusedWindow) { * @returns {void} */ function showSaveDialog(focusedWindow) { - const filename = dialog.showSaveDialog(focusedWindow, { + dialog.showSaveDialog(focusedWindow, { ...dialogOptions, title: 'Create a New Buttercup Archive' + }, filename => { + if (typeof filename === 'string' && filename.length > 0) { + loadFile(filename, focusedWindow, true); + } }); - - if (typeof filename === 'string' && filename.length > 0) { - loadFile(filename, focusedWindow, true); - } } /** @@ -71,17 +72,11 @@ export function loadFile(filePath, win, isNew = false) { return; } if (!win) { - win = BrowserWindow.getFocusedWindow(); + win = getMainWindow(); } - // If there's a window and it's in intro state - if (win && win.isIntro()) { - win.rpc.emit('load-archive', payload); - return; + if (win) { + win.webContents.send('load-archive', payload); } - // Otherwise just create a new window - windowManager.buildWindowOfType('main', (win, rpc) => { - rpc.emit('load-archive', payload); - }); } /** @@ -92,40 +87,39 @@ export function loadFile(filePath, win, isNew = false) { * @returns {void} */ export function openFile(focusedWindow) { + focusedWindow = getMainWindow(focusedWindow); if (!focusedWindow) { - focusedWindow = BrowserWindow.getFocusedWindow(); + windowManager.buildWindowOfType('main', win => showOpenDialog(win)); + return; } + showOpenDialog(focusedWindow); +} + +/** + * Create a new file and open it in Buttercup + * then ask the user for a password + * + * @param {BrowserWindow} focusedWindow + * @returns {void} + */ +export function newFile(focusedWindow) { + focusedWindow = getMainWindow(focusedWindow); if (!focusedWindow) { - windowManager.buildWindowOfType('main', win => { - showOpenDialog(win); - }); + windowManager.buildWindowOfType('main', win => showSaveDialog(win)); return; } - showOpenDialog(focusedWindow); + showSaveDialog(focusedWindow); } /** * @param {BrowserWindow} focusedWindow * @param {string} type + * @param {string} archiveId */ -const showImportDialog = function(focusedWindow, type) { - const types = { - '1pif': { - password: false, - name: '1Password' - }, - 'kdbx': { - password: true, - name: 'KeePass' - }, - 'csv': { - password: false, - name: 'LastPass' - } - }; - const typeInfo = types[type]; +const showImportDialog = function(focusedWindow, type, archiveId) { + const typeInfo = ImportTypeInfo[type]; - if (!Object.keys(types).includes(type)) { + if (!typeInfo) { throw new Error('Invalid import type requested'); } @@ -139,75 +133,46 @@ const showImportDialog = function(focusedWindow, type) { }; const handleSuccess = history => { - focusedWindow.rpc.emit('import-history', { history }); + focusedWindow.webContents.send('import-history', { history, archiveId }); }; - const [ filename ] = dialog.showOpenDialog(focusedWindow, { + dialog.showOpenDialog(focusedWindow, { filters: [{ name: `${typeInfo.name} Archives`, - extensions: [type] + extensions: [typeInfo.extension] }], title: `Load a ${typeInfo.name} archive` - }); - - if (!filename) { - return; - } - - if (typeInfo.password) { - focusedWindow.rpc.emit('import-history-prompt'); - focusedWindow.rpc.once('import-history-prompt-resp', password => { - importArchive(type, filename, password) + }, (files) => { + if (!files) { + return; + } + const [ filename ] = files; + if (typeInfo.password) { + focusedWindow.webContents.send('import-history-prompt', type); + ipc.once('import-history-prompt-resp', (e, password) => { + importArchive(type, filename, password) + .then(handleSuccess) + .catch(handleError); + }); + } else { + importArchive(type, filename) .then(handleSuccess) .catch(handleError); - }); - } else { - importArchive(type, filename) - .then(handleSuccess) - .catch(handleError); - } + } + }); }; /** * @param {BrowserWindow} focusedWindow * @param {string} type + * @param {string} archiveId */ -export function openFileForImporting(focusedWindow, type) { - if (focusedWindow && focusedWindow.isIntro()) { - dialog.showMessageBox(focusedWindow, { - title: 'Importing is not available', - message: 'To import an archive file, you must unlock a Buttercup archive first.' - }); - return; - } +export function openFileForImporting(focusedWindow, type, archiveId) { + focusedWindow = getMainWindow(focusedWindow); if (!focusedWindow) { - focusedWindow = BrowserWindow.getFocusedWindow(); + throw new Error('Import function should not be running without the main window running.'); } - if (!focusedWindow) { - windowManager.buildWindowOfType('main', win => { - showImportDialog(win, type); - }); - return; - } - showImportDialog(focusedWindow, type); -} - -/** - * Create a new file and open it in Buttercup - * then ask the user for a password - * - * @param {BrowserWindow} focusedWindow - * @returns {void} - */ -export function newFile(focusedWindow) { - if (!focusedWindow) { - focusedWindow = BrowserWindow.getFocusedWindow(); - } - if (!focusedWindow) { - windowManager.buildWindowOfType('main', win => showSaveDialog(win)); - return; - } - showSaveDialog(focusedWindow); + showImportDialog(focusedWindow, type, archiveId); } diff --git a/src/main/lib/rpc.js b/src/main/lib/rpc.js deleted file mode 100644 index 7cdc27f9..00000000 --- a/src/main/lib/rpc.js +++ /dev/null @@ -1,54 +0,0 @@ -import { EventEmitter } from 'events'; -import { ipcMain } from 'electron'; -import uuid from 'uuid'; - -class Server extends EventEmitter { - constructor(win) { - super(); - this.win = win; - this.ipcListener = this.ipcListener.bind(this); - - if (this.destroyed) { - return; - } - - const uid = uuid.v4(); - this.id = uid; - - ipcMain.on(uid, this.ipcListener); - - // we intentionally subscribe to `on` instead of `once` - // to support reloading the window and re-initializing - // the channel - this.wc.on('did-finish-load', () => { - this.wc.send('init', uid); - }); - } - - get wc() { - return this.win.webContents; - } - - ipcListener(event, {ev, data}) { - super.emit(ev, data); - } - - emit(ch, data) { - this.wc.send(this.id, {ch, data}); - } - - destroy() { - this.removeAllListeners(); - this.wc.removeAllListeners(); - if (this.id) { - ipcMain.removeListener(this.id, this.ipcListener); - } else { - // mark for `genUid` in constructor - this.destroyed = true; - } - } -} - -export function createRPC(win) { - return new Server(win); -} diff --git a/src/main/menu.js b/src/main/menu.js new file mode 100644 index 00000000..245fa602 --- /dev/null +++ b/src/main/menu.js @@ -0,0 +1,222 @@ +import { app, shell, Menu } from 'electron'; +import { isOSX } from '../shared/utils/platform'; +import { getCurrentArchiveId, getAllArchives, getSetting } from '../shared/selectors'; +import { setSetting } from '../shared/actions/settings'; +import { ImportTypeInfo } from '../shared/buttercup/types'; +import { openFile, openFileForImporting, newFile } from './lib/files'; +import { getWindowManager } from './lib/window-manager'; +import { getMainWindow } from './utils/window'; +import pkg from '../../package.json'; + +const defaultTemplate = [ + { + label: isOSX() ? 'Archive' : 'File', + submenu: [ + { + label: 'New Archive', + accelerator: 'CmdOrCtrl+N', + click: (item, focusedWindow) => newFile(focusedWindow) + }, + { + label: 'Open Archive', + accelerator: 'CmdOrCtrl+O', + click: (item, focusedWindow) => openFile(focusedWindow) + }, + { + label: 'Connect Cloud Sources', + accelerator: 'CmdOrCtrl+Shift+C', + click: (item, focusedWindow) => { + getWindowManager().buildWindowOfType('file-manager', null, { + parent: getMainWindow(focusedWindow) + }); + } + }, + { + type: 'separator' + }, + // @TODO: Gray out this option dynamically + // when target is not available + {}, + { + type: 'separator' + }, + { + role: 'close' + } + ] + }, + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'pasteandmatchstyle' }, + { role: 'delete' }, + { role: 'selectall' } + ] + }, + { + label: 'View', + submenu: [ + { type: 'separator' }, + { role: 'reload' }, + { role: 'forcereload' }, + { role: 'toggledevtools' }, + { type: 'separator' }, + { role: 'togglefullscreen' } + ] + }, + { + label: 'Window', + role: 'window', + submenu: [ + { role: 'minimize' }, + { role: 'close' } + ] + }, + { + label: 'Help', + role: 'help', + submenu: [ + { + label: 'Visit Our Website', + click: () => { + shell.openExternal('https://buttercup.pw'); + } + }, + { + label: 'Privacy Policy', + click: () => { + shell.openExternal('https://buttercup.pw/privacy'); + } + }, + { + label: `View Changelog For v${pkg.version}`, + click: () => { + shell.openExternal(`https://github.com/buttercup/buttercup/releases/tag/v${pkg.version}`); + } + } + ] + } +]; + +if (isOSX()) { + defaultTemplate.unshift({ + label: app.getName(), + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'services', submenu: [] }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideothers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ] + }); + + // Edit + defaultTemplate[2].submenu.push( + { type: 'separator' }, + { + label: 'Speech', + submenu: [ + { role: 'startspeaking' }, + { role: 'stopspeaking' } + ] + } + ); + + // Window + defaultTemplate[4].submenu.push( + { role: 'close' }, + { role: 'minimize' }, + { role: 'zoom' }, + { type: 'separator' }, + { role: 'front' } + ); +}; + +export function setupMenu(store) { + const state = store.getState(); + const archives = getAllArchives(state); + const currentArchiveId = getCurrentArchiveId(state); + let condenced = Boolean(getSetting(state, 'condencedSidebar')); + + const template = defaultTemplate.map((item, i) => { + // OSX has one more menu item + const index = isOSX() ? i : i + 1; + + switch (index) { + // Archive / File Menu: + case 1: + return { + ...item, + submenu: item.submenu.map((sub, i) => { + if (i === 4) { + return { + label: 'Import', + submenu: Object.entries(ImportTypeInfo).map(([typeKey, type]) => ({ + label: `From ${type.name} archive (.${type.extension})`, + submenu: archives.map(archive => ({ + label: `To ${archive.name}`, + enabled: archive.status === 'unlocked', + click: (item, focusedWindow) => openFileForImporting(focusedWindow, typeKey, archive.id) + })) + })) + }; + } + return sub; + }) + }; + // View Menu: + case 3: + return { + ...item, + submenu: [ + { + label: 'Condenced Sidebar', + type: 'checkbox', + checked: condenced, + accelerator: 'CmdOrCtrl+B', + click: () => { + condenced = !condenced; + store.dispatch(setSetting('condencedSidebar', condenced)); + } + }, + ...item.submenu + ] + }; + // Window Menu: + case 4: + return { + ...item, + submenu: [ + ...item.submenu, + { type: 'separator' }, + ...archives.map((archive, index) => ({ + label: archive.name, + accelerator: `CmdOrCtrl+${index + 1}`, + type: 'checkbox', + click: () => { + const win = getMainWindow(); + if (win) { + win.webContents.send('set-current-archive', archive.id); + } + }, + checked: (archive.id === currentArchiveId) + })) + ] + }; + } + + return item; + }); + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} diff --git a/src/main/utils/window.js b/src/main/utils/window.js new file mode 100644 index 00000000..06bbbce5 --- /dev/null +++ b/src/main/utils/window.js @@ -0,0 +1,17 @@ +import { BrowserWindow } from 'electron'; +import { getWindowManager } from '../lib/window-manager'; + +export function getMainWindow(focusedWindow = BrowserWindow.getFocusedWindow()) { + if (focusedWindow) { + return focusedWindow; + } + const wins = getWindowManager().getWindowsOfType('main'); + if (wins.length > 0) { + const win = wins[0]; + if (win.isMinimized()) { + win.restore(); + } + return win; + } + return null; +} diff --git a/src/main/windows.js b/src/main/windows.js index cf0161a1..a7379a2d 100644 --- a/src/main/windows.js +++ b/src/main/windows.js @@ -1,62 +1,49 @@ -import { app, BrowserWindow } from 'electron'; -import { throttle } from 'lodash'; +import { app, BrowserWindow, ipcMain as ipc } from 'electron'; +import debounce from 'lodash/debounce'; import { getWindowManager } from './lib/window-manager'; import { getPathToFile } from './lib/utils'; -import { createRPC } from './lib/rpc'; -import { loadFile, openFile, newFile } from './lib/files'; +import { loadFile } from './lib/files'; +import { config } from '../shared/config'; const windowManager = getWindowManager(); export function setupWindows() { // Intro Screen windowManager.setBuildProcedure('main', callback => { + const [width, height] = config.get('window.size', [950, 700]); // Create the browser window. const win = new BrowserWindow({ - width: 870, - height: 550, + width, + height, minWidth: 680, minHeight: 500, title: app.getName(), titleBarStyle: 'hidden-inset', show: process.env.NODE_ENV === 'development', - vibrancy: 'light' + darkTheme: true, + vibrancy: 'ultra-dark' }); win.loadURL(getPathToFile('views/index.html')); - const rpc = createRPC(win); - win.rpc = rpc; - - win.isIntro = () => { - return win.getTitle().toLowerCase().match(/welcome/i) !== null; - }; - // When user drops a file on the window win.webContents.on('will-navigate', (e, url) => { e.preventDefault(); loadFile(url, win); }); - rpc.on('open-file-dialog', () => { - openFile(); - }); - - rpc.on('new-file-dialog', () => { - newFile(); - }); - win.once('ready-to-show', () => { win.show(); }); - rpc.once('init', () => { + ipc.once('init', () => { if (callback) { - callback(win, rpc); + callback(win); } }); - win.on('resize', throttle(() => { - rpc.emit('size-change', win.getSize()); + win.on('resize', debounce(() => { + config.set('window.size', win.getSize()); }, 2000)); win.once('closed', () => { diff --git a/src/renderer/components/add-archive-button.js b/src/renderer/components/add-archive-button.js new file mode 100644 index 00000000..9cde2495 --- /dev/null +++ b/src/renderer/components/add-archive-button.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from '@buttercup/ui'; +import ArchiveIcon from 'react-icons/lib/md/add'; +import { showContextMenu } from '../system/menu'; + +const AddArchiveButton = ({ condenced = false, onNewClick, onOpenClick, onCloudClick, ...props }) => ( + +); + +AddArchiveButton.propTypes = { + condenced: PropTypes.bool, + onOpenClick: PropTypes.func, + onNewClick: PropTypes.func, + onCloudClick: PropTypes.func +}; + +export default AddArchiveButton; diff --git a/src/renderer/components/archive/entries.js b/src/renderer/components/archive/entries.js index 1b52f2a1..85645783 100644 --- a/src/renderer/components/archive/entries.js +++ b/src/renderer/components/archive/entries.js @@ -1,16 +1,30 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import PlusIcon from 'react-icons/lib/md/add'; -import cx from 'classnames'; +import styled from 'styled-components'; import { Button } from '@buttercup/ui'; -import { isOSX } from '../../system/utils'; +import { isOSX } from '../../../shared/utils/platform'; import { showContextMenu, createMenuFromGroups, createCopyMenu } from '../../system/menu'; -import Column from '../column'; -import styles from '../../styles/entries'; +import BaseColumn from '../column'; import List from './entries-list'; import SearchField from './search-field'; import SortButton from './sort-button'; +const Column = styled(BaseColumn)` + background-color: ${isOSX() ? 'var(--entries-bg-mac)' : 'var(--entries-bg)'}; + color: #fff; +`; + +const SearchWrapper = styled.div` + display: flex; + flex-direction: row; + margin-right: calc(-1 * var(--spacing-half)); + + button { + color: #fff; + } +`; + class Entries extends Component { handleFilterChange = value => { this.props.onFilterChange(value); @@ -45,22 +59,21 @@ class Entries extends Component { const addButton = ( ); const filterNode = ( -
+ -
+ ); return ( diff --git a/src/renderer/components/empty-view.js b/src/renderer/components/empty-view.js index 8938aefc..9750841d 100644 --- a/src/renderer/components/empty-view.js +++ b/src/renderer/components/empty-view.js @@ -2,6 +2,9 @@ import PropTypes from 'prop-types'; import React from 'react'; import styled from 'styled-components'; import { Flex } from 'styled-flexbox'; +import { isOSX } from '../../shared/utils/platform'; +import logo from '../styles/img/solo-logo.svg'; +import AddArchiveButton from '../containers/add-archive-button'; const Caption = styled.figcaption` color: var(--gray-dark); @@ -30,3 +33,33 @@ EmptyView.propTypes = { }; export default EmptyView; + +const ColoredFlex = styled(Flex)` + background-color: RGBA(20, 20, 20, .8); + color: #fff; +`; + +const Title = styled.h3` + margin-bottom: var(--spacing-half); +`; + +export const NoArchiveSelected = () => ( + +
+ + Welcome back to Buttercup. + Unlock an archive to begin ({isOSX() ? '⌘' : 'Ctrl'}+1). +
+
+); + +export const WelcomeScreen = () => ( + +
+ + Welcome to Buttercup. + You haven't added have any archives yet. Why not add one? +
+ +
+); diff --git a/src/renderer/components/recent-files.js b/src/renderer/components/recent-files.js deleted file mode 100644 index 14fdd729..00000000 --- a/src/renderer/components/recent-files.js +++ /dev/null @@ -1,102 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import HistoryIcon from 'react-icons/lib/go/history'; -import { Button } from '@buttercup/ui'; -import { brands } from '../../shared/buttercup/brands'; -import styles from '../styles/recent-files'; -import { parsePath } from '../system/utils'; -import { showContextMenu } from '../system/menu'; -import EmptyView from './empty-view'; -import Column from './column'; - -const File = ({archive, onClick, onRemoveClick}) => { - const { base, dir } = parsePath(archive.path); - return ( -
  • { - e.stopPropagation(); - showContextMenu([{ - label: `Unlock ${base}`, - click: onClick - }, { - label: `Remove ${base} from history`, - click: onRemoveClick - }]); - }} - > -
    -
    - -
    -
    -
    {base}
    -
    {dir}
    -
    -
    -
  • - ); -}; - -File.propTypes = { - archive: PropTypes.object, - onClick: PropTypes.func, - onRemoveClick: PropTypes.func -}; - -class RecentFiles extends Component { - static propTypes = { - archives: PropTypes.array, - onRemoveClick: PropTypes.func, - onClearClick: PropTypes.func, - onClick: PropTypes.func - }; - - showContextMenu = () => { - showContextMenu([ - { - label: 'Clear History', - click: this.props.onClearClick - } - ]); - } - - renderEmptyState() { - return ( -
    - -
    - ); - } - - render() { - const { archives } = this.props; - - if (archives.length === 0) { - return this.renderEmptyState(); - } - - const footer = ( - - ); - - return ( - -
    -
    History:
    -
      - {archives.map(archive => - this.props.onClick(archive)} - onRemoveClick={() => this.props.onRemoveClick(archive.id)} - /> - )} -
    -
    -
    - ); - } -} - -export default RecentFiles; diff --git a/src/renderer/components/sidebar-item.js b/src/renderer/components/sidebar-item.js new file mode 100644 index 00000000..0cc19ff0 --- /dev/null +++ b/src/renderer/components/sidebar-item.js @@ -0,0 +1,214 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import capitalize from 'lodash/capitalize'; +import LockOpen from 'react-icons/lib/md/lock-open'; +import LockClosed from 'react-icons/lib/md/lock-outline'; +import { GithubPicker } from 'react-color'; +import Portal from 'react-portal'; +import { brands } from '../../shared/buttercup/brands'; +import { ImportTypeInfo } from '../../shared/buttercup/types'; +import { showContextMenu } from '../system/menu'; + +const Wrapper = styled.li` + display: flex; + align-items: center; + color: #fff; + background-color: ${props => props.active ? 'rgba(255, 255, 255, .1)' : 'transparent'}; + padding: var(--spacing-half) var(--spacing-one); + cursor: ${props => props.locked ? 'pointer' : 'default'} !important; + + .status { + font-weight: 300; + font-size: .75em; + color: ${props => props.locked ? 'var(--red)' : 'var(--gray-dark)'}; + text-transform: uppercase; + display: block; + margin-top: .3em; + + svg { + vertical-align: -2px !important; + margin-right: 3px; + height: 12px; + width: 12px; + } + } + + section { + font-size: .9em; + flex: 1; + min-width: 0; + padding-left: var(--spacing-one); + + div { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +`; + +const Avatar = styled.div` + width: 3rem; + height: 3rem; + border-radius: 50px; + border: 2px solid rgba(255, 255, 255, .2); + background-color: ${props => props.color}; + font-weight: 400; + font-size: 1rem; + display: flex; + align-items: center; + justify-content: center; + position: relative; + + &:hover { + .cog { + display: block; + } + } +`; + +const Icon = styled.figure` + margin: 0; + padding: 0; + position: absolute; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background-color: #fff; + width: 18px; + height: 18px; + transform: translate(20%, 20%); + + img { + width: 14px; + display: block; + } +`; + +const PickerWrapper = styled.div` + position: fixed; + top: ${props => props.top + 20}px; + left: ${props => props.left + 5}px; +`; + +class SidebarItem extends Component { + static propTypes = { + archive: PropTypes.object, + active: PropTypes.bool, + condenced: PropTypes.bool, + index: PropTypes.number, + onClick: PropTypes.func, + onRemoveClick: PropTypes.func, + onArchiveUpdate: PropTypes.func, + showImportDialog: PropTypes.func + }; + + state = { + isPickerOpen: false, + top: 0, + left: 0 + }; + + handleContextMenu = () => { + const { status, name, id } = this.props.archive; + showContextMenu([{ + label: `${status === 'locked' ? 'Unlock' : 'Open'} ${name}`, + accelerator: `CmdOrCtrl+${this.props.index + 1}`, + click: this.props.onClick + }, { + label: 'Change Color', + click: this.showColorPopup + }, { + type: 'separator' + }, { + label: 'Import', + submenu: Object.entries(ImportTypeInfo).map(([type, typeInfo]) => ({ + label: `From ${typeInfo.name} archive (.${typeInfo.extension})`, + enabled: (status === 'unlocked'), + click: () => this.props.showImportDialog({ + type, + archiveId: id + }) + })) + }, { + type: 'separator' + }, { + label: `Remove ${name}`, + click: this.props.onRemoveClick + }]); + } + + showColorPopup = () => { + const el = this.avatarRef; + const bodyRect = document.body.getBoundingClientRect(); + const targetRect = el.getBoundingClientRect(); + this.setState({ + isPickerOpen: true, + top: (targetRect.top - bodyRect.top) + el.clientHeight - 10, + left: targetRect.left - bodyRect.left + }); + } + + handlePickerClose = () => { + this.setState({ isPickerOpen: false }); + } + + handleColorChange = color => { + this.props.onArchiveUpdate({ + ...this.props.archive, + color: color.hex + }); + } + + render() { + const { archive, onClick, active, condenced } = this.props; + const { name, color, status, type } = archive; + const locked = status === 'locked'; + + const formattedName = capitalize(name.replace('.bcup', '')); + const briefName = capitalize(name.substring(0, 2)); + + return ( + + { + this.avatarRef = ref; + }}> + {briefName} + {condenced && brands[type].remote && + {brands[type].name} + } + + + + + + + {!condenced &&
    +
    {formattedName}
    + + {locked ? : } + {brands[type].name} + +
    } +
    + ); + } +} + +export default SidebarItem; diff --git a/src/renderer/components/sidebar.js b/src/renderer/components/sidebar.js new file mode 100644 index 00000000..96e6b737 --- /dev/null +++ b/src/renderer/components/sidebar.js @@ -0,0 +1,68 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styled from 'styled-components'; +import { isOSX } from '../../shared/utils/platform'; +import AddArchiveButton from '../containers/add-archive-button'; +import EmptyView from './empty-view'; +import BaseColumn from './column'; +import SidebarItem from './sidebar-item'; + +const Column = styled(BaseColumn)` + width: ${props => props.condenced ? 'var(--sidebar-width-condenced)' : 'var(--sidebar-width)'}; + height: 100%; + background-color: ${isOSX() ? 'transparent' : 'var(--sidebar-bg)'}; + display: flex; +`; + +const ArchiveList = styled.ul` + margin: calc(var(--spacing-one) * 3) 0 0 0; + padding: 0; +`; + +class RecentFiles extends Component { + static propTypes = { + condenced: PropTypes.bool, + archives: PropTypes.array, + currentArchiveId: PropTypes.string, + onRemoveClick: PropTypes.func, + onArchiveUpdate: PropTypes.func, + onClick: PropTypes.func, + showImportDialog: PropTypes.func + }; + + renderEmptyState() { + return ( +
    + +
    + ); + } + + render() { + const { archives, currentArchiveId, condenced } = this.props; + + const footer = ; + + return ( + + + {archives.map((archive, i) => + this.props.onClick(archive.id)} + onRemoveClick={() => this.props.onRemoveClick(archive.id)} + showImportDialog={this.props.showImportDialog} + /> + )} + + + ); + } +} + +export default RecentFiles; diff --git a/src/renderer/components/tree-view/index.js b/src/renderer/components/tree-view/index.js index 0b7990ce..4628ba8b 100644 --- a/src/renderer/components/tree-view/index.js +++ b/src/renderer/components/tree-view/index.js @@ -3,15 +3,21 @@ import { isString } from 'lodash'; import cx from 'classnames'; import PropTypes from 'prop-types'; import Tree, { TreeNode } from 'rc-tree'; +import styled from 'styled-components'; import PlusIcon from 'react-icons/lib/md/add'; import { Button } from '@buttercup/ui'; import { showContextMenu, createMenuFromGroups, createSortMenu } from '../../system/menu'; -import { isOSX } from '../../system/utils'; +import { isOSX } from '../../../shared/utils/platform'; import '../../styles/tree-view.global'; -import styles from '../../styles/tree-view'; -import Column from '../column'; +import BaseColumn from '../column'; import TreeLabel from './tree-label'; +const Column = styled(BaseColumn)` + background-color: ${isOSX() ? 'var(--groups-bg-mac)' : 'var(--groups-bg)'}; + color: #fff; + padding-top: var(--spacing-one); +`; + class TreeView extends Component { handleColumnRightClick() { const { sortMode, onSortModeChange } = this.props; @@ -142,6 +148,7 @@ class TreeView extends Component { onAddClick={this.handleAddClick} onRemoveClick={this.handleRemoveClick} onSaveClick={this.props.onSaveClick} + onCreateNew={this.props.onCreateNew} onDismissClick={this.props.onDismissClick} /> } @@ -162,7 +169,6 @@ class TreeView extends Component { icon={} >New Group } - className={cx(styles.column, isOSX() && styles.mac)} onContextMenu={() => this.handleColumnRightClick()} > { const { isNew, parentId, id } = this.props.node; - this.props.onSaveClick( - isNew, - isNew ? parentId : id, - title - ); + if (isNew) { + this.props.onCreateNew(parentId, id, title); + } else { + this.props.onSaveClick(id, title); + } } handleDismiss = () => { diff --git a/src/renderer/components/workspace.js b/src/renderer/components/workspace.js index e60274f6..cfdf731f 100644 --- a/src/renderer/components/workspace.js +++ b/src/renderer/components/workspace.js @@ -1,30 +1,53 @@ import PropTypes from 'prop-types'; import React from 'react'; -import Intro from '../components/intro'; +import styled from 'styled-components'; +import { Flex } from 'styled-flexbox'; import Archive from '../components/archive'; +import Sidebar from '../containers/sidebar'; import '../styles/workspace.global.scss'; import UpdateNotice from './update-notice'; +import { NoArchiveSelected, WelcomeScreen } from './empty-view'; -const Workspace = ({ currentArchive, update, installUpdate, setColumnSize, columnSizes }) => { +const Primary = styled(Flex)` + position: relative; +`; + +const Workspace = ({ + currentArchive, + archivesCount, + update, + installUpdate, + setColumnSize, + columnSizes, + condencedSidebar +}) => { return ( -
    - { - (currentArchive === null) - ? - : - } + + {archivesCount > 0 && } + + + + + + 0 && currentArchive === null}> + + + + + + + installUpdate()} /> -
    + ); }; Workspace.propTypes = { currentArchive: PropTypes.object, + archivesCount: PropTypes.number, update: PropTypes.object, columnSizes: PropTypes.object, + condencedSidebar: PropTypes.bool, installUpdate: PropTypes.func, setColumnSize: PropTypes.func, }; diff --git a/src/renderer/containers/add-archive-button.js b/src/renderer/containers/add-archive-button.js new file mode 100644 index 00000000..2c77ae20 --- /dev/null +++ b/src/renderer/containers/add-archive-button.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import { openArchive, newArchive, openFileManager } from '../../shared/actions/files'; +import AddArchiveButton from '../components/add-archive-button'; + +export default connect( + state => ({}), + { + onOpenClick: openArchive, + onNewClick: newArchive, + onCloudClick: openFileManager + } +)(AddArchiveButton); diff --git a/src/renderer/containers/recent-files.js b/src/renderer/containers/recent-files.js deleted file mode 100644 index 738e72b5..00000000 --- a/src/renderer/containers/recent-files.js +++ /dev/null @@ -1,15 +0,0 @@ -import { connect } from 'react-redux'; -import { removeArchive, clearArchives, loadArchive } from '../../shared/actions/archives'; -import { getSortedArchives } from '../../shared/selectors'; -import RecentFiles from '../components/recent-files'; - -export default connect( - state => ({ - archives: getSortedArchives(state) - }), - { - onRemoveClick: removeArchive, - onClearClick: clearArchives, - onClick: loadArchive - } -)(RecentFiles); diff --git a/src/renderer/containers/sidebar.js b/src/renderer/containers/sidebar.js new file mode 100644 index 00000000..4e2dbd5f --- /dev/null +++ b/src/renderer/containers/sidebar.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import { removeArchive, loadOrUnlockArchive, updateArchive, showImportDialog } from '../../shared/actions/archives'; +import { openArchive, newArchive, openFileManager } from '../../shared/actions/files'; +import { getAllArchives, getCurrentArchiveId } from '../../shared/selectors'; +import ArchiveList from '../components/sidebar'; + +export default connect( + state => ({ + archives: getAllArchives(state), + currentArchiveId: getCurrentArchiveId(state) + }), + { + onRemoveClick: removeArchive, + onClick: loadOrUnlockArchive, + onOpenClick: openArchive, + onNewClick: newArchive, + onArchiveUpdate: updateArchive, + onCloudClick: openFileManager, + showImportDialog + } +)(ArchiveList); diff --git a/src/renderer/containers/tree-view.js b/src/renderer/containers/tree-view.js index 5da3dec0..24754028 100644 --- a/src/renderer/containers/tree-view.js +++ b/src/renderer/containers/tree-view.js @@ -14,8 +14,9 @@ export default connect( { onAddClick: groupTools.addGroup, onRemoveClick: groupTools.removeGroup, - onSaveClick: groupTools.saveGroup, - onDismissClick: groupTools.dismissNewGroup, + onSaveClick: groupTools.saveGroupTitle, + onCreateNew: groupTools.createNewGroup, + onDismissClick: groupTools.dismissNewGroups, onRenameClick: groupTools.renameGroup, onEmptyTrash: groupTools.emptyTrash, onMoveGroup: groupTools.moveGroupToParent, diff --git a/src/renderer/containers/workspace.js b/src/renderer/containers/workspace.js index 42ca065c..b35b0016 100644 --- a/src/renderer/containers/workspace.js +++ b/src/renderer/containers/workspace.js @@ -1,13 +1,15 @@ import { connect } from 'react-redux'; import Workspace from '../components/workspace'; import { installUpdate } from '../../shared/actions/update'; -import { setColumnSize } from '../../shared/actions/ui'; -import { getCurrentArchive, getColumnSizes } from '../../shared/selectors'; +import { setColumnSize } from '../../shared/actions/settings'; +import { getCurrentArchive, getSetting, getArchivesCount } from '../../shared/selectors'; export default connect( state => ({ - columnSizes: getColumnSizes(state), + columnSizes: getSetting(state, 'columnSizes'), + condencedSidebar: getSetting(state, 'condencedSidebar'), currentArchive: getCurrentArchive(state), + archivesCount: getArchivesCount(state), update: state.update, }), { diff --git a/src/renderer/index.js b/src/renderer/index.js index b0f3ada4..0e0412d9 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -1,15 +1,18 @@ import Buttercup from 'buttercup-web'; import React from 'react'; +import { ipcRenderer as ipc } from 'electron'; import { render } from 'react-dom'; import { AppContainer } from 'react-hot-loader'; import configureStore from '../shared/store/configure-store'; -import { loadArchiveFromSource } from '../shared/actions/archives'; -import * as groupActions from '../shared/actions/groups'; -import * as uiActions from '../shared/actions/ui'; -import rpc from './system/rpc'; -import { getWorkspace } from './system/buttercup/archive'; -import { importHistoryFromRequest, showHistoryPasswordPrompt } from './system/buttercup/import'; -import { setWindowSize } from './system/utils'; +import { getSharedArchiveManager } from '../shared/buttercup/archive'; +import { linkArchiveManagerToStore } from '../shared/buttercup/store'; +import { + addArchiveFromSource, + loadOrUnlockArchive, + setCurrentArchive, + importHistoryIntoArchive +} from '../shared/actions/archives'; +import { showHistoryPasswordPrompt } from '../shared/buttercup/import'; import { setupShortcuts } from './system/shortcuts'; import Root from './containers/root'; @@ -19,40 +22,37 @@ unhandled(); // Make crypto faster! Buttercup.Web.HashingTools.patchCorePBKDF(); - -window.__defineGetter__('rpc', () => rpc); const store = configureStore({}, 'renderer'); -setWindowSize(870, 550, 'light'); +// temp +global.archiveManager = getSharedArchiveManager(); + +linkArchiveManagerToStore(store); setupShortcuts(store); -rpc.on('ready', () => { - rpc.emit('init'); -}); +// Reset current archive +store.dispatch(setCurrentArchive(null)); -rpc.on('load-archive', payload => { - store.dispatch(loadArchiveFromSource(payload)); -}); +ipc.send('init'); -rpc.on('is-in-workspace', () => { - rpc.emit('in-workspace', getWorkspace() !== null); +ipc.on('load-archive', (e, payload) => { + store.dispatch(addArchiveFromSource(payload)); }); -rpc.on('size-change', size => { - store.dispatch(uiActions.setWindowSize(size)); +ipc.on('set-current-archive', (e, payload) => { + store.dispatch(loadOrUnlockArchive(payload)); }); -rpc.on('import-history', request => { - importHistoryFromRequest(request); - store.dispatch(groupActions.reloadGroups()); +ipc.on('import-history', (e, payload) => { + store.dispatch(importHistoryIntoArchive(payload)); }); -rpc.on('import-history-prompt', () => { - showHistoryPasswordPrompt() +ipc.on('import-history-prompt', (e, payload) => { + showHistoryPasswordPrompt(payload) .then(result => { - rpc.emit('import-history-prompt-resp', result); + ipc.send('import-history-prompt-resp', result); }).catch(() => { - rpc.emit('import-history-prompt-resp', null); + ipc.send('import-history-prompt-resp', null); }); }); diff --git a/src/renderer/styles/entries.scss b/src/renderer/styles/entries.scss deleted file mode 100644 index 838f11af..00000000 --- a/src/renderer/styles/entries.scss +++ /dev/null @@ -1,20 +0,0 @@ -@import './variables'; - -.column { - background-color: #31353D; - color: #fff -} - -.mac { - background-color: rgba(#31353D, .8); -} - -.searchWrapper { - display: flex; - flex-direction: row; - margin-right: -$spacing-half; - - button { - color: #fff; - } -} diff --git a/src/renderer/styles/img/logos/floppy-square.png b/src/renderer/styles/img/logos/floppy-square.png new file mode 100644 index 00000000..82bc3967 Binary files /dev/null and b/src/renderer/styles/img/logos/floppy-square.png differ diff --git a/src/renderer/styles/img/logos/owncloud-square.svg b/src/renderer/styles/img/logos/owncloud-square.svg index be2fc8bf..3fe3a905 100644 --- a/src/renderer/styles/img/logos/owncloud-square.svg +++ b/src/renderer/styles/img/logos/owncloud-square.svg @@ -1,11 +1,11 @@ - + Slice Created with Sketch. - + diff --git a/src/renderer/styles/img/solo-logo.svg b/src/renderer/styles/img/solo-logo.svg new file mode 100644 index 00000000..02739393 --- /dev/null +++ b/src/renderer/styles/img/solo-logo.svg @@ -0,0 +1,14 @@ + + + + Combined Shape + Created with Sketch. + + + + + \ No newline at end of file diff --git a/src/renderer/styles/recent-files.scss b/src/renderer/styles/recent-files.scss deleted file mode 100644 index ffcfe9ad..00000000 --- a/src/renderer/styles/recent-files.scss +++ /dev/null @@ -1,76 +0,0 @@ -@import './variables'; -@import './common'; - -.container { - background-color: $white-60; - border-left: 1px solid $black-10; -} - -.emptyContainer { - @extend .container; - flex: 1; - display: flex; -} - -.content { - padding: $spacing-one; -} - -.heading { - @extend .heading; - margin-top: 1rem; -} - -.list { - list-style: none; - padding: 0; - margin: 0 0 $spacing-one; - - > li { - padding: $spacing-half 0; - position: relative; - border-bottom: 1px dotted $gray; - cursor: pointer !important; - } -} - -.fileInfo { - display: flex; - align-items: center; - - figure { - margin: 0; - padding: 0; - flex: 0 0 2rem; - width: 2rem; - height: 2rem; - display: flex; - justify-content: center; - align-items: center; - - img { - width: 100%; - display: block; - } - } - - section { - font-size: .9em; - flex: 1; - min-width: 0; - padding-left: $spacing-one; - - div { - width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - div:last-child { - font-weight: 300; - font-size: .8em; - color: $gray-dark; - } - } -} diff --git a/src/renderer/styles/tree-view.scss b/src/renderer/styles/tree-view.scss deleted file mode 100644 index 3b149bd4..00000000 --- a/src/renderer/styles/tree-view.scss +++ /dev/null @@ -1,10 +0,0 @@ -.column { - background-color: #292C33; - color: #fff; - padding-top: 1em; -} - -.mac { - padding-top: 3em; - background-color: rgba(#292C33, .8); -} diff --git a/src/renderer/styles/workspace.global.scss b/src/renderer/styles/workspace.global.scss index 29280533..d5697676 100644 --- a/src/renderer/styles/workspace.global.scss +++ b/src/renderer/styles/workspace.global.scss @@ -37,6 +37,7 @@ body { padding: 0; display: flex; height: 100%; + overflow: hidden; -webkit-app-region: drag; } @@ -48,7 +49,7 @@ body, button, input, select, textarea { box-sizing: border-box; } -#root, #root > div { +#root { display: flex; flex: 1; } @@ -75,6 +76,15 @@ input, button, textarea, :focus { --spacing-half: 6px; --spacing-one: 12px; --spacing-two: 20px; + --sidebar-width: 250px; + --sidebar-width-condenced: 75px; + --sidebar-bg: RGBA(33, 37, 43, .9); + --groups-bg: #292C33; + --groups-bg-mac: rgba(0,0,0,.2); + --entries-bg: #31353D; + /*--entries-bg-mac: rgba(49, 53, 61, 0.4);*/ + --entries-bg-mac: rgba(0, 0, 0, .35); + --red: #EB5767; --gray-light: #F5F7FA; --gray: #E4E9F2; --gray-dark: #777; diff --git a/src/renderer/system/buttercup/archive.js b/src/renderer/system/buttercup/archive.js deleted file mode 100644 index 05cc0080..00000000 --- a/src/renderer/system/buttercup/archive.js +++ /dev/null @@ -1,97 +0,0 @@ -import { Workspace, Archive, createCredentials, DatasourceAdapter } from 'buttercup-web'; -import './ipc-datasource'; - -let __currentWorkspace = null; - -function createWorkspace(datasource, passwordCredentials) { - const workspace = new Workspace(); - - return datasource - .load(passwordCredentials) - .then(archive => { - workspace.setPrimaryArchive( - archive, - datasource, - passwordCredentials - ); - return workspace; - }); -} - -function createDefaults(datasource, passwordCredentials) { - const archive = Archive.createWithDefaults(); - return datasource.save(archive, passwordCredentials); -} - -async function parseConfig(config, passwordCredentials) { - let { credentials, datasource, encryptedCredentials, isNew, ...rest } = config; - - if (typeof encryptedCredentials === 'string') { - credentials = await createCredentials.fromSecureString(encryptedCredentials, passwordCredentials); - datasource = credentials.getValueOrFail('datasource'); - } else { - datasource = { - type: config.type, - ...datasource - }; - encryptedCredentials = createCredentials(rest.type, credentials); - encryptedCredentials.setValue('datasource', datasource); - encryptedCredentials = await encryptedCredentials.toSecureString(passwordCredentials); - } - - return { - isNew, - credentials, - datasource, - config: { - ...rest, - encryptedCredentials - } - }; -} - -export async function loadWorkspace(masterConfig, masterPassword) { - const passwordCredentials = createCredentials.fromPassword(masterPassword); - const { isNew, credentials, datasource, config } = await parseConfig(masterConfig, masterPassword); - const dsInstance = DatasourceAdapter.objectToDatasource(datasource, credentials); - - if (isNew === true) { - await createDefaults(dsInstance, passwordCredentials); - } - - const workspace = await createWorkspace(dsInstance, passwordCredentials); - __currentWorkspace = { - instance: workspace, - ...config - }; - - return { - id: getArchive().getID(), - ...config - }; -} - -export function getWorkspace() { - if (__currentWorkspace === null) { - return null; - } - return __currentWorkspace; -} - -export function getArchive() { - return getWorkspace().instance.primary.archive; -} - -export function saveWorkspace() { - const workspace = getWorkspace().instance; - return workspace - .localDiffersFromRemote() - .then(differs => differs - ? workspace.mergeSaveablesFromRemote().then(() => true) - : false - ) - .then(shouldSave => shouldSave - ? workspace.save() - : null - ); -} diff --git a/src/renderer/system/buttercup/groups.js b/src/renderer/system/buttercup/groups.js deleted file mode 100644 index 61fb7b38..00000000 --- a/src/renderer/system/buttercup/groups.js +++ /dev/null @@ -1,135 +0,0 @@ -import { Group } from 'buttercup-web'; -import { saveWorkspace as save, getArchive } from './archive'; - -/** - * Return recursive groups JSON structure - * - * @export - * @returns {Object} JSON structure - */ -export function getGroups() { - const arch = getArchive(); - return arch.getGroups().map( - group => Object.assign(group.toObject(Group.OutputFlag.Groups), { - isTrash: group.isTrash() - }) - ); -} - -/** - * Create a new group under a parent group - * - * @export - * @param {string} parentId - * @param {string} groupName - */ -export function createGroup(parentId, groupName) { - const arch = getArchive(); - let group = null; - - if (parentId === null) { - group = arch; - } else { - group = arch.findGroupByID(parentId); - } - - if (!group) { - throw new Error('Group has not been found.'); - } - - group.createGroup(groupName); - - if (groupName.toLowerCase() !== 'untitled') { - save(); - } -} - -/** - * Delete a group - * - * @export - * @param {string} groupId - */ -export function deleteGroup(groupId) { - const arch = getArchive(); - const group = arch.findGroupByID(groupId); - - if (!group) { - throw new Error('Group has not been found.'); - } - - group.delete(); - save(); -} - -/** - * Save group title - * - * @export - * @param {string} groupId - * @param {string} title - */ -export function saveGroup(groupId, title) { - const arch = getArchive(); - const group = arch.findGroupByID(groupId); - - if (!group) { - throw new Error('Group has not been found.'); - } - - group.setTitle(title); - save(); -} - -/** - * Move group to another parent - * - * @export - * @param {string} groupId - * @param {string} parentId - */ -export function moveGroup(groupId, parentId, dropToGap = false) { - const arch = getArchive(); - const group = arch.findGroupByID(groupId); - let parent = parentId ? arch.findGroupByID(parentId) : arch; - - if (dropToGap) { - parent = findParentGroup(parentId, arch); - } - - if (!group || !parent) { - throw new Error('Group has not been found.'); - } - - group.moveToGroup(parent); - save(); -} - -/** - * Empty Trash Group - * @export - */ -export function emptyTrash() { - const arch = getArchive(); - arch.emptyTrash(); - save(); -} - -/** - * Find a Group's parent Group - * @param {String} groupId Group ID to comapre - * @param {Buttercup.Group} group Group Object - */ -function findParentGroup(groupId, group) { - const groups = group.getGroups(); - for (const subGroup of groups) { - if (subGroup.getID() === groupId) { - return group; - } - const findInChildren = findParentGroup(groupId, subGroup); - if (findInChildren !== false) { - return findInChildren; - } - } - return false; -} diff --git a/src/renderer/system/buttercup/import.js b/src/renderer/system/buttercup/import.js deleted file mode 100644 index 0b98eb99..00000000 --- a/src/renderer/system/buttercup/import.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Archive } from 'buttercup-web'; -import { showPasswordDialog } from '../dialog'; -import { getArchive, saveWorkspace } from './archive'; - -export function importHistoryFromRequest(request) { - const { history } = request; - const tempArchive = Archive.createFromHistory(history); - const mainArchive = getArchive(); - - tempArchive.getGroups().forEach(group => { - group.moveTo(mainArchive); - }); - - saveWorkspace(); -} - -export function showHistoryPasswordPrompt() { - return showPasswordDialog(null, { - title: 'KeePass Password', - confirmButtonText: 'Import KeePass Archive', - cancelButtonText: 'Cancel Import' - }); -} diff --git a/src/renderer/system/menu.js b/src/renderer/system/menu.js index 37e3d39d..adcaa2b4 100644 --- a/src/renderer/system/menu.js +++ b/src/renderer/system/menu.js @@ -13,12 +13,12 @@ export function createMenu(items = []) { }))); } -export function showContextMenu(menu = []) { +export function showContextMenu(menu = [], async = true) { if (Array.isArray(menu)) { menu = createMenu(menu); } menu.popup(currentWindow, { - async: true + async }); } diff --git a/src/renderer/system/rpc.js b/src/renderer/system/rpc.js deleted file mode 100644 index 3cfe5fa5..00000000 --- a/src/renderer/system/rpc.js +++ /dev/null @@ -1,60 +0,0 @@ -class Client { - constructor() { - const electron = window.require('electron'); - const EventEmitter = window.require('events'); - this.emitter = new EventEmitter(); - this.ipc = electron.ipcRenderer; - this.ipcListener = this.ipcListener.bind(this); - if (window.__rpcId) { - setTimeout(() => { - this.id = window.__rpcId; - this.ipc.on(this.id, this.ipcListener); - this.emitter.emit('ready'); - }, 0); - } else { - this.ipc.on('init', (ev, uid) => { - // we cache so that if the object - // gets re-instantiated we don't - // wait for a `init` event - window.__rpcId = uid; - this.id = uid; - this.ipc.on(uid, this.ipcListener); - this.emitter.emit('ready'); - }); - } - } - - ipcListener(event, {ch, data}) { - this.emitter.emit(ch, data); - } - - on(ev, fn) { - this.emitter.on(ev, fn); - } - - once(ev, fn) { - this.emitter.once(ev, fn); - } - - emit(ev, data) { - if (!this.id) { - throw new Error('Not ready'); - } - this.ipc.send(this.id, {ev, data}); - } - - removeListener(ev, fn) { - this.emitter.removeListener(ev, fn); - } - - removeAllListeners() { - this.emitter.removeAllListeners(); - } - - destroy() { - this.removeAllListeners(); - this.ipc.removeAllListeners(); - } -} - -export default new Client(); diff --git a/src/renderer/system/utils.js b/src/renderer/system/utils.js index b7271f1e..966444ef 100644 --- a/src/renderer/system/utils.js +++ b/src/renderer/system/utils.js @@ -24,10 +24,6 @@ export function selectElementContents(el) { sel.addRange(range); } -export function parsePath(filepath) { - return path.parse(filepath); -} - export function setWindowSize(width, height, vibrancy) { currentWindow.setSize(width, height, false); if (typeof vibrancy !== 'undefined') { @@ -35,26 +31,14 @@ export function setWindowSize(width, height, vibrancy) { } } -export function isOSX() { - return process.platform === 'darwin'; -} - -export function isWindows() { - return process.platform === 'win32'; -} - -export function isLinux() { - return process.platform === 'linux'; -} - export function isButtercupFile(filePath) { return path.extname(filePath).toLowerCase() === '.bcup'; } export function emitActionToParentAndClose(name, payload) { const win = remote.getCurrentWindow(); - const rpc = win.getParentWindow().rpc; - rpc.emit(name, payload); + const ipc = win.getParentWindow().webContents; + ipc.send(name, payload); win.close(); } diff --git a/src/shared/actions/archives.js b/src/shared/actions/archives.js index 7fe20e27..d496913f 100644 --- a/src/shared/actions/archives.js +++ b/src/shared/actions/archives.js @@ -1,48 +1,86 @@ -import path from 'path'; +// @ts-check + import isError from 'is-error'; +import { ipcRenderer as ipc } from 'electron'; import { createAction } from 'redux-actions'; -import { loadWorkspace } from '../../renderer/system/buttercup/archive'; import { ArchiveTypes } from '../buttercup/types'; +import { importHistory } from '../buttercup/import'; import { showPasswordDialog } from '../../renderer/system/dialog'; -import { setWindowSize } from '../../renderer/system/utils'; -import { getWindowSize } from '../selectors'; import { reloadGroups } from './groups'; -import { ARCHIVES_ADD, ARCHIVES_REMOVE, ARCHIVES_SET_CURRENT, ARCHIVES_CLEAR } from './types'; +import { + ARCHIVES_ADD, + ARCHIVES_REMOVE, + ARCHIVES_LOCK, + ARCHIVES_UNLOCK, + ARCHIVES_SET_CURRENT, + ARCHIVES_UPDATE +} from './types'; +import { getArchive, getCurrentArchiveId } from '../selectors'; +import { + addArchiveToArchiveManager, + removeArchiveFromArchiveManager, + unlockArchiveInArchiveManager +} from '../buttercup/archive'; -export const addArchive = createAction(ARCHIVES_ADD, payload => ({ - ...payload, - lastAccessed: (new Date()).getTime() -})); +// Store Actions +export const addArchiveToStore = createAction(ARCHIVES_ADD); +export const removeArchiveFromStore = createAction(ARCHIVES_REMOVE); +export const unlockArchiveInStore = createAction(ARCHIVES_UNLOCK); +export const lockArchiveInStore = createAction(ARCHIVES_LOCK); export const setCurrentArchive = createAction(ARCHIVES_SET_CURRENT); -export const removeArchive = createAction(ARCHIVES_REMOVE); -export const clearArchives = createAction(ARCHIVES_CLEAR); +export const updateArchive = createAction(ARCHIVES_UPDATE); + +// Impure Buttercup actions +export const loadArchive = payload => (dispatch, getState) => { + if (payload === getCurrentArchiveId(getState())) { + return; + } + dispatch(setCurrentArchive(payload)); + dispatch(reloadGroups()); + ipc.send('archive-list-updated'); +}; -export const loadArchive = payload => async (dispatch, getState) => { - try { - const archive = await showPasswordDialog( - password => loadWorkspace(payload, password).catch(err => { - const unknownMessage = 'An unknown error has occurred'; - return Promise.reject( - isError(err) - ? err.message || unknownMessage - : unknownMessage - ); - }) - ); +export const removeArchive = payload => () => { + return removeArchiveFromArchiveManager(payload); +}; + +export const unlockArchive = payload => dispatch => { + return showPasswordDialog( + password => unlockArchiveInArchiveManager(payload, password) + ).then( + archiveId => dispatch(loadArchive(archiveId)) + ); +}; - dispatch(setCurrentArchive(archive.id)); - dispatch(reloadGroups()); - dispatch(addArchive(archive)); +export const loadOrUnlockArchive = payload => (dispatch, getState) => { + const archive = getArchive(getState(), payload); + if (!archive) { + return; + } + if (archive.status === 'locked') { + dispatch(unlockArchive(payload)); + } else { + dispatch(loadArchive(payload)); + } +}; - // Changes to interface: - const [width, height] = getWindowSize(getState()); - setWindowSize(width, height, 'dark'); - window.document.title = `${path.basename(archive.path)} - Buttercup`; - } catch (err) { } +export const addArchive = payload => async (dispatch, getState) => { + return showPasswordDialog( + password => addArchiveToArchiveManager(payload, password).catch(err => { + const unknownMessage = 'An unknown error has occurred'; + return Promise.reject( + isError(err) + ? err.message || unknownMessage + : unknownMessage + ); + }) + ).then( + archiveId => dispatch(loadArchive(archiveId)) + ); }; -export const loadArchiveFromFile = ({ path, isNew = false }) => dispatch => { - dispatch(loadArchive({ +export const addArchiveFromFile = ({ path, isNew = false }) => dispatch => { + dispatch(addArchive({ type: ArchiveTypes.FILE, isNew, path, @@ -52,8 +90,8 @@ export const loadArchiveFromFile = ({ path, isNew = false }) => dispatch => { })); }; -export const loadArchiveFromWebdav = ({ path, endpoint, credentials, isNew = false }, type) => dispatch => { - dispatch(loadArchive({ +export const addArchiveFromWebdav = ({ path, endpoint, credentials, isNew = false }, type) => dispatch => { + dispatch(addArchive({ type, isNew, path, @@ -65,8 +103,8 @@ export const loadArchiveFromWebdav = ({ path, endpoint, credentials, isNew = fal })); }; -export const loadArchiveFromDropbox = ({ path, token, isNew = false }) => dispatch => { - dispatch(loadArchive({ +export const addArchiveFromDropbox = ({ path, token, isNew = false }) => dispatch => { + dispatch(addArchive({ type: ArchiveTypes.DROPBOX, isNew, path, @@ -77,21 +115,31 @@ export const loadArchiveFromDropbox = ({ path, token, isNew = false }) => dispat })); }; -export const loadArchiveFromSource = payload => dispatch => { +export const addArchiveFromSource = payload => dispatch => { const { type, ...config } = payload; switch (type) { case ArchiveTypes.DROPBOX: - dispatch(loadArchiveFromDropbox(config)); + dispatch(addArchiveFromDropbox(config)); break; case ArchiveTypes.OWNCLOUD: case ArchiveTypes.NEXTCLOUD: case ArchiveTypes.WEBDAV: - dispatch(loadArchiveFromWebdav(config, type)); + dispatch(addArchiveFromWebdav(config, type)); break; case ArchiveTypes.FILE: - dispatch(loadArchiveFromFile(config)); + dispatch(addArchiveFromFile(config)); break; default: break; } }; + +export const importHistoryIntoArchive = payload => (dispatch, getState) => { + const { archiveId, history } = payload; + importHistory(archiveId, history); + dispatch(reloadGroups()); +}; + +export const showImportDialog = payload => () => { + ipc.send('show-import-dialog', payload); +}; diff --git a/src/shared/actions/entries.js b/src/shared/actions/entries.js index 113d5256..a6c60cfa 100644 --- a/src/shared/actions/entries.js +++ b/src/shared/actions/entries.js @@ -1,7 +1,7 @@ -import * as entryTools from '../../renderer/system/buttercup/entries'; +import { createAction } from 'redux-actions'; +import * as entryTools from '../buttercup/entries'; import { showConfirmDialog } from '../../renderer/system/dialog'; -import { getCurrentGroupId } from '../selectors'; - +import { getCurrentGroupId, getCurrentArchiveId } from '../selectors'; import { ENTRIES_LOADED, ENTRIES_SELECTED, @@ -14,38 +14,40 @@ import { ENTRIES_SET_SORT, } from './types'; -export const loadEntries = groupId => ({ - type: ENTRIES_LOADED, - payload: entryTools.loadEntries(groupId) -}); - -export const selectEntry = entryId => ({ - type: ENTRIES_SELECTED, - payload: entryId -}); +export const selectEntry = createAction(ENTRIES_SELECTED); +export const setFilter = createAction(ENTRIES_SET_FILTER); +export const setSortMode = createAction(ENTRIES_SET_SORT); export const changeMode = mode => () => ({ type: ENTRIES_CHANGE_MODE, payload: mode }); -export const updateEntry = newValues => dispatch => { +export const loadEntries = (archiveId, groupId) => ({ + type: ENTRIES_LOADED, + payload: entryTools.loadEntries(archiveId, groupId) +}); + +export const updateEntry = newValues => (dispatch, getState) => { + const archiveId = getCurrentArchiveId(getState()); dispatch({ type: ENTRIES_UPDATE, payload: newValues }); - entryTools.updateEntry(newValues); + entryTools.updateEntry(archiveId, newValues); dispatch(changeMode('view')()); }; export const newEntry = newValues => (dispatch, getState) => { - const currentGroupId = getCurrentGroupId(getState()); + const state = getState(); + const currentGroupId = getCurrentGroupId(state); + const archiveId = getCurrentArchiveId(state); if (!currentGroupId) { return null; } - entryTools.createEntry(newValues, currentGroupId).then(entryObj => { + entryTools.createEntry(archiveId, currentGroupId, newValues).then(entryObj => { dispatch({ type: ENTRIES_CREATE, payload: entryObj @@ -56,7 +58,8 @@ export const newEntry = newValues => (dispatch, getState) => { }); }; -export const moveEntry = (entryId, groupId) => dispatch => { +export const moveEntry = (entryId, groupId) => (dispatch, getState) => { + const archiveId = getCurrentArchiveId(getState()); dispatch({ type: ENTRIES_MOVE, payload: { @@ -64,27 +67,18 @@ export const moveEntry = (entryId, groupId) => dispatch => { groupId } }); - entryTools.moveEntry(entryId, groupId); + entryTools.moveEntry(archiveId, entryId, groupId); }; -export const deleteEntry = entryId => dispatch => { +export const deleteEntry = entryId => (dispatch, getState) => { + const archiveId = getCurrentArchiveId(getState()); showConfirmDialog('Are you sure?', resp => { if (resp === 0) { dispatch({ type: ENTRIES_DELETE, payload: entryId }); - entryTools.deleteEntry(entryId); + entryTools.deleteEntry(archiveId, entryId); } }); }; - -export const setFilter = filter => ({ - type: ENTRIES_SET_FILTER, - payload: filter -}); - -export const setSortMode = sortKey => ({ - type: ENTRIES_SET_SORT, - payload: sortKey -}); diff --git a/src/shared/actions/files.js b/src/shared/actions/files.js index 138898ac..57e66dd2 100644 --- a/src/shared/actions/files.js +++ b/src/shared/actions/files.js @@ -1,11 +1,11 @@ import { ipcRenderer } from 'electron'; export const newArchive = () => () => { - window.rpc.emit('new-file-dialog'); + ipcRenderer.send('new-file-dialog'); }; export const openArchive = () => () => { - window.rpc.emit('open-file-dialog'); + ipcRenderer.send('open-file-dialog'); }; export const openFileManager = () => () => { diff --git a/src/shared/actions/groups.js b/src/shared/actions/groups.js index d36ec240..b877c02f 100644 --- a/src/shared/actions/groups.js +++ b/src/shared/actions/groups.js @@ -1,92 +1,140 @@ import { createAction } from 'redux-actions'; +import uuid from 'uuid'; import { showConfirmDialog } from '../../renderer/system/dialog'; -import * as groupTools from '../../renderer/system/buttercup/groups'; +import * as groupTools from '../buttercup/groups'; import { loadEntries } from './entries'; import { addExpandedKeys } from './ui'; - +import { + getCurrentArchiveId, + getDismissableGroupIds, + getGroupsById, + getTrashGroupId, + getTrashChildrenIds +} from '../selectors'; import { GROUPS_SELECTED, GROUPS_SET_SORT, GROUPS_RESET, - GROUPS_MOVE, GROUPS_RENAME, + GROUPS_MOVE, GROUPS_ADD_NEW_CHILD, GROUPS_DISMISS, + GROUPS_UPDATE, } from './types'; -export const resetGroups = createAction(GROUPS_RESET); +export const resetGroups = createAction(GROUPS_RESET, payload => groupTools.normalizeGroups(payload)); export const renameGroup = createAction(GROUPS_RENAME); -export const dismissNewGroup = createAction(GROUPS_DISMISS); export const setSortMode = createAction(GROUPS_SET_SORT); +export const dismissGroup = createAction(GROUPS_DISMISS); +export const updateGroup = createAction(GROUPS_UPDATE); +export const moveGroup = createAction(GROUPS_MOVE); +export const setCurrentGroup = createAction(GROUPS_SELECTED); +export const addNewGroup = createAction(GROUPS_ADD_NEW_CHILD); +export const addTemporaryGroup = createAction(GROUPS_ADD_NEW_CHILD, payload => ({ + parentId: payload, + group: { + id: uuid.v4(), + parentId: payload, + title: '', + isNew: true + } +})); -export function removeGroup(id) { - return dispatch => { - groupTools.deleteGroup(id); - dispatch(reloadGroups()); - }; -} - -export function addGroup(parentId) { - return dispatch => { - dispatch(addExpandedKeys(parentId)); - dispatch({ - type: GROUPS_ADD_NEW_CHILD, - payload: parentId - }); - }; -} - -export function saveGroup(isNew, groupId, title) { - return dispatch => { - if (isNew) { - groupTools.createGroup(groupId, title); - } else { - groupTools.saveGroup(groupId, title); - } - dispatch(reloadGroups()); - }; -} +export const dismissNewGroups = () => (dispatch, getState) => { + const ids = getDismissableGroupIds(getState()); + ids.forEach(id => dispatch(dismissGroup(id))); +}; + +export const removeGroup = groupId => (dispatch, getState) => { + const archiveId = getCurrentArchiveId(getState()); + const isInTrash = groupTools.isGroupInTrash(archiveId, groupId); -export function reloadGroups() { - return dispatch => { - const groups = groupTools.getGroups(); - dispatch(resetGroups(groups)); + groupTools.deleteGroup(archiveId, groupId); - if (groups.length > 0) { - dispatch(loadGroup(groups[0].id)); + // Delete + if (isInTrash) { + dispatch(dismissGroup(groupId)); + } else { + const state = getState(); + const allGroups = getGroupsById(state); + const fromParentId = groupTools.findParentId(allGroups, groupId); + const toParentId = getTrashGroupId(state); + + dispatch(moveGroup({ + fromParentId, + toParentId, + groupId + })); + } +}; + +// @todo: fix expanded keys +export const addGroup = parentId => dispatch => { + dispatch(addExpandedKeys(parentId)); + dispatch(addTemporaryGroup(parentId)); +}; + +export const createNewGroup = (parentId, temporaryGroupId, title) => (dispatch, getState) => { + const archiveId = getCurrentArchiveId(getState()); + const group = groupTools.createGroup(archiveId, parentId, title); + + dispatch(dismissNewGroups()); + dispatch(addNewGroup({ + parentId, + group + })); +}; + +export const saveGroupTitle = (groupId, title) => (dispatch, getState) => { + const archiveId = getCurrentArchiveId(getState()); + const group = groupTools.saveGroup(archiveId, groupId, title); + dispatch(updateGroup(group)); +}; + +export const reloadGroups = () => (dispatch, getState) => { + const archiveId = getCurrentArchiveId(getState()); + const groups = groupTools.getGroups(archiveId); + dispatch(resetGroups(groups)); + + if (groups.length > 0) { + dispatch(loadGroup(groups[0].id)); + } +}; + +export const moveGroupToParent = (groupId, parentId, gapDrop) => (dispatch, getState) => { + const state = getState(); + const archiveId = getCurrentArchiveId(state); + const allGroups = getGroupsById(state); + const fromParentId = groupTools.findParentId(allGroups, groupId); + const toParentId = gapDrop ? groupTools.findParentId(allGroups, parentId) : parentId; + + groupTools.moveGroup(archiveId, groupId, toParentId); + dispatch({ + type: GROUPS_MOVE, + payload: { + fromParentId, + toParentId, + groupId } - }; -} - -export function moveGroupToParent(groupId, parentId, dropToGap) { - return dispatch => { - groupTools.moveGroup(groupId, parentId, dropToGap); - dispatch(reloadGroups()); - dispatch({ - type: GROUPS_MOVE, - payload: { - parentId, - groupId - } - }); - }; -} - -export function loadGroup(groupId) { - return dispatch => { - dispatch({ - type: GROUPS_SELECTED, - payload: groupId - }); - dispatch(loadEntries(groupId)); - }; -} - -export const emptyTrash = () => dispatch => { + }); +}; + +export const loadGroup = groupId => (dispatch, getState) => { + const archiveId = getCurrentArchiveId(getState()); + dispatch(setCurrentGroup(groupId)); + dispatch(loadEntries(archiveId, groupId)); +}; + +// @todo: remove expanded key from trash +export const emptyTrash = () => (dispatch, getState) => { + const state = getState(); + const archiveId = getCurrentArchiveId(state); + const trashIds = getTrashChildrenIds(state); + showConfirmDialog('Are you sure you want to empty Trash?', resp => { if (resp === 0) { - groupTools.emptyTrash(); - dispatch(reloadGroups()); + trashIds.forEach(id => dispatch(dismissGroup(id))); + groupTools.emptyTrash(archiveId); } }); }; diff --git a/src/shared/actions/settings.js b/src/shared/actions/settings.js new file mode 100644 index 00000000..5076f984 --- /dev/null +++ b/src/shared/actions/settings.js @@ -0,0 +1,14 @@ +import { createAction } from 'redux-actions'; +import { + COLUMN_SIZE_SET, + SETTING_SET, +} from './types'; + +export const setColumnSize = createAction(COLUMN_SIZE_SET); +export const setSetting = (key, value) => ({ + type: SETTING_SET, + payload: { + key, + value + } +}); diff --git a/src/shared/actions/types.js b/src/shared/actions/types.js index b66828a6..4bb09968 100644 --- a/src/shared/actions/types.js +++ b/src/shared/actions/types.js @@ -14,18 +14,20 @@ export const FILES_NEW = 'buttercup/files/NEW'; export const FILES_CANCELLED = 'buttercup/files/CANCELLED'; export const GROUPS_SELECTED = 'buttercup/groups/SELECTED'; -export const GROUPS_ADD_NEW_CHILD = 'buttercup/groups/ADD_CHILD'; +export const GROUPS_ADD_NEW_CHILD = 'buttercup/groups/ADD_NEW_CHILD'; export const GROUPS_DISMISS = 'buttercup/groups/DISMISS'; export const GROUPS_MOVE = 'buttercup/groups/MOVE'; export const GROUPS_SET_SORT = 'buttercup/groups/SET_SORT'; export const GROUPS_RESET = 'buttercup/groups/RESET'; export const GROUPS_REMOVE = 'buttercup/groups/REMOVE'; export const GROUPS_RENAME = 'buttercup/groups/RENAME'; +export const GROUPS_UPDATE = 'buttercup/groups/UPDATE'; export const TREE_ADD_EXPANDED_KEY = 'buttercup/ui/TREE_ADD_EXPANDED_KEY'; export const TREE_SET_EXPANDED_KEYS = 'buttercup/ui/TREE_SET_EXPANDED_KEYS'; export const COLUMN_SIZE_SET = 'buttercup/ui/COLUMN_SIZE_SET'; -export const WINDOW_SIZE_SET = 'buttercup/ui/WINDOW_SIZE_SET'; + +export const SETTING_SET = 'buttercup/settings/SET'; export const SET_WORKSPACE = 'buttercup/workspace/SET'; @@ -34,5 +36,9 @@ export const UPDATE_INSTALL = 'buttercup/ui/UPDATE_INSTALL'; export const ARCHIVES_ADD = 'buttercup/archives/ADD'; export const ARCHIVES_REMOVE = 'buttercup/archives/REMOVE'; +export const ARCHIVES_LOCK = 'buttercup/archives/LOCK'; +export const ARCHIVES_UNLOCK = 'buttercup/archives/UNLOCK'; export const ARCHIVES_CLEAR = 'buttercup/archives/CLEAR'; +export const ARCHIVES_SET = 'buttercup/archives/SET'; export const ARCHIVES_SET_CURRENT = 'buttercup/archives/SET_CURRENT'; +export const ARCHIVES_UPDATE = 'buttercup/archives/UPDATE'; diff --git a/src/shared/actions/ui.js b/src/shared/actions/ui.js index cbc53ef5..9df8ce58 100644 --- a/src/shared/actions/ui.js +++ b/src/shared/actions/ui.js @@ -3,11 +3,7 @@ import { createLocalAction } from '../utils/redux'; import { TREE_ADD_EXPANDED_KEY, TREE_SET_EXPANDED_KEYS, - COLUMN_SIZE_SET, - WINDOW_SIZE_SET, } from './types'; export const setExpandedKeys = createLocalAction(TREE_SET_EXPANDED_KEYS); export const addExpandedKeys = createLocalAction(TREE_ADD_EXPANDED_KEY); -export const setColumnSize = createLocalAction(COLUMN_SIZE_SET); -export const setWindowSize = createLocalAction(WINDOW_SIZE_SET); diff --git a/src/shared/buttercup/archive.js b/src/shared/buttercup/archive.js new file mode 100644 index 00000000..89d3be4a --- /dev/null +++ b/src/shared/buttercup/archive.js @@ -0,0 +1,75 @@ +import path from 'path'; +import { ArchiveManager, createCredentials } from 'buttercup-web'; +import ElectronStorageInterface from './storage'; +import './ipc-datasource'; + +let __sharedManager = null; + +export function addArchiveToArchiveManager(masterConfig, masterPassword) { + const { credentials, datasource, type, path: filePath, isNew } = masterConfig; + + const passwordCredentials = createCredentials.fromPassword(masterPassword); + const sourceCredentials = createCredentials(type, credentials); + sourceCredentials.setValue('datasource', JSON.stringify({ + type, + ...datasource + })); + + const manager = getSharedArchiveManager(); + + return manager.addSource( + path.basename(filePath), + sourceCredentials, + passwordCredentials, + isNew + ); +} + +export function removeArchiveFromArchiveManager(archiveId) { + const manager = getSharedArchiveManager(); + return manager.remove(archiveId); +} + +export function unlockArchiveInArchiveManager(archiveId, masterPassword) { + const manager = getSharedArchiveManager(); + return manager + .unlock(archiveId, masterPassword) + .then(() => archiveId) + .catch(err => { + if (err.message && err.message.includes('ENOENT')) { + throw new Error('Archive source was not found.'); + } + throw err; + }); +} + +export function getSharedArchiveManager() { + if (__sharedManager === null) { + __sharedManager = new ArchiveManager(new ElectronStorageInterface()); + } + return __sharedManager; +} + +export function getArchive(archiveId) { + const manager = getSharedArchiveManager(); + const sourceIndex = manager.indexOfSource(archiveId); + const source = manager.sources[sourceIndex]; + return source.workspace.primary.archive; +} + +export function saveWorkspace(archiveId) { + const manager = getSharedArchiveManager(); + const sourceIndex = manager.indexOfSource(archiveId); + const { workspace } = manager.sources[sourceIndex]; + + return workspace + .localDiffersFromRemote() + .then(differs => differs + ? workspace.mergeSaveablesFromRemote().then(() => true) + : false + ) + .then(shouldSave => shouldSave + ? workspace.save() + : null + ); +} diff --git a/src/shared/buttercup/brands.js b/src/shared/buttercup/brands.js index 34a2bafa..9622145d 100644 --- a/src/shared/buttercup/brands.js +++ b/src/shared/buttercup/brands.js @@ -1,10 +1,8 @@ import dropboxLogo from '../../renderer/styles/img/logos/dropbox.svg'; -import dropboxLogoSquare from '../../renderer/styles/img/logos/dropbox-square.svg'; import ownCloud from '../../renderer/styles/img/logos/owncloud.png'; import ownCloudSquare from '../../renderer/styles/img/logos/owncloud-square.svg'; import nextCloud from '../../renderer/styles/img/logos/nextcloud.svg'; import nextCloudSquare from '../../renderer/styles/img/logos/nextcloud-square.svg'; -import fileSystemLogo from '../../renderer/styles/img/icons/disk-player.svg'; import webDAV from '../../renderer/styles/img/logos/webdav.png'; import { ArchiveTypes } from './types'; @@ -13,7 +11,7 @@ export const brands = { remote: true, name: 'Dropbox', logo: dropboxLogo, - icon: dropboxLogoSquare + icon: dropboxLogo }, [ArchiveTypes.OWNCLOUD]: { remote: true, @@ -37,6 +35,6 @@ export const brands = { remote: false, name: 'File System', logo: '', - icon: fileSystemLogo + icon: '' } }; diff --git a/src/renderer/system/buttercup/entries.js b/src/shared/buttercup/entries.js similarity index 79% rename from src/renderer/system/buttercup/entries.js rename to src/shared/buttercup/entries.js index 6c33c07f..8d0aef60 100644 --- a/src/renderer/system/buttercup/entries.js +++ b/src/shared/buttercup/entries.js @@ -1,4 +1,4 @@ -import { saveWorkspace as save, getArchive } from './archive'; +import { saveWorkspace, getArchive } from './archive'; function entryToObj(entry) { const obj = entry.toObject(); @@ -12,8 +12,8 @@ function entryToObj(entry) { }; } -export function loadEntries(groupId) { - const arch = getArchive(); +export function loadEntries(archiveId, groupId) { + const arch = getArchive(archiveId); const group = arch.findGroupByID(groupId); if (!group) { @@ -23,8 +23,8 @@ export function loadEntries(groupId) { return group.getEntries().map(entry => entryToObj(entry)); } -export function updateEntry(entryObj) { - const arch = getArchive(); +export function updateEntry(archiveId, entryObj) { + const arch = getArchive(archiveId); const entry = arch.getEntryByID(entryObj.id); if (!entry) { @@ -64,11 +64,11 @@ export function updateEntry(entryObj) { }); // Save workspace - save(); + saveWorkspace(archiveId); } -export function createEntry(newValues, groupId) { - const arch = getArchive(); +export function createEntry(archiveId, groupId, newValues) { + const arch = getArchive(archiveId); const group = arch.findGroupByID(groupId); if (!group) { @@ -87,13 +87,13 @@ export function createEntry(newValues, groupId) { entry.setMeta(meta.key, meta.value); }); - save(); + saveWorkspace(archiveId); return Promise.resolve(entryToObj(entry)); } -export function deleteEntry(entryId) { - const arch = getArchive(); +export function deleteEntry(archiveId, entryId) { + const arch = getArchive(archiveId); const entry = arch.getEntryByID(entryId); if (!entry) { @@ -101,11 +101,11 @@ export function deleteEntry(entryId) { } entry.delete(); - save(); + saveWorkspace(archiveId); } -export function moveEntry(entryId, groupId) { - const arch = getArchive(); +export function moveEntry(archiveId, entryId, groupId) { + const arch = getArchive(archiveId); const entry = arch.getEntryByID(entryId); const group = arch.findGroupByID(groupId); @@ -114,5 +114,5 @@ export function moveEntry(entryId, groupId) { } entry.moveToGroup(group); - save(); + saveWorkspace(archiveId); } diff --git a/src/shared/buttercup/groups.js b/src/shared/buttercup/groups.js new file mode 100644 index 00000000..4fc551b3 --- /dev/null +++ b/src/shared/buttercup/groups.js @@ -0,0 +1,111 @@ +import { Group } from 'buttercup-web'; +import { getArchive, saveWorkspace } from './archive'; +import { normalize, denormalize, schema } from 'normalizr'; + +const group = new schema.Entity('groups'); +const groups = new schema.Array(group); +group.define({ groups }); + +export function normalizeGroups(payload) { + return normalize(payload, groups); +} + +export function denormalizeGroups(shownIds, allIds) { + return denormalize(shownIds, groups, { groups: allIds }); +} + +export function groupToObject(group) { + const obj = group.toObject(); + return { + ...obj, + isTrash: group.isTrash(), + groups: obj.groups.map(g => g.id) + }; +} + +export function findParentId(groups, groupId) { + return Object.keys(groups).find(parentId => { + const group = groups[parentId]; + if (group.groups && group.groups.indexOf(groupId) !== -1) { + return true; + } + return false; + }); +} + +export function getGroups(archiveId) { + const arch = getArchive(archiveId); + return arch.getGroups().map( + group => Object.assign(group.toObject(Group.OutputFlag.Groups), { + isTrash: group.isTrash() + }) + ); +} + +export function createGroup(archiveId, parentId, groupName) { + const arch = getArchive(archiveId); + const group = parentId ? arch.findGroupByID(parentId) : arch; + + if (!group) { + throw new Error('Group has not been found.'); + } + + const newGroup = group.createGroup(groupName); + + if (groupName.toLowerCase() !== 'untitled') { + saveWorkspace(archiveId); + } + + return groupToObject(newGroup); +} + +export function deleteGroup(archiveId, groupId) { + const arch = getArchive(archiveId); + const group = arch.findGroupByID(groupId); + + if (!group) { + throw new Error('Group has not been found.'); + } + + group.delete(); + saveWorkspace(archiveId); +} + +export function saveGroup(archiveId, groupId, title) { + const arch = getArchive(archiveId); + const group = arch.findGroupByID(groupId); + + if (!group) { + throw new Error('Group has not been found.'); + } + + group.setTitle(title); + saveWorkspace(archiveId); + + return groupToObject(group); +} + +export function moveGroup(archiveId, groupId, parentId) { + const arch = getArchive(archiveId); + const group = arch.findGroupByID(groupId); + const parent = parentId ? arch.findGroupByID(parentId) : arch; + + if (!group || !parent) { + throw new Error('Group has not been found.'); + } + + group.moveToGroup(parent); + saveWorkspace(archiveId); +} + +export function isGroupInTrash(archiveId, groupId) { + const arch = getArchive(archiveId); + const group = arch.findGroupByID(groupId); + return group.isInTrash(); +} + +export function emptyTrash(archiveId) { + const arch = getArchive(archiveId); + arch.emptyTrash(); + saveWorkspace(archiveId); +} diff --git a/src/shared/buttercup/import.js b/src/shared/buttercup/import.js new file mode 100644 index 00000000..8ff20a10 --- /dev/null +++ b/src/shared/buttercup/import.js @@ -0,0 +1,24 @@ +import { Archive } from 'buttercup-web'; +import { showPasswordDialog } from '../../renderer/system/dialog'; +import { getArchive, saveWorkspace } from './archive'; +import { ImportTypeInfo } from './types'; + +export function importHistory(archiveId, history) { + const tempArchive = Archive.createFromHistory(history); + const mainArchive = getArchive(archiveId); + + tempArchive.getGroups().forEach(group => { + group.moveTo(mainArchive); + }); + + saveWorkspace(archiveId); +} + +export function showHistoryPasswordPrompt(type) { + const typeInfo = ImportTypeInfo[type]; + return showPasswordDialog(null, { + title: `${typeInfo.name} Archive Password`, + confirmButtonText: `Import ${typeInfo.name} Archive`, + cancelButtonText: 'Cancel Import' + }); +} diff --git a/src/renderer/system/buttercup/ipc-datasource.js b/src/shared/buttercup/ipc-datasource.js similarity index 83% rename from src/renderer/system/buttercup/ipc-datasource.js rename to src/shared/buttercup/ipc-datasource.js index b98f03cc..7b364cda 100644 --- a/src/renderer/system/buttercup/ipc-datasource.js +++ b/src/shared/buttercup/ipc-datasource.js @@ -1,4 +1,4 @@ -import { ipcRenderer as ipc } from 'electron'; +import fs from 'fs'; import { TextDatasource, DatasourceAdapter } from 'buttercup-web'; const registerDatasource = DatasourceAdapter.registerDatasource; @@ -15,7 +15,7 @@ export class IpcDatasource extends TextDatasource { load(password) { return Promise - .resolve(ipc.sendSync('read-archive', this.path)) + .resolve(fs.readFileSync(this.path, 'utf8')) .then(content => { this.setContent(content); return super.load(password); @@ -26,10 +26,7 @@ export class IpcDatasource extends TextDatasource { return super .save(archive, password) .then(encryptedContent => { - ipc.sendSync('write-archive', { - filename: this.path, - content: encryptedContent - }); + fs.writeFileSync(this.path, encryptedContent, 'utf8'); }); } diff --git a/src/shared/buttercup/storage.js b/src/shared/buttercup/storage.js new file mode 100644 index 00000000..186add36 --- /dev/null +++ b/src/shared/buttercup/storage.js @@ -0,0 +1,58 @@ +import Buttercup from 'buttercup-web'; +import Store from 'electron-store'; + +const { StorageInterface } = Buttercup.storage; +const storage = new Store({ + name: 'archives' +}); + +/** + * Interface for localStorage + * @augments StorageInterface + */ +export default class ElectronStorageInterface extends StorageInterface { + /** + * Get all keys from storage + * @returns {Promise.>} A promise that resolves with an array of keys + */ + getAllKeys() { + return new Promise((resolve, reject) => { + const values = [...storage]; + resolve(values.map(val => val[0])); + }); + } + + /** + * Get the value of a key + * @param {String} name The key name + * @returns {Promise.} A promise that resolves with the value + */ + getValue(name) { + return new Promise((resolve, reject) => { + resolve(storage.get(name)); + }); + } + + /** + * Remove a key from the storage + * @param {String} key The key to remove + * @returns {Promise} A promise that resolves once the item has been removed + */ + removeKey(key) { + return new Promise((resolve, reject) => { + resolve(storage.delete(key)); + }); + } + + /** + * Set the value for a key + * @param {String} name The key name + * @param {String} value The value to set + * @returns {Promise} A promise that resolves when the value is set + */ + setValue(name, value) { + return new Promise((resolve, reject) => { + resolve(storage.set(name, value)); + }); + } +} diff --git a/src/shared/buttercup/store.js b/src/shared/buttercup/store.js new file mode 100644 index 00000000..cecbee2a --- /dev/null +++ b/src/shared/buttercup/store.js @@ -0,0 +1,32 @@ +import { ipcRenderer as ipc } from 'electron'; +import { getSharedArchiveManager } from './archive'; +import { + addArchiveToStore, + removeArchiveFromStore, + lockArchiveInStore, + unlockArchiveInStore, +} from '../actions/archives.js'; + +export function linkArchiveManagerToStore(store) { + const archiveManager = getSharedArchiveManager(); + + // attach listeners + archiveManager.on('sourceAdded', function __handleNewSource(sourceInfo) { + store.dispatch(addArchiveToStore(sourceInfo)); + ipc.send('archive-list-updated'); + }); + archiveManager.on('sourceRehydrated', function __handleRehydratedSource(sourceInfo) { + store.dispatch(lockArchiveInStore(sourceInfo.id)); + ipc.send('archive-list-updated'); + }); + archiveManager.on('sourceUnlocked', function __handleUnlockedSource(sourceInfo) { + store.dispatch(unlockArchiveInStore(sourceInfo.id)); + }); + archiveManager.on('sourceRemoved', function __handleRemovedSource(sourceInfo) { + store.dispatch(removeArchiveFromStore(sourceInfo.id)); + ipc.send('archive-list-updated'); + }); + + // rehydrate + archiveManager.rehydrate(); +} diff --git a/src/shared/buttercup/types.js b/src/shared/buttercup/types.js index e455a1fc..c421494f 100644 --- a/src/shared/buttercup/types.js +++ b/src/shared/buttercup/types.js @@ -5,3 +5,27 @@ export const ArchiveTypes = { NEXTCLOUD: 'nextcloud', WEBDAV: 'webdav' }; + +export const ImportTypes = { + ONE_PASSWORD: '1password', + KEEPASS: 'keepass', + LASTPASS: 'lastpass' +}; + +export const ImportTypeInfo = { + [ImportTypes.ONE_PASSWORD]: { + password: false, + name: '1Password', + extension: '1pif' + }, + [ImportTypes.KEEPASS]: { + password: true, + name: 'KeePass', + extension: 'kdbx' + }, + [ImportTypes.LASTPASS]: { + password: false, + name: 'LastPass', + extension: 'csv' + } +}; diff --git a/src/shared/config.js b/src/shared/config.js new file mode 100644 index 00000000..ec7ecc7a --- /dev/null +++ b/src/shared/config.js @@ -0,0 +1,3 @@ +import Store from 'electron-store'; + +export const config = new Store(); diff --git a/src/shared/reducers/archives.js b/src/shared/reducers/archives.js index f49d0b9c..12e67675 100644 --- a/src/shared/reducers/archives.js +++ b/src/shared/reducers/archives.js @@ -1,20 +1,47 @@ import { createIdentityReducer } from '../utils/redux'; -import { ARCHIVES_ADD, ARCHIVES_REMOVE, ARCHIVES_SET_CURRENT, ARCHIVES_CLEAR } from '../actions/types'; +import { + ARCHIVES_ADD, + ARCHIVES_REMOVE, + ARCHIVES_LOCK, + ARCHIVES_UNLOCK, + ARCHIVES_SET_CURRENT, + ARCHIVES_UPDATE, +} from '../actions/types'; export default function archivesReducer(state = [], action) { switch (action.type) { case ARCHIVES_ADD: - return { + if (state.find(archive => archive.id === action.payload.id)) { + return state; + } + return [ ...state, - [action.payload.id]: action.payload - }; + action.payload + ]; case ARCHIVES_REMOVE: { - const nextState = {...state}; - delete nextState[action.payload]; - return nextState; + return state.filter(archive => archive.id !== action.payload); } - case ARCHIVES_CLEAR: - return []; + case ARCHIVES_LOCK: + case ARCHIVES_UNLOCK: + return state.map(archive => { + if (archive.id === action.payload) { + return { + ...archive, + status: action.type === ARCHIVES_LOCK ? 'locked' : 'unlocked' + }; + } + return archive; + }); + case ARCHIVES_UPDATE: + return state.map(archive => { + if (archive.id === action.payload.id) { + return { + ...archive, + ...action.payload + }; + } + return archive; + }); default: return state; } diff --git a/src/shared/reducers/groups.js b/src/shared/reducers/groups.js index f12dfb00..adb3734c 100644 --- a/src/shared/reducers/groups.js +++ b/src/shared/reducers/groups.js @@ -1,14 +1,13 @@ import { combineReducers } from 'redux'; -import { deepAdd, deepFilter, deepMap } from '../utils/collection'; - import { GROUPS_ADD_NEW_CHILD, GROUPS_DISMISS, GROUPS_SELECTED, GROUPS_SET_SORT, GROUPS_RESET, - GROUPS_REMOVE, - GROUPS_RENAME + GROUPS_RENAME, + GROUPS_MOVE, + GROUPS_UPDATE, } from '../actions/types'; // Reducers -> @@ -22,35 +21,125 @@ function currentGroup(state = null, action) { } } -function groups(state = [], action) { +function byId(state = {}, action) { switch (action.type) { - case GROUPS_ADD_NEW_CHILD: - return deepAdd(state, action.payload, 'groups', { - id: Math.random().toString(), - parentId: action.payload, - title: '', - isNew: true - }); + case GROUPS_ADD_NEW_CHILD: { + const { group, parentId } = action.payload; + let newState = {...state}; + if (parentId !== null) { + newState = { + ...newState, + [parentId]: { + ...newState[parentId], + groups: [ + ...newState[parentId].groups, + group.id + ] + } + }; + } + return { + ...newState, + [group.id]: group + }; + } case GROUPS_DISMISS: - const newState = deepFilter(state, 'groups', group => !group.isNew); - return deepMap(newState, 'groups', item => ({ - ...item, - isRenaming: false - })); + return Object.keys(state).reduce((newState, id) => { + if (id === action.payload) { + return newState; + } + newState[id] = { + ...state[id], + groups: state[id].groups.filter(groupId => groupId !== action.payload) + }; + return newState; + }, {}); + case GROUPS_MOVE: { + const { groupId, fromParentId, toParentId } = action.payload; + + if (toParentId === fromParentId) { + return state; + } + + let newState = {...state}; + + if (fromParentId) { + newState = { + ...newState, + [fromParentId]: { + ...newState[fromParentId], + groups: newState[fromParentId].groups.filter(id => id !== groupId) + } + }; + } + + if (toParentId) { + newState = { + ...newState, + [toParentId]: { + ...newState[toParentId], + groups: [ + ...newState[toParentId].groups, + groupId + ] + } + }; + } + + return newState; + } case GROUPS_RESET: - return action.payload; - case GROUPS_REMOVE: - return []; + return action.payload.entities.groups; case GROUPS_RENAME: - return deepMap(state, 'groups', item => { - if (item.id === action.payload) { - return { - ...item, - isRenaming: true - }; + return { + ...state, + [action.payload]: { + ...state[action.payload], + isRenaming: true } - return item; - }); + }; + case GROUPS_UPDATE: + return { + ...state, + [action.payload.id]: action.payload + }; + default: + return state; + } +} + +function shownIds(state = [], action) { + switch (action.type) { + case GROUPS_ADD_NEW_CHILD: + if (!action.payload.parentId) { + return [ + ...state, + action.payload.group.id + ]; + } + return state; + case GROUPS_MOVE: { + const { groupId, fromParentId, toParentId } = action.payload; + + if (toParentId === fromParentId) { + return state; + } + + if (!fromParentId) { + return state.filter(id => id !== groupId); + } + if (!toParentId) { + return [ + ...state, + groupId + ]; + } + return state; + } + case GROUPS_DISMISS: + return state.filter(id => id !== action.payload); + case GROUPS_RESET: + return action.payload.result; default: return state; } @@ -66,7 +155,8 @@ function sortMode(state = 'title-asc', action) { } export default combineReducers({ - byId: groups, + byId, + shownIds, currentGroup, sortMode }); diff --git a/src/shared/reducers/index.js b/src/shared/reducers/index.js index 444818a4..05064140 100644 --- a/src/shared/reducers/index.js +++ b/src/shared/reducers/index.js @@ -2,21 +2,22 @@ import { combineReducers } from 'redux'; import { reducer as form } from 'redux-form'; import groups from './groups'; import entries from './entries'; -import settingsByArchiveId from './settings'; +import { settings, settingsByArchiveId } from './settings'; import archives, { currentArchive } from './archives'; import update from './update'; export default function getRootReducer(scope = 'main') { let reducers = { settingsByArchiveId, + currentArchive, archives, update, + settings, }; if (scope === 'renderer') { reducers = { ...reducers, - currentArchive, groups, entries, form, diff --git a/src/shared/reducers/settings.js b/src/shared/reducers/settings.js index ef2646f1..958a43e4 100644 --- a/src/shared/reducers/settings.js +++ b/src/shared/reducers/settings.js @@ -1,4 +1,5 @@ import uiReducer from './ui'; +import { ARCHIVES_REMOVE, COLUMN_SIZE_SET, SETTING_SET } from '../actions/types'; function itemReducer(state = {}, action) { return { @@ -6,7 +7,7 @@ function itemReducer(state = {}, action) { }; } -export default function archivesReducer(state = {}, action) { +export function settingsByArchiveId(state = {}, action) { const archiveId = action.meta && action.meta.archiveId; if (archiveId) { return { @@ -14,5 +15,35 @@ export default function archivesReducer(state = {}, action) { [archiveId]: itemReducer(state[archiveId], action) }; } + if (action.type === ARCHIVES_REMOVE && state[action.payload]) { + const newState = {...state}; + delete newState[action.payload]; + return newState; + } return state; } + +const DEFAULT_SETTINGS = { + columnSizes: { tree: 230, entries: 230 }, + condencedSidebar: true +}; + +export function settings(state = DEFAULT_SETTINGS, action) { + switch (action.type) { + case COLUMN_SIZE_SET: + return { + ...state, + columnSizes: { + ...state.columnSizes, + [action.payload.name]: action.payload.size + } + }; + case SETTING_SET: + return { + ...state, + [action.payload.key]: action.payload.value + }; + default: + return state; + } +} diff --git a/src/shared/reducers/ui.js b/src/shared/reducers/ui.js index 8264cca8..bc2d1fe4 100644 --- a/src/shared/reducers/ui.js +++ b/src/shared/reducers/ui.js @@ -1,10 +1,7 @@ import { combineReducers } from 'redux'; -import { createIdentityReducer } from '../utils/redux'; import { TREE_ADD_EXPANDED_KEY, TREE_SET_EXPANDED_KEYS, - COLUMN_SIZE_SET, - WINDOW_SIZE_SET, } from '../actions/types'; function treeExpandedKeys(state = [], action) { @@ -21,28 +18,6 @@ function treeExpandedKeys(state = [], action) { } } -function columnSizes(state = {tree: 230, entries: 230}, action) { - if (!action.payload || !action.payload.name || !action.payload.size) { - return state; - } - switch (action.type) { - case COLUMN_SIZE_SET: - return { - ...state, - [action.payload.name]: action.payload.size - }; - default: - return state; - } -} - -export const windowSize = createIdentityReducer( - WINDOW_SIZE_SET, - [950, 700] -); - export default combineReducers({ treeExpandedKeys, - columnSizes, - windowSize }); diff --git a/src/shared/selectors/index.js b/src/shared/selectors.js similarity index 59% rename from src/shared/selectors/index.js rename to src/shared/selectors.js index 72e34dc7..e511516c 100644 --- a/src/shared/selectors/index.js +++ b/src/shared/selectors.js @@ -2,24 +2,20 @@ import { createSelector } from 'reselect'; import { filterByText, sortByKey, - sortDeepByKey, - sortByLastAccessed, - deepFindById -} from '../utils/collection'; + sortDeepByKey +} from './utils/collection'; +import { denormalizeGroups } from './buttercup/groups'; // Archive -> -export const getAllArchives = state => Object.values(state.archives); -export const getSortedArchives = createSelector( - getAllArchives, - archives => sortByLastAccessed(archives) -); - +export const getArchivesCount = state => state.archives.length; +export const getAllArchives = state => sortByKey(Object.values(state.archives), 'name-asc'); +export const getArchive = (state, archiveId) => state.archives.find(archive => archive.id === archiveId); export const getCurrentArchiveId = state => state.currentArchive; export const getCurrentArchive = createSelector( state => state.archives, getCurrentArchiveId, - (archives, archiveId) => archives[archiveId] || null + (archives, archiveId) => archives.find(archive => archive.id === archiveId) || null ); // Settings -> @@ -31,23 +27,13 @@ export const getCurrentArchiveSettings = createSelector( (settings, archiveId) => settings[archiveId] ); -// UI -> +export const getSetting = (state, key) => state.settings[key]; export const getExpandedKeys = createSelector( getCurrentArchiveSettings, archive => archive ? archive.ui.treeExpandedKeys : [] ); -export const getColumnSizes = createSelector( - getCurrentArchiveSettings, - archive => archive ? archive.ui.columnSizes : null -); - -export const getWindowSize = createSelector( - getCurrentArchiveSettings, - archive => archive ? archive.ui.windowSize : [950, 700] -); - // Entries -> export const getAllEntries = state => state.entries.byId; @@ -80,13 +66,21 @@ export const getEntries = createSelector( // Groups -> -export const getAllGroups = state => state.groups.byId; +export const getAllGroups = state => denormalizeGroups(state.groups.shownIds, state.groups.byId); +export const getDismissableGroupIds = state => Object.keys(state.groups.byId) + .filter(groupId => state.groups.byId[groupId].isNew); +export const getGroupsById = state => state.groups.byId; export const getCurrentGroupId = state => state.groups.currentGroup; - -export const getCurrentGroup = createSelector( - getAllGroups, - getCurrentGroupId, - (groups, groupId) => deepFindById(groups, groupId, 'groups') +export const getCurrentGroup = state => state.groups.currentGroup + ? state.groups.byId[state.groups.currentGroup] + : null; +export const getTrashGroupId = state => Object.keys(state.groups.byId) + .find(groupId => state.groups.byId[groupId].isTrash); + +export const getTrashChildrenIds = createSelector( + getGroupsById, + getTrashGroupId, + (groups, trashGroup) => groups[trashGroup].groups ); export const getGroups = createSelector( diff --git a/src/shared/store/configure-store.js b/src/shared/store/configure-store.js index b2c7fb97..7b219a22 100644 --- a/src/shared/store/configure-store.js +++ b/src/shared/store/configure-store.js @@ -33,6 +33,7 @@ export default function configureStore(initialState, scope = 'main') { settingsByArchiveId: true, archives: true, update: true, + settings: true }; if (scope === 'renderer') { diff --git a/src/shared/utils/collection.js b/src/shared/utils/collection.js index 02845c4b..08594fa7 100644 --- a/src/shared/utils/collection.js +++ b/src/shared/utils/collection.js @@ -36,55 +36,3 @@ export function sortDeepByKey(list, sortKey, childrenKey) { }; }); } - -export function deepFilter(list, key, fn) { - return list - .filter(fn) - .map(item => ({ - ...item, - [key]: deepFilter(item[key], key, fn) - })); -} - -export function deepMap(list, key, fn) { - return list - .map(fn) - .map(item => ({ - ...item, - [key]: deepMap(item[key], key, fn) - })); -} - -export function deepAdd(list, id, key, newItem) { - if (id === null) { - return [ - newItem, - ...list - ]; - } - return list.map(item => { - return { - ...item, - [key]: (item.id === id) - ? [newItem, ...item[key]] - : deepAdd(item[key], id, key, newItem) - }; - }); -} - -export function deepFindById(list, id, key) { - if (!Array.isArray(list)) { - return null; - } - - for (const item of list) { - if (item.id === id) { - return item; - } - const resultInChild = deepFindById(item[key], id, key); - if (resultInChild !== null) { - return resultInChild; - } - } - return null; -} diff --git a/src/main/lib/platform.js b/src/shared/utils/platform.js similarity index 100% rename from src/main/lib/platform.js rename to src/shared/utils/platform.js