diff --git a/.eslintrc b/.eslintrc
index 3eed71dc..aa750a50 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -16,7 +16,8 @@
"no-multiple-empty-lines": [1, {"max": 2}],
"comma-dangle": [2, "always-multiline"],
"standard/no-callback-literal": "off",
- "prefer-const": "off"
+ "prefer-const": "off",
+ "no-labels": "off"
},
"settings": {
"html/html-extensions": [".html", ".vue"]
diff --git a/package-lock.json b/package-lock.json
index d0ea2fff..01d4f1c7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3093,6 +3093,21 @@
"integrity": "sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow==",
"dev": true
},
+ "@types/component-emitter": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
+ "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg=="
+ },
+ "@types/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
+ },
+ "@types/cors": {
+ "version": "2.8.12",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
+ "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
+ },
"@types/debug": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
@@ -3171,8 +3186,7 @@
"@types/node": {
"version": "13.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.0.tgz",
- "integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ==",
- "dev": true
+ "integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ=="
},
"@types/parse-json": {
"version": "4.0.0",
@@ -3441,7 +3455,6 @@
"version": "1.3.7",
"resolved": "https://registry.npm.taobao.org/accepts/download/accepts-1.3.7.tgz",
"integrity": "sha1-UxvHJlF6OytB+FACHGzBXqq1B80=",
- "dev": true,
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
@@ -3594,6 +3607,11 @@
"color-convert": "^1.9.0"
}
},
+ "any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
+ },
"app-builder-bin": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-3.5.13.tgz",
@@ -4343,6 +4361,11 @@
"pascalcase": "^0.1.1"
}
},
+ "base64-arraybuffer": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
+ "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
+ },
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -4350,6 +4373,11 @@
"dev": true,
"optional": true
},
+ "base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
+ },
"batch": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -4474,9 +4502,7 @@
"boolean": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.1.2.tgz",
- "integrity": "sha512-YN6UmV0FfLlBVvRvNPx3pz5W/mUoYB24J4WSXOKP/OOJpi+Oq6WYqPaNTHzjI0QzwWtnvEd5CGYyQPgp1jFxnw==",
- "dev": true,
- "optional": true
+ "integrity": "sha512-YN6UmV0FfLlBVvRvNPx3pz5W/mUoYB24J4WSXOKP/OOJpi+Oq6WYqPaNTHzjI0QzwWtnvEd5CGYyQPgp1jFxnw=="
},
"boxen": {
"version": "5.0.1",
@@ -4583,6 +4609,14 @@
"integrity": "sha1-Uvq8xqYG0aADAoAmSO9o9jnaJow=",
"dev": true
},
+ "bufferutil": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz",
+ "integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==",
+ "requires": {
+ "node-gyp-build": "^4.2.0"
+ }
+ },
"builder-util": {
"version": "22.11.7",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-22.11.7.tgz",
@@ -4726,6 +4760,15 @@
"unset-value": "^1.0.0"
}
},
+ "cache-content-type": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz",
+ "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==",
+ "requires": {
+ "mime-types": "^2.1.18",
+ "ylru": "^1.2.0"
+ }
+ },
"cacheable-request": {
"version": "6.1.0",
"resolved": "https://registry.npm.taobao.org/cacheable-request/download/cacheable-request-6.1.0.tgz",
@@ -5136,6 +5179,11 @@
"mimic-response": "^1.0.0"
}
},
+ "co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
+ },
"collection-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@@ -5202,8 +5250,7 @@
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
- "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
- "dev": true
+ "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"compressible": {
"version": "2.0.18",
@@ -5388,7 +5435,6 @@
"version": "0.5.3",
"resolved": "https://registry.npm.taobao.org/content-disposition/download/content-disposition-0.5.3.tgz",
"integrity": "sha1-4TDK9+cnkIfFYWwgB9BIVpiYT70=",
- "dev": true,
"requires": {
"safe-buffer": "5.1.2"
},
@@ -5396,16 +5442,14 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.1.2.tgz",
- "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=",
- "dev": true
+ "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0="
}
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npm.taobao.org/content-type/download/content-type-1.0.4.tgz",
- "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=",
- "dev": true
+ "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js="
},
"convert-source-map": {
"version": "1.8.0",
@@ -5436,6 +5480,22 @@
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
"dev": true
},
+ "cookies": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz",
+ "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==",
+ "requires": {
+ "depd": "~2.0.0",
+ "keygrip": "~1.1.0"
+ },
+ "dependencies": {
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+ }
+ }
+ },
"copy-anything": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.3.tgz",
@@ -5610,6 +5670,15 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
+ "cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "requires": {
+ "object-assign": "^4",
+ "vary": "^1"
+ }
+ },
"cosmiconfig": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz",
@@ -6010,6 +6079,11 @@
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
"dev": true
},
+ "deepmerge": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
+ "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
+ },
"default-gateway": {
"version": "4.2.0",
"resolved": "https://registry.npm.taobao.org/default-gateway/download/default-gateway-4.2.0.tgz?cache=0&sync_timestamp=1598471816842&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdefault-gateway%2Fdownload%2Fdefault-gateway-4.2.0.tgz",
@@ -6030,7 +6104,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
- "dev": true,
"requires": {
"object-keys": "^1.0.12"
}
@@ -6074,22 +6147,30 @@
}
}
},
+ "delay": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
+ "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw=="
+ },
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
+ "delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
+ },
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
- "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
- "dev": true
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
- "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
- "dev": true
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"detect-node": {
"version": "2.0.4",
@@ -6368,8 +6449,12 @@
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
- "dev": true
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "eiows": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/eiows/-/eiows-3.6.0.tgz",
+ "integrity": "sha512-B2A6GXQ153/XVwecF2mkMNa108TJWGBXTBfHSmW2hf86D26cp3+lAhwizgEsV1mhcuZYgNQNeddgh4J5kYi7ng=="
},
"ejs": {
"version": "3.1.6",
@@ -6708,8 +6793,7 @@
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
- "dev": true
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"end-of-stream": {
"version": "1.4.4",
@@ -6720,6 +6804,48 @@
"once": "^1.4.0"
}
},
+ "engine.io": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.1.1.tgz",
+ "integrity": "sha512-aMWot7H5aC8L4/T8qMYbLdvKlZOdJTH54FxfdFunTGvhMx1BHkJOntWArsVfgAZVwAO9LC2sryPWRcEeUzCe5w==",
+ "requires": {
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "~0.4.1",
+ "cors": "~2.8.5",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~4.0.0",
+ "ws": "~7.4.2"
+ },
+ "dependencies": {
+ "cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
+ },
+ "debug": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
+ "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "ws": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
+ "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="
+ }
+ }
+ },
+ "engine.io-parser": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz",
+ "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==",
+ "requires": {
+ "base64-arraybuffer": "0.1.4"
+ }
+ },
"enhanced-resolve": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz",
@@ -6884,8 +7010,7 @@
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
- "dev": true
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"escape-string-regexp": {
"version": "1.0.5",
@@ -8031,12 +8156,31 @@
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
+ "fast-json-stringify": {
+ "version": "2.7.7",
+ "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-2.7.7.tgz",
+ "integrity": "sha512-2kiwC/hBlK7QiGALsvj0QxtYwaReLOmAwOWJIxt5WHBB9EwXsqbsu8LCel47yh8NV8CEcFmnZYcXh4BionJcwQ==",
+ "requires": {
+ "ajv": "^6.11.0",
+ "deepmerge": "^4.2.2",
+ "rfdc": "^1.2.0",
+ "string-similarity": "^4.0.1"
+ }
+ },
"fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
+ "fast-printf": {
+ "version": "1.6.6",
+ "resolved": "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.6.tgz",
+ "integrity": "sha512-Uz/uW6R1Fd8YqCGeoQosRIfB4dBbr8uMbFVdEci2AyXYcfucFqhpSMAGs8skRRdZd+MGCDBu48+B8Zmu7Pta5A==",
+ "requires": {
+ "boolean": "^3.0.2"
+ }
+ },
"fastest-levenshtein": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz",
@@ -8257,8 +8401,7 @@
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
- "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
- "dev": true
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"friendly-errors-webpack-plugin": {
"version": "1.7.0",
@@ -8497,8 +8640,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz",
"integrity": "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==",
- "dev": true,
- "optional": true,
"requires": {
"define-properties": "^1.1.3"
}
@@ -8760,6 +8901,22 @@
"entities": "^2.0.0"
}
},
+ "http-assert": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz",
+ "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==",
+ "requires": {
+ "deep-equal": "~1.0.1",
+ "http-errors": "~1.7.2"
+ },
+ "dependencies": {
+ "deep-equal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
+ "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
+ }
+ }
+ },
"http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npm.taobao.org/http-cache-semantics/download/http-cache-semantics-4.1.0.tgz",
@@ -8776,7 +8933,6 @@
"version": "1.7.2",
"resolved": "https://registry.npm.taobao.org/http-errors/download/http-errors-1.7.2.tgz?cache=0&sync_timestamp=1593407647372&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhttp-errors%2Fdownload%2Fhttp-errors-1.7.2.tgz",
"integrity": "sha1-T1ApzxMjnzEDblsuVSkrz7zIXI8=",
- "dev": true,
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
@@ -8788,8 +8944,7 @@
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
- "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
- "dev": true
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
}
}
},
@@ -8927,6 +9082,43 @@
"sshpk": "^1.7.0"
}
},
+ "http-terminator": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/http-terminator/-/http-terminator-3.0.0.tgz",
+ "integrity": "sha512-YdNsDQgsHuxBSOWWhkQHMgOD7c5CU3e9u+pokp9tI6BwJ8LjUhJYBO+k2a3NXoDXbToSm7OQk4RzNTooPQP5IQ==",
+ "requires": {
+ "delay": "^5.0.0",
+ "roarr": "^4.0.10",
+ "type-fest": "^0.20.2"
+ },
+ "dependencies": {
+ "detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
+ },
+ "roarr": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/roarr/-/roarr-4.2.5.tgz",
+ "integrity": "sha512-ZSs1hr2gyWickWDr2Yw0qcuef+EJKwZtNxUj7poxvIDxVq+ZvQreVNdPVLHonWpavBeZaOcAGVFV5xM/HqRR8g==",
+ "requires": {
+ "boolean": "^3.0.2",
+ "detect-node": "^2.0.5",
+ "fast-json-stringify": "^2.5.2",
+ "fast-printf": "^1.6.4",
+ "globalthis": "^1.0.2",
+ "is-circular": "^1.0.2",
+ "json-stringify-safe": "^5.0.1",
+ "semver-compare": "^1.0.0"
+ }
+ },
+ "type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="
+ }
+ }
+ },
"human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -9150,6 +9342,11 @@
"ci-info": "^3.1.1"
}
},
+ "is-circular": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-circular/-/is-circular-1.0.2.tgz",
+ "integrity": "sha512-YttjnrswnUYRVJvxCvu8z+PGMUSzC2JttP0OEXezlAEdp3EXzhf7IZ3j0gRAybJBQupedIZFhY61Tga6E0qASA=="
+ },
"is-color-stop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz",
@@ -9235,6 +9432,11 @@
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
+ "is-generator-function": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.9.tgz",
+ "integrity": "sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A=="
+ },
"is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
@@ -9651,6 +9853,14 @@
"integrity": "sha1-iBkexzjOn3WRwl6QVt6Si0AncZQ=",
"dev": true
},
+ "keygrip": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
+ "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==",
+ "requires": {
+ "tsscmp": "1.0.6"
+ }
+ },
"keyv": {
"version": "3.1.0",
"resolved": "https://registry.npm.taobao.org/keyv/download/keyv-3.1.0.tgz?cache=0&sync_timestamp=1600337463601&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fkeyv%2Fdownload%2Fkeyv-3.1.0.tgz",
@@ -9678,6 +9888,80 @@
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==",
"dev": true
},
+ "koa": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.1.tgz",
+ "integrity": "sha512-Lb2Dloc72auj5vK4X4qqL7B5jyDPQaZucc9sR/71byg7ryoD1NCaCm63CShk9ID9quQvDEi1bGR/iGjCG7As3w==",
+ "requires": {
+ "accepts": "^1.3.5",
+ "cache-content-type": "^1.0.0",
+ "content-disposition": "~0.5.2",
+ "content-type": "^1.0.4",
+ "cookies": "~0.8.0",
+ "debug": "~3.1.0",
+ "delegates": "^1.0.0",
+ "depd": "^2.0.0",
+ "destroy": "^1.0.4",
+ "encodeurl": "^1.0.2",
+ "escape-html": "^1.0.3",
+ "fresh": "~0.5.2",
+ "http-assert": "^1.3.0",
+ "http-errors": "^1.6.3",
+ "is-generator-function": "^1.0.7",
+ "koa-compose": "^4.1.0",
+ "koa-convert": "^1.2.0",
+ "on-finished": "^2.3.0",
+ "only": "~0.0.2",
+ "parseurl": "^1.3.2",
+ "statuses": "^1.5.0",
+ "type-is": "^1.6.16",
+ "vary": "^1.1.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ }
+ }
+ },
+ "koa-compose": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz",
+ "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw=="
+ },
+ "koa-convert": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz",
+ "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=",
+ "requires": {
+ "co": "^4.6.0",
+ "koa-compose": "^3.0.0"
+ },
+ "dependencies": {
+ "koa-compose": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz",
+ "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=",
+ "requires": {
+ "any-promise": "^1.1.0"
+ }
+ }
+ }
+ },
"latest-version": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
@@ -9946,6 +10230,20 @@
"integrity": "sha1-AF/eL15uRwaPk1/yhXPhJe9y8Zc=",
"dev": true
},
+ "long": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
+ },
+ "loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dev": true,
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ },
"lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -10067,8 +10365,7 @@
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
- "dev": true
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"memory-fs": {
"version": "0.4.1",
@@ -10360,8 +10657,7 @@
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npm.taobao.org/negotiator/download/negotiator-0.6.2.tgz",
- "integrity": "sha1-/qz3zPUlp3rpY0Q2pkiD/+yjRvs=",
- "dev": true
+ "integrity": "sha1-/qz3zPUlp3rpY0Q2pkiD/+yjRvs="
},
"neo-async": {
"version": "2.6.2",
@@ -10406,6 +10702,11 @@
"integrity": "sha1-Mt6ir7Ppkm8C7lzoeUkCaRpna/M=",
"dev": true
},
+ "node-gyp-build": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
+ "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg=="
+ },
"node-id3": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.2.3.tgz",
@@ -10513,8 +10814,7 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
- "dev": true
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-copy": {
"version": "0.1.0",
@@ -10609,8 +10909,7 @@
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
- "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
- "dev": true
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
},
"object-visit": {
"version": "1.0.1",
@@ -10663,7 +10962,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
- "dev": true,
"requires": {
"ee-first": "1.1.1"
}
@@ -10692,6 +10990,11 @@
"mimic-fn": "^2.1.0"
}
},
+ "only": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz",
+ "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q="
+ },
"opn": {
"version": "5.5.0",
"resolved": "https://registry.npm.taobao.org/opn/download/opn-5.5.0.tgz",
@@ -10839,8 +11142,7 @@
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npm.taobao.org/parseurl/download/parseurl-1.3.3.tgz",
- "integrity": "sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ=",
- "dev": true
+ "integrity": "sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ="
},
"pascal-case": {
"version": "3.1.2",
@@ -12257,6 +12559,11 @@
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true
},
+ "rfdc": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
+ "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA=="
+ },
"rgb-regex": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz",
@@ -12366,9 +12673,7 @@
"semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
- "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
- "dev": true,
- "optional": true
+ "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w="
},
"semver-diff": {
"version": "3.1.1",
@@ -12559,8 +12864,7 @@
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npm.taobao.org/setprototypeof/download/setprototypeof-1.1.1.tgz",
- "integrity": "sha1-fpWsskqpL1iF4KvvW6ExMw1K5oM=",
- "dev": true
+ "integrity": "sha1-fpWsskqpL1iF4KvvW6ExMw1K5oM="
},
"shallow-clone": {
"version": "3.0.1",
@@ -12781,6 +13085,57 @@
}
}
},
+ "socket.io": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.1.3.tgz",
+ "integrity": "sha512-tLkaY13RcO4nIRh1K2hT5iuotfTaIQw7cVIe0FUykN3SuQi0cm7ALxuyT5/CtDswOMWUzMGTibxYNx/gU7In+Q==",
+ "requires": {
+ "@types/cookie": "^0.4.0",
+ "@types/cors": "^2.8.10",
+ "@types/node": ">=10.0.0",
+ "accepts": "~1.3.4",
+ "base64id": "~2.0.0",
+ "debug": "~4.3.1",
+ "engine.io": "~5.1.1",
+ "socket.io-adapter": "~2.3.1",
+ "socket.io-parser": "~4.0.4"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
+ "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ }
+ }
+ },
+ "socket.io-adapter": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.1.tgz",
+ "integrity": "sha512-8cVkRxI8Nt2wadkY6u60Y4rpW3ejA1rxgcK2JuyIhmF+RMNpTy1QRtkHIDUOf3B4HlQwakMsWbKftMv/71VMmw=="
+ },
+ "socket.io-parser": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
+ "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
+ "requires": {
+ "@types/component-emitter": "^1.2.10",
+ "component-emitter": "~1.3.0",
+ "debug": "~4.3.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
+ "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ }
+ }
+ },
"sockjs": {
"version": "0.3.21",
"resolved": "https://registry.npm.taobao.org/sockjs/download/sockjs-0.3.21.tgz?cache=0&sync_timestamp=1596167355358&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsockjs%2Fdownload%2Fsockjs-0.3.21.tgz",
@@ -13090,8 +13445,12 @@
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
- "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
- "dev": true
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+ },
+ "string-similarity": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz",
+ "integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ=="
},
"string-width": {
"version": "4.2.2",
@@ -13683,8 +14042,7 @@
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npm.taobao.org/toidentifier/download/toidentifier-1.0.0.tgz",
- "integrity": "sha1-fhvjRw8ed5SLxD2Uo8j013UrpVM=",
- "dev": true
+ "integrity": "sha1-fhvjRw8ed5SLxD2Uo8j013UrpVM="
},
"token-stream": {
"version": "1.0.0",
@@ -13739,6 +14097,11 @@
"integrity": "sha1-zy04vcNKE0vK8QkcQfZhni9nLQA=",
"dev": true
},
+ "tsscmp": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
+ "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="
+ },
"tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
@@ -13777,7 +14140,6 @@
"version": "1.6.18",
"resolved": "https://registry.npm.taobao.org/type-is/download/type-is-1.6.18.tgz",
"integrity": "sha1-TlUs0F3wlGfcvE73Od6J8s83wTE=",
- "dev": true,
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
@@ -14127,6 +14489,14 @@
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
"dev": true
},
+ "utf-8-validate": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.5.tgz",
+ "integrity": "sha512-+pnxRYsS/axEpkrrEpzYfNZGXp0IjC/9RIxwM5gntY4Koi8SHmUGSfxfWqxZdRxrtaoVstuOzUp/rbs3JSPELQ==",
+ "requires": {
+ "node-gyp-build": "^4.2.0"
+ }
+ },
"utf8-byte-length": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
@@ -14175,8 +14545,7 @@
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
- "dev": true
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"vendors": {
"version": "1.0.4",
@@ -15293,6 +15662,11 @@
"fd-slicer": "~1.1.0"
}
},
+ "ylru": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz",
+ "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ=="
+ },
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 70977622..4a1e8097 100644
--- a/package.json
+++ b/package.json
@@ -229,14 +229,21 @@
"dependencies": {
"crypto-js": "^4.1.1",
"electron-log": "^4.4.1",
+ "bufferutil": "^4.0.3",
+ "eiows": "^3.6.0",
"electron-store": "^8.0.0",
"electron-updater": "^4.3.9",
+ "http-terminator": "^3.0.0",
"iconv-lite": "^0.6.3",
"image-size": "^1.0.0",
+ "koa": "^2.13.1",
+ "long": "^4.0.0",
"lrc-file-parser": "^1.1.0",
"needle": "^2.8.0",
"node-id3": "^0.2.3",
"request": "^2.88.2",
+ "socket.io": "^4.1.2",
+ "utf-8-validate": "^5.0.5",
"vue": "^2.6.14",
"vue-i18n": "^8.25.0",
"vue-router": "^3.5.2",
diff --git a/src/common/defaultSetting.js b/src/common/defaultSetting.js
index 6a531679..abf9a439 100644
--- a/src/common/defaultSetting.js
+++ b/src/common/defaultSetting.js
@@ -2,7 +2,7 @@ const path = require('path')
const os = require('os')
const defaultSetting = {
- version: '1.0.42',
+ version: '1.0.43',
player: {
togglePlayMethod: 'listLoop',
highQuality: false,
@@ -85,6 +85,10 @@ const defaultSetting = {
isToTray: false,
themeId: 0,
},
+ sync: {
+ enable: false,
+ port: 23332,
+ },
windowSizeId: 2,
themeId: 0,
langId: null,
diff --git a/src/common/ipcNames.js b/src/common/ipcNames.js
index aa68e8d1..ad9bb243 100644
--- a/src/common/ipcNames.js
+++ b/src/common/ipcNames.js
@@ -66,6 +66,13 @@ const names = {
get_music_url: 'get_music_url',
save_music_url: 'save_music_url',
clear_music_url: 'clear_music_url',
+
+ sync_enable: 'sync_enable',
+ sync_status: 'sync_status',
+ sync_get_status: 'sync_get_status',
+ sync_generate_code: 'sync_generate_code',
+ sync_action_list: 'sync_action_list',
+ sync_list: 'sync_list',
},
winLyric: {
close: 'close',
diff --git a/src/main/events/Common.js b/src/main/events/Common.js
index bb9dea0d..e1ac61da 100644
--- a/src/main/events/Common.js
+++ b/src/main/events/Common.js
@@ -10,6 +10,10 @@ class Common extends EventEmitter {
configStatus(name) {
this.emit(COMMON_EVENT_NAME.configStatus, name)
}
+
+ saveMyList(data) {
+ this.emit(COMMON_EVENT_NAME.saveMyList, data)
+ }
}
module.exports = Common
diff --git a/src/main/events/_name.js b/src/main/events/_name.js
index d15a2c09..ea4c5154 100644
--- a/src/main/events/_name.js
+++ b/src/main/events/_name.js
@@ -1,6 +1,7 @@
exports.common = {
initConfig: 'initConfig',
configStatus: 'config',
+ saveMyList: 'saveMyList',
}
exports.mainWindow = {
diff --git a/src/main/events/index.js b/src/main/events/index.js
index 6899d1f0..9b543697 100644
--- a/src/main/events/index.js
+++ b/src/main/events/index.js
@@ -7,6 +7,7 @@ const WinLyric = require('./WinLyric')
const HotKey = require('./HotKey')
const { Event: UserApi } = require('../modules/userApi')
+const { Event: Sync } = require('../modules/sync')
if (!global.lx_event.common) global.lx_event.common = new Common()
if (!global.lx_event.mainWindow) global.lx_event.mainWindow = new MainWindow()
@@ -15,3 +16,4 @@ if (!global.lx_event.winLyric) global.lx_event.winLyric = new WinLyric()
if (!global.lx_event.hotKey) global.lx_event.hotKey = new HotKey()
if (!global.lx_event.userApi) global.lx_event.userApi = new UserApi()
+if (!global.lx_event.sync) global.lx_event.sync = new Sync()
diff --git a/src/main/modules/sync/event/event.js b/src/main/modules/sync/event/event.js
new file mode 100644
index 00000000..ade680a9
--- /dev/null
+++ b/src/main/modules/sync/event/event.js
@@ -0,0 +1,23 @@
+const { EventEmitter } = require('events')
+const SYNC_EVENT_NAME = require('./name')
+
+class Sync extends EventEmitter {
+ status(status) {
+ this.emit(SYNC_EVENT_NAME.status, status)
+ }
+
+ sync_list(data) {
+ this.emit(SYNC_EVENT_NAME.sync_list, data)
+ }
+
+ sync_handle_list(data) {
+ this.emit(SYNC_EVENT_NAME.sync_handle_list, data)
+ }
+
+ action_list(data) {
+ this.emit(SYNC_EVENT_NAME.sync_action_list, data)
+ }
+}
+
+module.exports = Sync
+
diff --git a/src/main/modules/sync/event/name.js b/src/main/modules/sync/event/name.js
new file mode 100644
index 00000000..30abdf51
--- /dev/null
+++ b/src/main/modules/sync/event/name.js
@@ -0,0 +1,6 @@
+module.exports = {
+ sync_action_list: 'sync_action_list',
+ sync_list: 'sync_list',
+ sync_handle_list: 'sync_handle_list',
+ status: 'status',
+}
diff --git a/src/main/modules/sync/index.js b/src/main/modules/sync/index.js
new file mode 100644
index 00000000..c1237a44
--- /dev/null
+++ b/src/main/modules/sync/index.js
@@ -0,0 +1,15 @@
+const Event = require('./event/event')
+const eventNames = require('./event/name')
+const modules = require('./modules')
+const { startServer, stopServer, getStatus, generateCode } = require('./server/server')
+
+
+module.exports = {
+ startServer,
+ stopServer,
+ getStatus,
+ generateCode,
+ Event,
+ eventNames,
+ modules,
+}
diff --git a/src/main/modules/sync/modules/index.js b/src/main/modules/sync/modules/index.js
new file mode 100644
index 00000000..8e783c9a
--- /dev/null
+++ b/src/main/modules/sync/modules/index.js
@@ -0,0 +1 @@
+exports.list = require('./list')
diff --git a/src/main/modules/sync/modules/list.js b/src/main/modules/sync/modules/list.js
new file mode 100644
index 00000000..41574768
--- /dev/null
+++ b/src/main/modules/sync/modules/list.js
@@ -0,0 +1,41 @@
+const { encryptMsg, decryptMsg } = require('../server/utils')
+let io
+
+const handleListAction = ({ action, data }) => {
+ // console.log(action, data)
+ global.lx_event.sync.action_list({ action, data })
+}
+
+// const addMusic = (orderId, callback) => {
+// // ...
+// }
+
+const broadcast = async(action, data, excludeIds = []) => {
+ if (!io) return
+ const sockets = await io.fetchSockets()
+ for (const socket of sockets) {
+ if (excludeIds.includes(socket.data.keyInfo.clientId)) continue
+ socket.emit(action, encryptMsg(socket.data.keyInfo, data))
+ }
+}
+
+exports.sendListAction = (action, data) => {
+ // io.sockets
+ return broadcast('list:action', JSON.stringify({ action, data }))
+}
+
+exports.registerListHandler = (_io, socket) => {
+ io = _io
+ socket.on('list:action', msg => {
+ // console.log(msg)
+ msg = decryptMsg(socket.data.keyInfo, msg)
+ if (!msg) return
+ handleListAction(JSON.parse(msg))
+ broadcast('list:action', msg, [socket.data.keyInfo.clientId])
+ // socket.broadcast.emit('list:action', { action: 'list_remove', data: { id: 'default', index: 0 } })
+ })
+ // socket.on('list:add', addMusic)
+}
+exports.unregisterListHandler = () => {
+ io = null
+}
diff --git a/src/main/modules/sync/server/auth.js b/src/main/modules/sync/server/auth.js
new file mode 100644
index 00000000..df1d3bf5
--- /dev/null
+++ b/src/main/modules/sync/server/auth.js
@@ -0,0 +1,69 @@
+const { aesEncrypt, aesDecrypt, createClientKeyInfo, getClientKeyInfo, setClientKeyInfo } = require('./utils')
+
+const authMsg = 'lx-music auth::'
+const helloMsg = 'Hello~::^-^::'
+
+exports.authCode = async(req, res, authCode) => {
+ let code = 401
+ let msg = 'Forbidden'
+ // console.log(req.headers)
+ if (req.headers.m) {
+ label:
+ if (req.headers.i) {
+ const keyInfo = getClientKeyInfo(req.headers.i)
+ if (!keyInfo) break label
+ let text
+ try {
+ text = aesDecrypt(req.headers.m, keyInfo.key, keyInfo.iv)
+ } catch (err) {
+ break label
+ }
+ console.log(text)
+ if (text.startsWith(authMsg)) {
+ code = 200
+ const deviceName = text.replace(authMsg, '') || 'Unknown'
+ if (deviceName != keyInfo.deviceName) {
+ keyInfo.deviceName = deviceName
+ setClientKeyInfo(keyInfo)
+ }
+ msg = aesEncrypt(helloMsg, keyInfo.key, keyInfo.iv)
+ }
+ } else {
+ let key = ''.padStart(16, Buffer.from(authCode).toString('hex'))
+ const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
+ key = Buffer.from(key).toString('base64')
+ // console.log(authCode, key, iv)
+ let text
+ try {
+ text = aesDecrypt(req.headers.m, key, iv)
+ } catch (err) {
+ break label
+ }
+ console.log(text)
+ if (text.startsWith(authMsg)) {
+ code = 200
+ const deviceName = text.replace(authMsg, '') || 'Unknown'
+ msg = aesEncrypt(JSON.stringify(createClientKeyInfo(deviceName)), key, iv)
+ }
+ }
+ }
+ res.writeHead(code)
+ res.end(msg)
+}
+
+exports.authConnect = async req => {
+ const { i, t } = req._query
+ label:
+ if (i && t) {
+ const keyInfo = getClientKeyInfo(i)
+ if (!keyInfo) break label
+ let text
+ try {
+ text = aesDecrypt(t, keyInfo.key, keyInfo.iv)
+ } catch (err) {
+ break label
+ }
+ if (text == 'lx-music connect') return
+ }
+ throw new Error('failed')
+}
diff --git a/src/main/modules/sync/server/index.js b/src/main/modules/sync/server/index.js
new file mode 100644
index 00000000..dbfb83f1
--- /dev/null
+++ b/src/main/modules/sync/server/index.js
@@ -0,0 +1,7 @@
+const { startServer, stopServer, getStatus } = require('./server')
+
+module.exports = {
+ startServer,
+ stopServer,
+ getStatus,
+}
diff --git a/src/main/modules/sync/server/server.js b/src/main/modules/sync/server/server.js
new file mode 100644
index 00000000..bcb521ee
--- /dev/null
+++ b/src/main/modules/sync/server/server.js
@@ -0,0 +1,165 @@
+const http = require('http')
+const sio = require('socket.io')
+const { createHttpTerminator } = require('http-terminator')
+const modules = require('../modules')
+const { authCode, authConnect } = require('./auth')
+const { getAddress, getServerId, generateCode, getClientKeyInfo } = require('./utils')
+const syncList = require('./syncList')
+
+
+let status = {
+ status: false,
+ message: '',
+ address: [],
+ code: '',
+ devices: [],
+}
+
+const handleConnection = (io, socket) => {
+ console.log('connection')
+ // console.log(socket.handshake.query)
+ for (const module of Object.values(modules)) {
+ module.registerListHandler(io, socket)
+ }
+}
+
+const authConnection = (req, callback) => {
+ // console.log(req.headers)
+ // // console.log(req.auth)
+ // console.log(req._query.authCode)
+ authConnect(req).then(() => {
+ callback(null, true)
+ }).catch(err => {
+ callback(err, false)
+ })
+}
+
+let httpTerminator = null
+let io = null
+
+const handleStartServer = (port = 9527) => new Promise((resolve, reject) => {
+ const httpServer = http.createServer((req, res) => {
+ // console.log(req.url)
+ let code
+ let msg
+ switch (req.url) {
+ case '/hello':
+ code = 200
+ msg = 'Hello~::^-^::'
+ break
+ case '/id':
+ code = 200
+ msg = 'OjppZDo6' + getServerId()
+ break
+ case '/ah':
+ authCode(req, res, status.code)
+ break
+ default:
+ code = 401
+ msg = 'Forbidden'
+ break
+ }
+ if (!code) return
+ res.writeHead(code)
+ res.end(msg)
+ })
+ httpTerminator = createHttpTerminator({
+ server: httpServer,
+ })
+ io = sio(httpServer, {
+ path: '/sync',
+ serveClient: false,
+ connectTimeout: 10000,
+ pingTimeout: 30000,
+ maxHttpBufferSize: 3e6,
+ allowRequest: authConnection,
+ transports: ['websocket'],
+ })
+
+ io.on('connection', async socket => {
+ socket.on('disconnect', reason => {
+ console.log('disconnect', reason)
+ status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo.clientId), 1)
+ global.lx_event.sync.status(status)
+ })
+ const keyInfo = getClientKeyInfo(socket.handshake.query.i)
+ // socket.lx_keyInfo = keyInfo
+ socket.data.keyInfo = keyInfo
+ try {
+ await syncList(io, socket)
+ } catch (err) {
+ console.log(err)
+ return
+ }
+ status.devices.push(keyInfo)
+ handleConnection(io, socket, keyInfo)
+ global.lx_event.sync.status(status)
+ })
+
+ httpServer.on('error', error => {
+ console.log(error)
+ reject(error)
+ })
+
+ httpServer.on('listening', () => {
+ const addr = httpServer.address()
+ const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
+ console.info(`Listening on ${bind}`)
+ resolve()
+ })
+
+ httpServer.listen(port)
+})
+
+const handleStopServer = async() => {
+ if (!httpTerminator) return
+ await io.close()
+ await httpTerminator.terminate().catch(() => {})
+ io = null
+ httpTerminator = null
+}
+
+exports.stopServer = async() => {
+ if (!status.status) return
+ console.log('stoping sync server...')
+ return handleStopServer().then(() => {
+ console.log('sync server stoped')
+ status.status = false
+ status.message = ''
+ status.address = []
+ status.code = ''
+ }).catch(err => {
+ console.log(err)
+ status.message = err.message
+ }).finally(() => {
+ global.lx_event.sync.status(status)
+ })
+}
+exports.startServer = async port => {
+ if (status.status) await handleStopServer()
+
+ console.log('starting sync server...')
+ return handleStartServer(port).then(() => {
+ console.log('sync server started')
+ status.status = true
+ status.message = ''
+ status.address = getAddress()
+ status.code = generateCode()
+ }).catch(err => {
+ console.log(err)
+ status.status = false
+ status.message = err.message
+ status.address = []
+ status.code = ''
+ }).finally(() => {
+ global.lx_event.sync.status(status)
+ })
+}
+
+exports.getStatus = () => status
+
+exports.generateCode = async() => {
+ status.code = generateCode()
+ global.lx_event.sync.status(status)
+ return status.code
+}
diff --git a/src/main/modules/sync/server/syncList.js b/src/main/modules/sync/server/syncList.js
new file mode 100644
index 00000000..e2df19ab
--- /dev/null
+++ b/src/main/modules/sync/server/syncList.js
@@ -0,0 +1,388 @@
+const path = require('path')
+const fs = require('fs')
+const fsPromises = fs.promises
+const { app } = require('electron')
+const { encryptMsg, decryptMsg } = require('./utils')
+const SYNC_EVENT_NAMES = require('../event/name')
+const { common: COMMON_EVENT_NAME } = require('@main/events/_name')
+const { throttle } = require('@common/utils')
+
+let io
+// const checkFile = path => {
+// fsPromises.access(path, fs.constants.R_OK | fs.constants.W_OK)
+// .then(() => console.log('can access'))
+// .catch(() => console.error('cannot access'))
+// }
+
+const getRemoteListData = socket => new Promise((resolve, reject) => {
+ console.log('getRemoteListData')
+ const handleError = reason => {
+ reject(new Error(reason))
+ }
+ const handleSuccess = enData => {
+ socket.removeListener('disconnect', handleError)
+ socket.removeListener('list:sync', handleSuccess)
+ console.log('getRemoteListData', 'handleSuccess')
+ const data = JSON.parse(decryptMsg(socket.data.keyInfo, enData))
+ if (!data) return reject(new Error('Get remote list data failed'))
+ if (data.action != 'getData') return
+ resolve(data.data)
+ }
+
+ socket.on('disconnect', handleError)
+ socket.on('list:sync', handleSuccess)
+ socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'getData', data: 'all' })))
+})
+
+const getLocalListData = () => new Promise((resolve, reject) => {
+ const handleSuccess = ({ action, data }) => {
+ if (action !== 'getData') return
+ global.lx_event.sync.off(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
+ resolve(data)
+ }
+ global.lx_event.sync.on(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
+ global.lx_event.sync.sync_list({
+ action: 'getData',
+ })
+})
+const getSyncMode = keyInfo => new Promise((resolve, reject) => {
+ const handleSuccess = ({ action, data }) => {
+ if (action !== 'selectMode') return
+ global.lx_event.sync.off(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
+ resolve(data)
+ }
+ global.lx_event.sync.on(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
+ global.lx_event.sync.sync_list({
+ action: 'selectMode',
+ data: keyInfo,
+ })
+})
+
+const finishedSync = socket => {
+ return socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({
+ action: 'finished',
+ })))
+}
+
+const setLocalList = listData => {
+ global.lx_event.sync.sync_list({
+ action: 'setData',
+ data: listData,
+ })
+}
+const setRemotelList = async(socket, listData) => {
+ if (!io) return
+ const sockets = await io.fetchSockets()
+ for (const socket of sockets) {
+ // if (excludeIds.includes(socket.data.keyInfo.clientId)) continue
+ socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'setData', data: listData })))
+ }
+}
+
+let writeFilePromises = {}
+const updateSnapshot = (path, data) => {
+ console.log('updateSnapshot', path)
+ let writeFilePromise = writeFilePromises[path] || Promise.resolve()
+ return writeFilePromise.then(() => {
+ writeFilePromise = writeFilePromises[path] = fsPromises.writeFile(path, data)
+ return writeFilePromise
+ })
+}
+
+
+const createListDataObj = listData => {
+ const listDataObj = {}
+ for (const list of listData.userList) listDataObj[list.id] = list
+ return listDataObj
+}
+
+const handleMergeList = (sourceList, targetList, addMusicLocationType) => {
+ let newList
+ switch (addMusicLocationType) {
+ case 'top':
+ newList = [...targetList.list, ...sourceList.list]
+ break
+ case 'bottom':
+ default:
+ newList = [...sourceList.list, ...targetList.list]
+ break
+ }
+ const map = {}
+ const ids = []
+ switch (addMusicLocationType) {
+ case 'top':
+ newList = [...targetList.list, ...sourceList.list]
+ for (let i = newList.length - 1; i > -1; i--) {
+ const item = newList[i]
+ if (map[item.songmid]) continue
+ ids.unshift(item.songmid)
+ map[item.songmid] = item
+ }
+ break
+ case 'bottom':
+ default:
+ newList = [...sourceList.list, ...targetList.list]
+ for (const item of newList) {
+ if (map[item.songmid]) continue
+ ids.push(item.songmid)
+ map[item.songmid] = item
+ }
+ break
+ }
+ return {
+ ...sourceList,
+ list: ids.map(id => map[id]),
+ }
+}
+const mergeList = (sourceListData, targetListData) => {
+ const addMusicLocationType = global.appSetting.list.addMusicLocationType
+ const newListData = {}
+ newListData.defaultList = handleMergeList(sourceListData.defaultList, targetListData.defaultList, addMusicLocationType)
+ newListData.loveList = handleMergeList(sourceListData.loveList, targetListData.loveList, addMusicLocationType)
+
+ const listDataObj = createListDataObj(sourceListData)
+ newListData.userList = [...sourceListData.userList]
+
+ for (const list of targetListData.userList) {
+ const targetList = listDataObj[list.id]
+ if (targetList) {
+ targetList.list = handleMergeList(targetList, list, addMusicLocationType).list
+ } else {
+ newListData.userList.push(list)
+ }
+ }
+
+ return newListData
+}
+const overwriteList = (sourceListData, targetListData) => {
+ const newListData = {}
+ newListData.defaultList = sourceListData.defaultList
+ newListData.loveList = sourceListData.loveList
+
+ const listDataObj = createListDataObj(sourceListData)
+ newListData.userList = [...sourceListData.userList]
+
+ for (const list of targetListData.userList) {
+ const targetList = listDataObj[list.id]
+ if (targetList) continue
+ newListData.userList.push(list)
+ }
+
+ return newListData
+}
+
+const handleMergeListData = async socket => {
+ let isSelectingMode = false
+ const handleDisconnect = () => {
+ if (!isSelectingMode) return
+ global.lx_event.sync.sync_list({
+ action: 'closeSelectMode',
+ })
+ }
+ socket.on('disconnect', handleDisconnect)
+ isSelectingMode = true
+ const mode = await getSyncMode(socket.data.keyInfo)
+ isSelectingMode = false
+ const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
+ console.log('handleMergeListData', 'remoteListData, localListData')
+ let listData
+ switch (mode) {
+ case 'merge_local_remote':
+ listData = mergeList(localListData, remoteListData)
+ break
+ case 'merge_remote_local':
+ listData = mergeList(remoteListData, localListData)
+ break
+ case 'overwrite_local_remote':
+ listData = overwriteList(localListData, remoteListData)
+ break
+ case 'overwrite_remote_local':
+ listData = overwriteList(remoteListData, localListData)
+ break
+ case 'overwrite_local_remote_full':
+ listData = localListData
+ break
+ case 'overwrite_remote_local_full':
+ listData = remoteListData
+ break
+ case 'none': return
+ case 'cancel':
+ socket.disconnect(true)
+ throw new Error('cancel')
+ }
+ return listData
+}
+
+const handleSyncList = async socket => {
+ const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
+ console.log('handleSyncList', 'remoteListData, localListData')
+ const listData = {}
+ if (localListData.defaultList.list.length || localListData.loveList.list.length || localListData.userList.length) {
+ if (remoteListData.defaultList.list.length || remoteListData.loveList.list.length || remoteListData.userList.length) {
+ const mergedList = await handleMergeListData(socket)
+ console.log('handleMergeListData', 'mergedList')
+ console.log(mergedList)
+ if (!mergedList) return
+ listData.defaultList = mergedList.defaultList
+ listData.loveList = mergedList.loveList
+ listData.userList = mergedList.userList
+ setLocalList(mergedList)
+ setRemotelList(socket, mergedList)
+ } else {
+ setRemotelList(socket, localListData)
+ listData.defaultList = localListData.defaultList
+ listData.loveList = localListData.loveList
+ listData.userList = localListData.userList
+ }
+ } else {
+ if (remoteListData.defaultList.list.length || remoteListData.loveList.list.length || remoteListData.userList.length) {
+ setLocalList(remoteListData)
+ listData.defaultList = remoteListData.defaultList
+ listData.loveList = remoteListData.loveList
+ listData.userList = remoteListData.userList
+ } else {
+ listData.defaultList = localListData.defaultList
+ listData.loveList = localListData.loveList
+ listData.userList = localListData.userList
+ }
+ }
+ return updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
+ defaultList: listData.defaultList,
+ loveList: listData.loveList,
+ userList: listData.userList,
+ })).then(() => {
+ socket.data.isCreatedSnapshot = true
+ return listData
+ })
+}
+
+const mergeListDataFromSnapshot = (sourceList, targetList, snapshotList, addMusicLocationType) => {
+ const removedListIds = new Set()
+ const sourceListItemIds = new Set()
+ const targetListItemIds = new Set()
+ for (const m of sourceList.list) sourceListItemIds.add(m.songmid)
+ for (const m of targetList.list) targetListItemIds.add(m.songmid)
+ for (const m of snapshotList.list) {
+ if (!sourceListItemIds.has(m.songmid)) removedListIds.add(m.songmid)
+ }
+ for (const m of snapshotList.list) {
+ if (!targetListItemIds.has(m.songmid)) removedListIds.add(m.songmid)
+ }
+
+ let newList
+ const map = {}
+ const ids = []
+ switch (addMusicLocationType) {
+ case 'top':
+ newList = [...targetList.list, ...sourceList.list]
+ for (let i = newList.length - 1; i > -1; i--) {
+ const item = newList[i]
+ if (map[item.songmid] || removedListIds.has(item.songmid)) continue
+ ids.unshift(item.songmid)
+ map[item.songmid] = item
+ }
+ break
+ case 'bottom':
+ default:
+ newList = [...sourceList.list, ...targetList.list]
+ for (const item of newList) {
+ if (map[item.songmid] || removedListIds.has(item.songmid)) continue
+ ids.push(item.songmid)
+ map[item.songmid] = item
+ }
+ break
+ }
+ return {
+ ...sourceList,
+ list: ids.map(id => map[id]),
+ }
+}
+const handleMergeListDataFromSnapshot = async(socket, snapshot) => {
+ const addMusicLocationType = global.appSetting.list.addMusicLocationType
+ const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
+ console.log('handleMergeListDataFromSnapshot', 'remoteListData, localListData')
+ const newListData = {}
+ newListData.defaultList = mergeListDataFromSnapshot(localListData.defaultList, remoteListData.defaultList, snapshot.defaultList, addMusicLocationType)
+ newListData.loveList = mergeListDataFromSnapshot(localListData.loveList, remoteListData.loveList, snapshot.loveList, addMusicLocationType)
+ const localUserListData = createListDataObj(localListData)
+ const remoteUserListData = createListDataObj(remoteListData)
+ const snapshotUserListData = createListDataObj(snapshot)
+ const removedListIds = new Set()
+ const localUserListIds = new Set()
+ const remoteUserListIds = new Set()
+ for (const l of localListData.userList) localUserListIds.add(l.id)
+ for (const l of remoteListData.userList) remoteUserListIds.add(l.id)
+
+ for (const l of snapshot.userList) {
+ if (!localUserListIds.has(l.id)) removedListIds.add(l.id)
+ }
+ for (const l of snapshot.userList) {
+ if (!remoteUserListIds.has(l.id)) removedListIds.add(l.id)
+ }
+
+ let newUserList = []
+ for (const list of localListData.userList) {
+ if (removedListIds.has(list.id)) continue
+ const remoteList = remoteUserListData[list.id]
+ let newList
+ if (remoteList) {
+ newList = mergeListDataFromSnapshot(list, remoteList, snapshotUserListData[list.id], addMusicLocationType)
+ } else {
+ newList = { ...list }
+ }
+ newUserList.push(newList)
+ }
+ for (const list of remoteListData.userList) {
+ if (removedListIds.has(list.id) || localUserListData[list.id]) continue
+ newUserList.push({ ...list })
+ }
+ newListData.userList = newUserList
+ setLocalList(newListData)
+ setRemotelList(socket, newListData)
+ return updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
+ defaultList: newListData.defaultList,
+ loveList: newListData.loveList,
+ userList: newListData.userList,
+ })).then(() => {
+ socket.data.isCreatedSnapshot = true
+ return newListData
+ })
+}
+
+const registerUpdateSnapshotTask = (socket, snapshot) => {
+ if (!socket.data.isCreatedSnapshot) return
+ const handleUpdateSnapshot = throttle(({ defaultList, loveList, userList }) => {
+ if (defaultList != null) snapshot.defaultList = defaultList
+ if (loveList != null) snapshot.loveList = loveList
+ if (userList != null) snapshot.userList = userList
+ updateSnapshot(socket.data.snapshotFilePath, JSON.stringify(snapshot))
+ }, 10000)
+ global.lx_event.common.on(COMMON_EVENT_NAME.saveMyList, handleUpdateSnapshot)
+ socket.on('disconnect', () => {
+ global.lx_event.common.off(COMMON_EVENT_NAME.saveMyList, handleUpdateSnapshot)
+ })
+}
+
+const syncList = async socket => {
+ socket.data.snapshotFilePath = path.join(app.getPath('userData'), `snapshot-${Buffer.from(socket.data.keyInfo.clientId).toString('hex').substring(0, 10)}.json`)
+ let fileData
+ let isSyncRequired = false
+ try {
+ fileData = await fsPromises.readFile(socket.data.snapshotFilePath)
+ fileData = JSON.parse(fileData)
+ } catch (err) {
+ if (err.code !== 'ENOENT') throw err
+ isSyncRequired = true
+ }
+ console.log('isSyncRequired', isSyncRequired)
+ if (isSyncRequired) return handleSyncList(socket)
+ return handleMergeListDataFromSnapshot(socket, fileData)
+}
+
+module.exports = (_io, socket) => {
+ io = _io
+ return syncList(socket).then(newListData => {
+ registerUpdateSnapshotTask(socket, { ...newListData })
+ return finishedSync(socket)
+ })
+}
diff --git a/src/main/modules/sync/server/utils.js b/src/main/modules/sync/server/utils.js
new file mode 100644
index 00000000..e880fd83
--- /dev/null
+++ b/src/main/modules/sync/server/utils.js
@@ -0,0 +1,93 @@
+const { networkInterfaces } = require('os')
+const { randomBytes, createCipheriv, createDecipheriv } = require('crypto')
+const getStore = require('@common/store')
+const STORE_NAME = 'sync'
+
+exports.getAddress = () => {
+ const nets = networkInterfaces()
+ const results = []
+ // console.log(nets)
+
+ for (const name of Object.keys(nets)) {
+ for (const net of nets[name]) {
+ // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
+ if (net.family === 'IPv4' && !net.internal) {
+ results.push(net.address)
+ }
+ }
+ }
+ return results
+}
+
+let serverId
+exports.getServerId = () => {
+ if (serverId) return serverId
+ const store = getStore(STORE_NAME)
+ serverId = store.get('serverId')
+ if (!serverId) {
+ serverId = randomBytes(4 * 4).toString('base64')
+ store.set('serverId', serverId)
+ }
+ return serverId
+}
+
+let keyInfos
+exports.createClientKeyInfo = deviceName => {
+ const keyInfo = {
+ clientId: randomBytes(4 * 4).toString('base64'),
+ key: randomBytes(16).toString('base64'),
+ iv: randomBytes(16).toString('base64'),
+ deviceName,
+ }
+ const store = getStore(STORE_NAME)
+ if (!keyInfos) keyInfos = store.get('keys') || {}
+ if (Object.keys(keyInfos).length > 101) throw new Error('max keys')
+
+ keyInfos[keyInfo.clientId] = keyInfo
+ store.set('keys', keyInfos)
+ return keyInfo
+}
+exports.setClientKeyInfo = keyInfo => {
+ keyInfos[keyInfo.clientId] = keyInfo
+ const store = getStore(STORE_NAME)
+ store.set('keys', keyInfos)
+}
+exports.getClientKeyInfo = clientId => {
+ if (!keyInfos) {
+ const store = getStore(STORE_NAME)
+ keyInfos = store.get('keys') || {}
+ }
+ return keyInfos[clientId] || null
+}
+
+exports.generateCode = () => {
+ return Math.random().toString().substring(2, 8)
+}
+
+exports.aesEncrypt = (buffer, key, iv) => {
+ const cipher = createCipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
+ return Buffer.concat([cipher.update(buffer), cipher.final()]).toString('base64')
+}
+
+exports.aesDecrypt = (text, key, iv) => {
+ const decipher = createDecipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
+ return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()
+}
+
+exports.encryptMsg = (keyInfo, msg) => {
+ return msg
+ // if (!keyInfo) return ''
+ // return exports.aesEncrypt(msg, keyInfo.key, keyInfo.iv)
+}
+
+exports.decryptMsg = (keyInfo, enMsg) => {
+ return enMsg
+ // if (!keyInfo) return ''
+ // let msg = ''
+ // try {
+ // msg = exports.aesDecrypt(enMsg, keyInfo.key, keyInfo.iv)
+ // } catch (err) {
+ // console.log(err)
+ // }
+ // return msg
+}
diff --git a/src/main/rendererEvents/index.js b/src/main/rendererEvents/index.js
index 9fafd6df..1df53eeb 100644
--- a/src/main/rendererEvents/index.js
+++ b/src/main/rendererEvents/index.js
@@ -23,3 +23,4 @@ require('./musicUrl')
require('./kw_decodeLyric')
require('./userApi')
+require('./sync')
diff --git a/src/main/rendererEvents/playList.js b/src/main/rendererEvents/playList.js
index 3eb0aed3..d73a7e29 100644
--- a/src/main/rendererEvents/playList.js
+++ b/src/main/rendererEvents/playList.js
@@ -24,6 +24,7 @@ mainOn(ipcMainWindowNames.save_playlist, (event, { type, data }) => {
switch (type) {
case 'myList':
handleSaveList(data)
+ global.lx_event.common.saveMyList(data)
break
case 'downloadList':
getStore('downloadList').set('list', data)
diff --git a/src/main/rendererEvents/sync.js b/src/main/rendererEvents/sync.js
new file mode 100644
index 00000000..55569f84
--- /dev/null
+++ b/src/main/rendererEvents/sync.js
@@ -0,0 +1,33 @@
+const { mainSend, NAMES: { mainWindow: ipcMainWindowNames }, mainOn, mainHandle } = require('@common/ipc')
+const { eventNames, modules, startServer, stopServer, getStatus, generateCode } = require('../modules/sync')
+
+
+mainOn(ipcMainWindowNames.sync_action_list, (event, { action, data }) => {
+ modules.list.sendListAction(action, data)
+})
+
+mainHandle(ipcMainWindowNames.sync_enable, (event, { enable, port }) => {
+ return enable ? startServer(port) : stopServer()
+})
+
+mainHandle(ipcMainWindowNames.sync_get_status, () => {
+ return getStatus()
+})
+
+mainHandle(ipcMainWindowNames.sync_generate_code, () => {
+ return generateCode()
+})
+
+mainOn(ipcMainWindowNames.sync_list, (event, { action, data }) => {
+ global.lx_event.sync.sync_handle_list({ action, data })
+})
+
+global.lx_event.sync.on(eventNames.sync_action_list, ({ action, data }) => {
+ mainSend(global.modules.mainWindow, ipcMainWindowNames.sync_action_list, { action, data })
+})
+global.lx_event.sync.on(eventNames.status, status => {
+ mainSend(global.modules.mainWindow, ipcMainWindowNames.sync_status, status)
+})
+global.lx_event.sync.on(eventNames.sync_list, ({ action, data }) => {
+ mainSend(global.modules.mainWindow, ipcMainWindowNames.sync_list, { action, data })
+})
diff --git a/src/renderer/App.vue b/src/renderer/App.vue
index 56626d6b..5e548f19 100644
--- a/src/renderer/App.vue
+++ b/src/renderer/App.vue
@@ -8,6 +8,7 @@
core-icons
material-version-modal(v-show="version.showModal")
material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact")
+ material-sync-mode-modal(v-show="globalObj.sync.isShowSyncMode")
#container(v-else :class="theme")
core-aside#left
#right
@@ -17,6 +18,7 @@
core-icons
material-version-modal(v-show="version.showModal")
material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact")
+ material-sync-mode-modal(v-show="globalObj.sync.isShowSyncMode")
+
+
+
diff --git a/src/renderer/event/index.js b/src/renderer/event/index.js
index d818b955..29374416 100644
--- a/src/renderer/event/index.js
+++ b/src/renderer/event/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue'
import keyBind from '../utils/keyBind'
import { rendererOn, rendererSend, NAMES, rendererInvoke } from '../../common/ipc'
-import { base as baseName } from './names'
+import { base as baseName, sync as syncName } from './names'
import { common as hotKeyNamesCommon } from '../../common/hotKey'
const eventHub = window.eventHub = new Vue()
@@ -77,3 +77,18 @@ rendererOn(NAMES.mainWindow.set_hot_key_config, (event, config) => {
}
window.eventHub.$emit(baseName.set_hot_key_config, config)
})
+
+rendererOn(NAMES.mainWindow.sync_action_list, (event, { action, data }) => {
+ window.eventHub.$emit(syncName.handle_action_list, { action, data })
+})
+eventHub.$on(syncName.send_action_list, ({ action, data }) => {
+ if (!window.globalObj.sync.enable) return
+ rendererSend(NAMES.mainWindow.sync_action_list, { action, data })
+})
+rendererOn(NAMES.mainWindow.sync_list, (event, { action, data }) => {
+ window.eventHub.$emit(syncName.handle_sync_list, { action, data })
+})
+eventHub.$on(syncName.send_sync_list, ({ action, data }) => {
+ if (!window.globalObj.sync.enable) return
+ rendererSend(NAMES.mainWindow.sync_list, { action, data })
+})
diff --git a/src/renderer/event/names.js b/src/renderer/event/names.js
index e8135907..7ce8c475 100644
--- a/src/renderer/event/names.js
+++ b/src/renderer/event/names.js
@@ -10,6 +10,12 @@ const names = {
set_config: 'set_config',
set_hot_key_config: 'set_hot_key_config',
},
+ sync: {
+ send_action_list: 'send_action_list',
+ handle_action_list: 'handle_action_list',
+ send_sync_list: 'send_sync_list',
+ handle_sync_list: 'handle_sync_list',
+ },
}
for (const item of Object.keys(names)) {
@@ -20,3 +26,4 @@ for (const item of Object.keys(names)) {
}
export const base = names.base
+export const sync = names.sync
diff --git a/src/renderer/lang/en-us/material/sync_mode_modal.json b/src/renderer/lang/en-us/material/sync_mode_modal.json
new file mode 100644
index 00000000..77d3b00d
--- /dev/null
+++ b/src/renderer/lang/en-us/material/sync_mode_modal.json
@@ -0,0 +1,19 @@
+{
+ "merge_btn_local_remote": "Local list merge remote list",
+ "merge_btn_remote_local": "Remote list merge local list",
+ "merge_label": "Merge",
+ "merge_tip": "Merge:",
+ "merge_tip_desc": "Merge the two lists together, the same song will be removed (the song of the merged person is removed), and different songs will be added.",
+ "other_label": "Other",
+ "other_tip": "Other: ",
+ "other_tip_desc": "\"Only use real-time synchronization function\" will not modify the lists of both parties, only real-time synchronization operations; \"Cancel synchronization\" will directly disconnect the two parties.",
+ "overwrite": "Full coverage",
+ "overwrite_btn_cancel": "Cancel sync",
+ "overwrite_btn_local_remote": "Local list Overwrite remote list",
+ "overwrite_btn_none": "Only use real-time synchronization",
+ "overwrite_btn_remote_local": "Remote list Overwrite local list",
+ "overwrite_label": "Cover",
+ "overwrite_tip": "Cover: ",
+ "overwrite_tip_desc": "The list with the same ID of the covered person and the covered list will be deleted and replaced with the list of the covered person (lists with different list IDs will be merged together). If you check Complete coverage, all lists of the covered person will be moved. \nDivide, and then replace with a list of overriders.",
+ "title": "Choose how to synchronize the list with {name}"
+}
diff --git a/src/renderer/lang/en-us/view/setting.json b/src/renderer/lang/en-us/view/setting.json
index 946cbeb3..b3ce74f6 100644
--- a/src/renderer/lang/en-us/view/setting.json
+++ b/src/renderer/lang/en-us/view/setting.json
@@ -130,6 +130,14 @@
"search_focus_search_box": "Automatically focus the search box on startup",
"search_history": "Search history",
"search_hot": "Top Searches",
+ "sync": "Data synchronization [This is a test function, it is recommended to back up the playlist before using it for the first time]",
+ "sync_address": "Synchronization service address: {address}",
+ "sync_auth_code": "Connection code: {code}",
+ "sync_device": "Connected devices: {devices}",
+ "sync_enable": "Enable synchronization",
+ "sync_port": "Sync port settings",
+ "sync_port_tip": "Please enter the synchronization service port number",
+ "sync_refresh_code": "Refresh the connection code",
"update": "Update",
"update_checking": "Checking for updates...",
"update_current_label": "Current version: ",
diff --git a/src/renderer/lang/zh-cn/material/sync_mode_modal.json b/src/renderer/lang/zh-cn/material/sync_mode_modal.json
new file mode 100644
index 00000000..e50ff220
--- /dev/null
+++ b/src/renderer/lang/zh-cn/material/sync_mode_modal.json
@@ -0,0 +1,19 @@
+{
+ "merge_btn_local_remote": "本机列表 合并 远程列表",
+ "merge_btn_remote_local": "远程列表 合并 本机列表",
+ "merge_label": "合并",
+ "merge_tip": "合并:",
+ "merge_tip_desc": "将两边的列表合并到一起,相同的歌曲将被去掉(去掉的是被合并者的歌曲),不同的歌曲将被添加。",
+ "other_label": "其他",
+ "other_tip": "其他:",
+ "other_tip_desc": "“仅使用实时同步功能”将不修改双方的列表,仅实时同步操作;“取消同步”将直接断开双方的连接。",
+ "overwrite": "完全覆盖",
+ "overwrite_btn_cancel": "取消同步",
+ "overwrite_btn_local_remote": "本机列表 覆盖 远程列表",
+ "overwrite_btn_none": "仅使用实时同步功能",
+ "overwrite_btn_remote_local": "远程列表 覆盖 本机列表",
+ "overwrite_label": "覆盖",
+ "overwrite_tip": "覆盖:",
+ "overwrite_tip_desc": "被覆盖者与覆盖者列表ID相同的列表将被删除后替换成覆盖者的列表(列表ID不同的列表将被合并到一起),若勾选完全覆盖,则被覆盖者的所有列表将被移除,然后替换成覆盖者的列表。",
+ "title": "选择与 {name} 的列表同步方式"
+}
diff --git a/src/renderer/lang/zh-cn/view/setting.json b/src/renderer/lang/zh-cn/view/setting.json
index 7ee36002..e43d4aef 100644
--- a/src/renderer/lang/zh-cn/view/setting.json
+++ b/src/renderer/lang/zh-cn/view/setting.json
@@ -130,6 +130,14 @@
"search_focus_search_box": "启动时自动聚焦搜索框",
"search_history": "显示历史搜索记录",
"search_hot": "显示热门搜索",
+ "sync": "数据同步 [此为测试功能,首次使用前建议先备份一次歌单]",
+ "sync_address": "同步服务地址:{address}",
+ "sync_auth_code": "连接码:{code}",
+ "sync_device": "已连接的设备:{devices}",
+ "sync_enable": "启用同步功能",
+ "sync_port": "同步端口设置",
+ "sync_port_tip": "请输入同步服务端口号",
+ "sync_refresh_code": "刷新连接码",
"update": "软件更新",
"update_checking": "检查更新中...",
"update_current_label": "当前版本:",
diff --git a/src/renderer/lang/zh-tw/material/sync_mode_modal.json b/src/renderer/lang/zh-tw/material/sync_mode_modal.json
new file mode 100644
index 00000000..bce8305b
--- /dev/null
+++ b/src/renderer/lang/zh-tw/material/sync_mode_modal.json
@@ -0,0 +1,19 @@
+{
+ "merge_btn_local_remote": "本機列表 合併 遠程列表",
+ "merge_btn_remote_local": "遠程列表 合併 本機列表",
+ "merge_label": "合併",
+ "merge_tip": "合併:",
+ "merge_tip_desc": "將兩邊的列表合併到一起,相同的歌曲將被去掉(去掉的是被合併者的歌曲),不同的歌曲將被添加。",
+ "other_label": "其他",
+ "other_tip": "其他:",
+ "other_tip_desc": "“僅使用實時同步功能”將不修改雙方的列表,僅實時同步操作;“取消同步”將直接斷開雙方的連接。",
+ "overwrite": "完全覆蓋",
+ "overwrite_btn_cancel": "取消同步",
+ "overwrite_btn_local_remote": "本機列表 覆蓋 遠程列表",
+ "overwrite_btn_none": "僅使用實時同步功能",
+ "overwrite_btn_remote_local": "遠程列表 覆蓋 本機列表",
+ "overwrite_label": "覆蓋",
+ "overwrite_tip": "覆蓋:",
+ "overwrite_tip_desc": "被覆蓋者與覆蓋者列表ID相同的列表將被刪除後替換成覆蓋者的列表(列表ID不同的列表將被合併到一起),若勾選完全覆蓋,則被覆蓋者的所有列表將被移除,然後替換成覆蓋者的列表。",
+ "title": "選擇與 {name} 的列表同步方式"
+}
diff --git a/src/renderer/lang/zh-tw/view/setting.json b/src/renderer/lang/zh-tw/view/setting.json
index 79d82a22..5dce5ebd 100644
--- a/src/renderer/lang/zh-tw/view/setting.json
+++ b/src/renderer/lang/zh-tw/view/setting.json
@@ -130,6 +130,14 @@
"search_focus_search_box": "啟動時自動聚焦搜索框",
"search_history": "顯示歷史搜索記錄",
"search_hot": "顯示熱門搜索",
+ "sync": "數據同步 [此為測試功能,首次使用前建議先備份一次歌單]",
+ "sync_address": "同步服務地址:{address}",
+ "sync_auth_code": "連接碼:{code}",
+ "sync_device": "已連接的設備:{devices}",
+ "sync_enable": "啟用同步功能",
+ "sync_port": "同步端口設置",
+ "sync_port_tip": "請輸入同步服務端口號",
+ "sync_refresh_code": "刷新連接碼",
"update": "軟件更新",
"update_checking": "檢查更新中...",
"update_current_label": "當前版本:",
diff --git a/src/renderer/store/modules/list.js b/src/renderer/store/modules/list.js
index 13765a68..b53c6d71 100644
--- a/src/renderer/store/modules/list.js
+++ b/src/renderer/store/modules/list.js
@@ -1,10 +1,14 @@
import musicSdk from '../../utils/music'
import { clearLyric, clearMusicUrl } from '../../utils'
+import { sync as eventSyncName } from '@renderer/event/names'
let allList = {}
window.allList = allList
const allListInit = (defaultList, loveList, userList) => {
+ for (const id of Object.keys(allList)) {
+ delete allList[id]
+ }
allList[defaultList.id] = defaultList
allList[loveList.id] = loveList
for (const list of userList) allList[list.id] = list
@@ -67,8 +71,28 @@ const mutations = {
if (userList != null) state.userList = userList
allListInit(state.defaultList, state.loveList, state.userList)
state.isInitedList = true
+
+ // if (!isSync) {
+ // window.eventHub.$emit(eventSyncName.send_action_list, {
+ // action: 'init_list',
+ // data: { defaultList, loveList, userList },
+ // })
+ // }
},
- setList(state, { id, list, name, location, source, sourceListId }) {
+ setSyncListData(state, { defaultList, loveList, userList }) {
+ state.defaultList.list.splice(0, state.defaultList.list.length, ...defaultList.list)
+ state.loveList.list.splice(0, state.loveList.list.length, ...loveList.list)
+ state.userList = userList
+ allListInit(state.defaultList, state.loveList, state.userList)
+ },
+ setList(state, { id, list, name, location, source, sourceListId, isSync }) {
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'set_list',
+ data: { id, list, name, location, source, sourceListId },
+ })
+ }
+
const targetList = allList[id]
if (targetList) {
if (name && targetList.name === name) {
@@ -90,11 +114,20 @@ const mutations = {
state.userList.push(newList)
allListUpdate(newList)
},
- listAdd(state, { id, musicInfo }) {
+ listAdd(state, { id, musicInfo, addMusicLocationType, isSync }) {
+ if (!addMusicLocationType) addMusicLocationType = this.state.setting.list.addMusicLocationType
+
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'list_add',
+ data: { id, musicInfo, addMusicLocationType },
+ })
+ }
+
const targetList = allList[id]
if (!targetList) return
if (targetList.list.some(s => s.songmid === musicInfo.songmid)) return
- switch (this.state.setting.list.addMusicLocationType) {
+ switch (addMusicLocationType) {
case 'top':
targetList.list.unshift(musicInfo)
break
@@ -104,11 +137,18 @@ const mutations = {
break
}
},
- listMove(state, { fromId, musicInfo, toId }) {
+ listMove(state, { fromId, musicInfo, toId, isSync }) {
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'list_move',
+ data: { fromId, musicInfo, toId },
+ })
+ }
+
const fromList = allList[fromId]
const toList = allList[toId]
if (!fromList || !toList) return
- fromList.list.splice(fromList.list.indexOf(musicInfo), 1)
+ fromList.list.splice(fromList.list.findIndex(s => s.songmid === musicInfo.songmid), 1)
let index = toList.list.findIndex(s => s.songmid === musicInfo.songmid)
if (index < 0) {
switch (this.state.setting.list.addMusicLocationType) {
@@ -122,41 +162,79 @@ const mutations = {
}
}
},
- listAddMultiple(state, { id, list }) {
+ listAddMultiple(state, { id, list, addMusicLocationType, isSync }) {
+ if (!addMusicLocationType) addMusicLocationType = this.state.setting.list.addMusicLocationType
+
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'list_add_multiple',
+ data: { id, list, addMusicLocationType },
+ })
+ }
+
let targetList = allList[id]
if (!targetList) return
let newList
- switch (this.state.setting.list.addMusicLocationType) {
+ const map = {}
+ const ids = []
+ switch (addMusicLocationType) {
case 'top':
newList = [...list, ...targetList.list]
+ for (let i = newList.length - 1; i > -1; i--) {
+ const item = newList[i]
+ if (map[item.songmid]) continue
+ ids.unshift(item.songmid)
+ map[item.songmid] = item
+ }
break
case 'bottom':
default:
newList = [...targetList.list, ...list]
+ for (const item of newList) {
+ if (map[item.songmid]) continue
+ ids.push(item.songmid)
+ map[item.songmid] = item
+ }
break
}
- let map = {}
- let ids = []
- for (const item of newList) {
- if (map[item.songmid]) continue
- ids.push(item.songmid)
- map[item.songmid] = item
- }
targetList.list.splice(0, targetList.list.length, ...ids.map(id => map[id]))
},
// { fromId, toId, list }
- listMoveMultiple(state, { fromId, toId, list }) {
+ listMoveMultiple(state, { fromId, toId, list, isSync }) {
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'list_move_multiple',
+ data: { fromId, toId, list },
+ })
+ }
+
// console.log(state.commit)
- this.commit('list/listRemoveMultiple', { id: fromId, list })
- this.commit('list/listAddMultiple', { id: toId, list })
+ this.commit('list/listRemoveMultiple', { listId: fromId, ids: list.map(s => s.songmid), isSync: true })
+ this.commit('list/listAddMultiple', { id: toId, list, isSync: true })
},
- listRemove(state, { id, index }) {
- let targetList = allList[id]
+ listRemove(state, { listId, id, isSync }) {
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'list_remove',
+ data: { listId, id },
+ })
+ }
+
+ let targetList = allList[listId]
if (!targetList) return
+ const index = targetList.list.findIndex(item => item.songmid == id)
+ if (index < 0) return
targetList.list.splice(index, 1)
},
- listRemoveMultiple(state, { id, list }) {
- let targetList = allList[id]
+ listRemoveMultiple(state, { listId, ids: musicIds, isSync }) {
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'list_remove_multiple',
+ data: { listId, ids: musicIds },
+ })
+ }
+
+ let targetList = allList[listId]
if (!targetList) return
let map = {}
let ids = []
@@ -164,25 +242,50 @@ const mutations = {
ids.push(item.songmid)
map[item.songmid] = item
}
- for (const item of list) {
- if (map[item.songmid]) delete map[item.songmid]
+ for (const songmid of musicIds) {
+ if (map[songmid]) delete map[songmid]
}
let newList = []
for (const id of ids) if (map[id]) newList.push(map[id])
targetList.list.splice(0, targetList.list.length, ...newList)
},
- listClear(state, id) {
+ listClear(state, { id, isSync }) {
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'list_clear',
+ data: { id },
+ })
+ }
+
let targetList = allList[id]
if (!targetList) return
targetList.list.splice(0, targetList.list.length)
},
- updateMusicInfo(state, { id, index, data, musicInfo = {} }) {
- let targetList = allList[id]
- if (!targetList) return Object.assign(musicInfo, data)
- Object.assign(targetList.list[index], data)
+ updateMusicInfo(state, { listId, id, data, musicInfo, isSync }) {
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'update_music_info',
+ data: { listId, id, data, musicInfo },
+ })
+ }
+
+ let targetList = allList[listId]
+ if (!targetList) {
+ if (musicInfo) Object.assign(musicInfo, data)
+ return
+ }
+ const targetMusicInfo = targetList.list.find(item => item.songmid == id)
+ if (targetMusicInfo) Object.assign(targetMusicInfo, data)
},
- createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId }) {
+ createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId, isSync }) {
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'create_user_list',
+ data: { name, id, list, source, sourceListId },
+ })
+ }
+
let newList = state.userList.find(item => item.id === id)
if (!newList) {
newList = {
@@ -196,35 +299,75 @@ const mutations = {
state.userList.push(newList)
allListUpdate(newList)
}
- this.commit('list/listAddMultiple', { id, list })
+ this.commit('list/listAddMultiple', { id, list, isSync: true })
},
- removeUserList(state, index) {
+ removeUserList(state, { id, isSync }) {
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'remove_user_list',
+ data: { id },
+ })
+ }
+
+ const index = state.userList.findIndex(l => l.id === id)
+ if (index < 0) return
let list = state.userList.splice(index, 1)[0]
allListRemove(list)
},
- setUserListName(state, { index, name }) {
- let list = state.userList[index]
+ setUserListName(state, { id, name, isSync }) {
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'set_user_list_name',
+ data: { id, name },
+ })
+ }
+
+ let list = allList[id]
if (!list) return
list.name = name
},
- moveupUserList(state, index) {
- let targetList = state.userList[index]
+ moveupUserList(state, { id, isSync }) {
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'moveup_user_list',
+ data: { id },
+ })
+ }
+
+ const index = state.userList.findIndex(l => l.id == id)
+ if (index < 0) return
+ let targetList = allList[id]
state.userList.splice(index, 1)
state.userList.splice(index - 1, 0, targetList)
},
- movedownUserList(state, index) {
- let targetList = state.userList[index]
+ movedownUserList(state, { id, isSync }) {
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'movedown_user_list',
+ data: { id },
+ })
+ }
+ const index = state.userList.findIndex(l => l.id == id)
+ if (index < 0) return
+ let targetList = allList[id]
state.userList.splice(index, 1)
state.userList.splice(index + 1, 0, targetList)
},
setListScroll(state, { id, location }) {
if (allList[id]) allList[id].location = location
},
- sortList(state, { id, sortNum, musicInfos }) {
- let targetList = allList[id]
- this.commit('list/listRemoveMultiple', { id, list: musicInfos })
+ setMusicPosition(state, { id, position, list, isSync }) {
+ if (!isSync) {
+ window.eventHub.$emit(eventSyncName.send_action_list, {
+ action: 'set_music_position',
+ data: { id, position, list },
+ })
+ }
- targetList.list.splice(sortNum - 1, 0, ...musicInfos)
+ let targetList = allList[id]
+ this.commit('list/listRemoveMultiple', { listId: id, ids: list.map(m => m.songmid), isSync: true })
+
+ targetList.list.splice(position - 1, 0, ...list)
},
clearCache() {
const lists = Object.values(allList)
diff --git a/src/renderer/store/modules/player.js b/src/renderer/store/modules/player.js
index 9444d749..75a7bc9f 100644
--- a/src/renderer/store/modules/player.js
+++ b/src/renderer/store/modules/player.js
@@ -156,11 +156,11 @@ const getters = {
if (listId != '__temp__') {
if (isPlayList) {
- playIndex = state.listInfo.list.indexOf(state.playMusicInfo.musicInfo)
+ playIndex = state.listInfo.list.findIndex(m => m.songmid == state.playMusicInfo.musicInfo.songmid)
if (!isTempPlay) listPlayIndex = playIndex
} else {
let list = window.allList[listId]
- if (list) playIndex = list.list.indexOf(state.playMusicInfo.musicInfo)
+ if (list) playIndex = list.list.findIndex(m => m.songmid == state.playMusicInfo.musicInfo.songmid)
}
}
// console.log({
@@ -273,7 +273,8 @@ const actions = {
})
if (!filteredList.length) return commit('setPlayMusicInfo', null)
const playInfo = getters.playInfo
- let currentIndex = filteredList.indexOf(currentList[playInfo.listPlayIndex])
+ const currentMusic = currentList[playInfo.listPlayIndex]
+ let currentIndex = filteredList.findIndex(m => m.songmid == currentMusic.songmid)
if (currentIndex == -1) currentIndex = 0
let nextIndex = currentIndex
if (!playInfo.isTempPlay) {
@@ -334,7 +335,8 @@ const actions = {
if (!filteredList.length) return commit('setPlayMusicInfo', null)
const playInfo = getters.playInfo
- const currentIndex = filteredList.indexOf(currentList[playInfo.listPlayIndex])
+ const currentMusic = currentList[playInfo.listPlayIndex]
+ let currentIndex = filteredList.findIndex(m => m.songmid == currentMusic.songmid)
let nextIndex = currentIndex
switch (rootState.setting.player.togglePlayMethod) {
case 'listLoop':
@@ -433,7 +435,7 @@ const mutations = {
playIndex = -1
} else {
let listId = playMusicInfo.listId
- if (listId != '__temp__' && !playMusicInfo.isTempPlay && listId === state.listInfo.id) playIndex = state.listInfo.list.indexOf(playMusicInfo.musicInfo)
+ if (listId != '__temp__' && !playMusicInfo.isTempPlay && listId === state.listInfo.id) playIndex = state.listInfo.list.findIndex(m => m.songmid == playMusicInfo.musicInfo.songmid)
}
state.playMusicInfo = playMusicInfo
diff --git a/src/renderer/views/List.vue b/src/renderer/views/List.vue
index d71f876c..e9022860 100644
--- a/src/renderer/views/List.vue
+++ b/src/renderer/views/List.vue
@@ -355,7 +355,7 @@ export default {
'removeUserList',
'setListScroll',
'setList',
- 'sortList',
+ 'setMusicPosition',
]),
...mapActions('songList', ['getListDetailAll']),
...mapActions('leaderboard', {
@@ -576,7 +576,7 @@ export default {
this.setPlayList({ list: this.listData, index })
},
handleRemove(index) {
- this.listRemove({ id: this.listId, index })
+ this.listRemove({ listId: this.listId, id: this.list[index].songmid })
},
handleListBtnClick(info) {
switch (info.action) {
@@ -677,8 +677,9 @@ export default {
let dom_target = this.$refs.dom_lists_list.querySelector('.' + this.$style.editing)
if (dom_target) dom_target.classList.remove(this.$style.editing)
let name = event.target.value.trim()
- if (name.length) return this.setUserListName({ index, name })
- event.target.value = this.userList[index].name
+ const targetList = this.userList[index]
+ if (name.length) return this.setUserListName({ id: targetList.id, name })
+ event.target.value = targetList.name
},
handleListsCreate(event) {
if (event.target.readonly) return
@@ -774,13 +775,13 @@ export default {
this.handleSyncSourceList(index)
break
case 'moveup':
- this.moveupUserList(index)
+ this.moveupUserList({ id: this.userList[index].id })
break
case 'movedown':
- this.movedownUserList(index)
+ this.movedownUserList({ id: this.userList[index].id })
break
case 'remove':
- this.removeUserList(index)
+ this.removeUserList({ id: this.userList[index].id })
break
}
},
@@ -870,7 +871,7 @@ export default {
break
case 'remove':
if (this.selectdListDetailData.length) {
- this.listRemoveMultiple({ id: this.listId, list: this.selectdListDetailData })
+ this.listRemoveMultiple({ listId: this.listId, ids: this.selectdListDetailData.map(m => m.songmid) })
this.removeAllSelectListDetail()
} else {
this.handleRemove(index)
@@ -929,10 +930,10 @@ export default {
},
handleSortMusicInfo(num) {
num = Math.min(num, this.list.length)
- this.sortList({
+ this.setMusicPosition({
id: this.listId,
- sortNum: num,
- musicInfos: this.selectdListDetailData.length ? [...this.selectdListDetailData] : [this.musicInfo],
+ position: num,
+ list: this.selectdListDetailData.length ? [...this.selectdListDetailData] : [this.musicInfo],
})
this.removeAllSelectListDetail()
this.isShowListSortModal = false
diff --git a/src/renderer/views/Setting.vue b/src/renderer/views/Setting.vue
index ada914d9..b914e924 100644
--- a/src/renderer/views/Setting.vue
+++ b/src/renderer/views/Setting.vue
@@ -150,6 +150,21 @@ div(:class="$style.main")
div
material-checkbox(id="setting_download_isDownloadLrc" v-model="current_setting.download.isDownloadLrc" :label="$t('view.setting.is_enable')")
+ dt#sync {{$t('view.setting.sync')}}
+ dd
+ material-checkbox(id="setting_sync_enable" v-model="current_setting.sync.enable" @change="handleSyncChange('enable')" :label="syncEnableTitle")
+ div
+ p.small {{$t('view.setting.sync_auth_code', { code: sync.status.code || '' })}}
+ p.small {{$t('view.setting.sync_address', { address: sync.status.address.join(', ') || '' })}}
+ p.small {{$t('view.setting.sync_device', { devices: syncDevices })}}
+ p
+ material-btn(:class="$style.btn" min :disabled="!current_setting.sync.enable" @click="handleRefreshSyncCode") {{$t('view.setting.sync_refresh_code')}}
+ dd
+ h3#sync_port {{$t('view.setting.sync_port')}}
+ div
+ p
+ material-input(:class="$style.gapLeft" v-model.trim="current_setting.sync.port" @change="handleSyncChange('port')" :placeholder="$t('view.setting.sync_port_tip')")
+
dt#hot_key {{$t('view.setting.hot_key')}}
dd
h3#hot_key_local_title {{$t('view.setting.hot_key_local_title')}}
@@ -295,7 +310,7 @@ import {
getSetting,
saveSetting,
} from '../utils'
-import { rendererSend, rendererInvoke, NAMES } from '@common/ipc'
+import { rendererSend, rendererInvoke, rendererOn, NAMES, rendererOff } from '@common/ipc'
import { mergeSetting, isMac } from '../../common/utils'
import apiSourceInfo from '../utils/music/api-source-info'
import fs from 'fs'
@@ -410,6 +425,21 @@ export default {
},
]
},
+ syncEnableTitle() {
+ let title = this.$t('view.setting.sync_enable')
+ if (this.sync.status.message) {
+ title += ` [${this.sync.status.message}]`
+ }
+ // else if (this.sync.status.address.length) {
+ // // title += ` [${this.sync.status.address.join(', ')}]`
+ // }
+ return title
+ },
+ syncDevices() {
+ return this.sync.status.devices.length
+ ? this.sync.status.devices.map(d => `${d.deviceName} (${d.clientId.substring(0, 5)})`).join(', ')
+ : ''
+ },
},
data() {
return {
@@ -474,6 +504,10 @@ export default {
isToTray: false,
themeId: 0,
},
+ sync: {
+ enable: false,
+ port: '23332',
+ },
windowSizeId: 1,
langId: 'cns',
themeId: 0,
@@ -608,6 +642,15 @@ export default {
},
isDisabledResourceCacheClear: false,
isDisabledListCacheClear: false,
+ sync: {
+ status: {
+ status: false,
+ message: '',
+ address: [],
+ code: '',
+ devices: [],
+ },
+ },
}
},
watch: {
@@ -665,6 +708,7 @@ export default {
window.eventHub.$off(eventBaseName.set_config, this.handleUpdateSetting)
window.eventHub.$off(eventBaseName.key_down, this.handleKeyDown)
window.eventHub.$off(eventBaseName.set_hot_key_config, this.handleUpdateHotKeyConfig)
+ this.syncUnInit()
if (this.current_setting.network.proxy.enable && !this.current_setting.network.proxy.host) window.globalObj.proxy.enable = false
},
@@ -684,6 +728,7 @@ export default {
this.current_hot_key = window.appHotKeyConfig
this.initHotKeyConfig()
this.getHotKeyStatus()
+ this.syncInit()
},
// initTOC() {
// const list = this.$refs.dom_setting_list.children
@@ -1151,6 +1196,42 @@ export default {
return status
},
+ setStatus(e, status) {
+ this.sync.status.status = status.status
+ this.sync.status.message = status.message
+ this.sync.status.address = status.address
+ this.sync.status.code = status.code
+ this.sync.status.devices = status.devices
+ },
+ syncInit() {
+ rendererInvoke(NAMES.mainWindow.sync_get_status).then(status => {
+ this.sync.status.status = status.status
+ this.sync.status.message = status.message
+ this.sync.status.address = status.address
+ this.sync.status.code = status.code
+ this.sync.status.devices = status.devices
+ })
+ rendererOn(NAMES.mainWindow.sync_status, this.setStatus)
+ },
+ syncUnInit() {
+ rendererOff(NAMES.mainWindow.sync_status, this.setStatus)
+ },
+ handleSyncChange(action) {
+ switch (action) {
+ case 'port':
+ if (!this.current_setting.sync.enable) return
+ case 'enable':
+ rendererInvoke(NAMES.mainWindow.sync_enable, {
+ enable: this.current_setting.sync.enable,
+ port: this.current_setting.sync.port,
+ })
+ window.globalObj.sync.enable = this.current_setting.sync.enable
+ break
+ }
+ },
+ handleRefreshSyncCode() {
+ rendererInvoke(NAMES.mainWindow.sync_generate_code)
+ },
},
}