From 4137362501e8dfc9edf5447fd46b560a8cfd25c1 Mon Sep 17 00:00:00 2001 From: MrKBear Date: Sun, 24 Apr 2022 20:16:03 +0800 Subject: [PATCH 1/5] Add drag layer --- source/Component/LoadFile/LoadFile.scss | 55 +++++++++++++++-------- source/Component/LoadFile/LoadFile.tsx | 58 ++++++++++++++++++++----- 2 files changed, 84 insertions(+), 29 deletions(-) diff --git a/source/Component/LoadFile/LoadFile.scss b/source/Component/LoadFile/LoadFile.scss index 5d29e0e..a1c769a 100644 --- a/source/Component/LoadFile/LoadFile.scss +++ b/source/Component/LoadFile/LoadFile.scss @@ -5,34 +5,51 @@ div.load-file-layer-root { z-index: 1000; width: 100%; height: 100%; - display: flex; - align-content: center; - justify-content: center; - align-items: center; - flex-wrap: wrap; + box-sizing: border-box; + padding: 20px; - div { - user-select: none; - text-align: center; + div.load-file-layer { width: 100%; - } + height: 100%; + display: flex; + align-content: center; + justify-content: center; + align-items: center; + flex-wrap: wrap; + border-radius: 3px; - div.drag-icon { - font-weight: 200; - font-size: 2.8em; - } + div { + pointer-events: none; + user-select: none; + text-align: center; + width: 100%; + } - div.drag-title { - margin-top: 5px; - margin-bottom: 5px; - font-size: 1.5em; + div.drag-icon { + font-weight: 200; + font-size: 2.8em; + } + + div.drag-title { + margin-top: 5px; + margin-bottom: 5px; + font-size: 1.5em; + } } } div.load-file-layer-root.light { - background-color: rgba($color: #FFFFFF, $alpha: .75); + background-color: rgba($color: #FFFFFF, $alpha: .6); + + div.load-file-layer { + border: 2px dashed $lt-font-color-normal-light; + } } div.load-file-layer-root.dark { - background-color: rgba($color: #000000, $alpha: .75); + background-color: rgba($color: #000000, $alpha: .6); + + div.load-file-layer { + border: 2px dashed $lt-font-color-normal-dark; + } } \ No newline at end of file diff --git a/source/Component/LoadFile/LoadFile.tsx b/source/Component/LoadFile/LoadFile.tsx index 432b6bc..981e304 100644 --- a/source/Component/LoadFile/LoadFile.tsx +++ b/source/Component/LoadFile/LoadFile.tsx @@ -4,27 +4,65 @@ import { Icon } from "@fluentui/react"; import { Component, ReactNode } from "react"; import "./LoadFile.scss"; -class LoadFile extends Component { +interface ILoadFileState { + show: boolean; +} + +class LoadFile extends Component<{}, ILoadFileState> { + + public state: Readonly = { + show: false + }; private renderMask() { return -
- -
-
- -
-
- +
+
+ +
+
+ +
+
+ +
; } + private offDragTimer: NodeJS.Timeout | undefined; + + private handleWindowsDragOnFiles = () => { + clearTimeout(this.offDragTimer as number | undefined); + this.setState({ + show: true + }); + } + + private handleWindowsDragOffFiles = () => { + clearTimeout(this.offDragTimer as number | undefined); + this.offDragTimer = setTimeout(() => { + this.setState({ + show: false + }); + }); + } + + public componentDidMount() { + window.addEventListener("dragenter", this.handleWindowsDragOnFiles); + } + + public componentWillUnmount() { + window.removeEventListener("dragenter", this.handleWindowsDragOnFiles); + } + public render(): ReactNode { - return <>; + return this.state.show ? this.renderMask() : null; } } From 8a272a96267c08f1d23994d8996f823fc3bc3227 Mon Sep 17 00:00:00 2001 From: MrKBear Date: Mon, 25 Apr 2022 09:57:46 +0800 Subject: [PATCH 2/5] Set new file default save state true --- source/Model/Archive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Model/Archive.ts b/source/Model/Archive.ts index c4328e4..ed044d1 100644 --- a/source/Model/Archive.ts +++ b/source/Model/Archive.ts @@ -37,7 +37,7 @@ class Archive extends Emitter { /** * 是否保存 */ - public isSaved: boolean = false; + public isSaved: boolean = true; /** * 文件路径 From 0ab53c1800d033d9eaef3c75a8c072d09be044e2 Mon Sep 17 00:00:00 2001 From: MrKBear Date: Mon, 25 Apr 2022 21:32:50 +0800 Subject: [PATCH 3/5] Add file load UI --- config/webpack.common.js | 4 + package-lock.json | 190 ++++++++++++++++- package.json | 2 + .../Component/ConfirmPopup/ConfirmPopup.tsx | 7 +- source/Component/LoadFile/LoadFile.scss | 7 +- source/Component/LoadFile/LoadFile.tsx | 191 ++++++++++++------ source/Context/Status.tsx | 5 + source/Localization/EN-US.ts | 7 + source/Localization/ZH-CN.ts | 7 + source/Model/Archive.ts | 9 +- source/Page/SimulatorWeb/SimulatorWeb.tsx | 137 ++----------- 11 files changed, 372 insertions(+), 194 deletions(-) diff --git a/config/webpack.common.js b/config/webpack.common.js index 907e2b4..823093f 100644 --- a/config/webpack.common.js +++ b/config/webpack.common.js @@ -123,6 +123,10 @@ const resolve = (plugins = []) => { let res = { extensions: [ ".tsx", '.ts', '.js' ], + fallback: { + 'react/jsx-runtime': 'react/jsx-runtime.js', + 'react/jsx-dev-runtime': 'react/jsx-dev-runtime.js', + }, plugins: plugins }; diff --git a/package-lock.json b/package-lock.json index c38adc1..b8eb1b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ "express": "^4.17.3", "gl-matrix": "^3.4.3", "react": "^17.0.2", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^17.0.2", "uuid": "^8.3.2" }, @@ -726,6 +728,17 @@ "node": ">=4" } }, + "node_modules/@babel/runtime": { + "version": "7.17.9", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.17.9.tgz", + "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz", @@ -1023,6 +1036,21 @@ "node": ">=10" } }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, "node_modules/@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmmirror.com/@sindresorhus/is/-/is-0.14.0.tgz", @@ -1195,7 +1223,7 @@ "version": "17.0.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.21.tgz", "integrity": "sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==", - "dev": true + "devOptional": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -2817,6 +2845,16 @@ "node": ">=8" } }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmmirror.com/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, "node_modules/dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -3317,8 +3355,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.2.11", @@ -3945,6 +3982,14 @@ "he": "bin/he" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -6164,6 +6209,43 @@ "node": ">=0.10.0" } }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmmirror.com/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmmirror.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -6177,6 +6259,11 @@ "react": "17.0.2" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -6305,6 +6392,19 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/redux/-/redux-4.2.0.tgz", + "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, "node_modules/regexp.prototype.flags": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz", @@ -8767,6 +8867,14 @@ } } }, + "@babel/runtime": { + "version": "7.17.9", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.17.9.tgz", + "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, "@discoveryjs/json-ext": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz", @@ -9013,6 +9121,21 @@ "rimraf": "^3.0.2" } }, + "@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmmirror.com/@sindresorhus/is/-/is-0.14.0.tgz", @@ -9176,7 +9299,7 @@ "version": "17.0.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.21.tgz", "integrity": "sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==", - "dev": true + "devOptional": true }, "@types/normalize-package-data": { "version": "2.4.1", @@ -10488,6 +10611,16 @@ "path-type": "^4.0.0" } }, + "dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmmirror.com/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "requires": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -10905,8 +11038,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { "version": "3.2.11", @@ -11384,6 +11516,14 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -13051,6 +13191,26 @@ "object-assign": "^4.1.1" } }, + "react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmmirror.com/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "requires": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + } + }, + "react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmmirror.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "requires": { + "dnd-core": "^16.0.1" + } + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -13061,6 +13221,11 @@ "scheduler": "^0.20.2" } }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -13163,6 +13328,19 @@ "strip-indent": "^3.0.0" } }, + "redux": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/redux/-/redux-4.2.0.tgz", + "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, "regexp.prototype.flags": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz", diff --git a/package.json b/package.json index 9ccdc33..67b5b24 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,8 @@ "express": "^4.17.3", "gl-matrix": "^3.4.3", "react": "^17.0.2", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^17.0.2", "uuid": "^8.3.2" } diff --git a/source/Component/ConfirmPopup/ConfirmPopup.tsx b/source/Component/ConfirmPopup/ConfirmPopup.tsx index 98ca844..b27e56a 100644 --- a/source/Component/ConfirmPopup/ConfirmPopup.tsx +++ b/source/Component/ConfirmPopup/ConfirmPopup.tsx @@ -9,6 +9,7 @@ interface IConfirmPopupProps { titleI18N?: AllI18nKeys; titleI18NOption?: Record; infoI18n?: AllI18nKeys; + infoI18nOption?: Record; yesI18n?: AllI18nKeys; noI18n?: AllI18nKeys; renderInfo?: () => ReactNode; @@ -64,8 +65,10 @@ class ConfirmPopup extends Popup { this.props.renderInfo ? this.props.renderInfo() : this.props.infoI18n ? - : - null + : null } } diff --git a/source/Component/LoadFile/LoadFile.scss b/source/Component/LoadFile/LoadFile.scss index a1c769a..06eaaf0 100644 --- a/source/Component/LoadFile/LoadFile.scss +++ b/source/Component/LoadFile/LoadFile.scss @@ -1,10 +1,16 @@ @import "../Theme/Theme.scss"; +div.load-file-app-root { + width: 100%; + height: 100%; +} + div.load-file-layer-root { position: fixed; z-index: 1000; width: 100%; height: 100%; + pointer-events: none; box-sizing: border-box; padding: 20px; @@ -19,7 +25,6 @@ div.load-file-layer-root { border-radius: 3px; div { - pointer-events: none; user-select: none; text-align: center; width: 100%; diff --git a/source/Component/LoadFile/LoadFile.tsx b/source/Component/LoadFile/LoadFile.tsx index 981e304..fdb7ed4 100644 --- a/source/Component/LoadFile/LoadFile.tsx +++ b/source/Component/LoadFile/LoadFile.tsx @@ -1,69 +1,142 @@ +import { ConfirmPopup } from "@Component/ConfirmPopup/ConfirmPopup"; import { Localization } from "@Component/Localization/Localization"; import { FontLevel, Theme } from "@Component/Theme/Theme"; +import { Status, useStatus, IMixinStatusProps } from "@Context/Status"; import { Icon } from "@fluentui/react"; -import { Component, ReactNode } from "react"; +import { FunctionComponent } from "react"; +import { useDrop } from 'react-dnd' +import { NativeTypes } from "react-dnd-html5-backend" import "./LoadFile.scss"; -interface ILoadFileState { - show: boolean; -} +const DragFileMask: FunctionComponent = () => { -class LoadFile extends Component<{}, ILoadFileState> { - - public state: Readonly = { - show: false - }; - - private renderMask() { - return -
-
- -
-
- -
-
- -
+ return +
+
+
- ; - } - - private offDragTimer: NodeJS.Timeout | undefined; - - private handleWindowsDragOnFiles = () => { - clearTimeout(this.offDragTimer as number | undefined); - this.setState({ - show: true - }); - } - - private handleWindowsDragOffFiles = () => { - clearTimeout(this.offDragTimer as number | undefined); - this.offDragTimer = setTimeout(() => { - this.setState({ - show: false - }); - }); - } - - public componentDidMount() { - window.addEventListener("dragenter", this.handleWindowsDragOnFiles); - } - - public componentWillUnmount() { - window.removeEventListener("dragenter", this.handleWindowsDragOnFiles); - } - - public render(): ReactNode { - return this.state.show ? this.renderMask() : null; - } +
+ +
+
+ +
+
+
; } +async function fileChecker(status: Status, file?: File) { + + if (!status) return undefined; + + return new Promise((r, j) => { + + // 检查文件存在性 + if (!file) { + status.popup.showPopup(ConfirmPopup, { + infoI18n: "Popup.Load.Save.Error.Empty", + titleI18N: "Popup.Load.Save.Title", + yesI18n: "Popup.Load.Save.confirm" + }); + return j(); + } + + // 检测拓展名 + let extendName = (file.name.match(/\.(\w+)$/) ?? [])[1]; + if (extendName !== "ltss") { + status.popup.showPopup(ConfirmPopup, { + infoI18n: "Popup.Load.Save.Error.Type", + infoI18nOption: { ext: extendName }, + titleI18N: "Popup.Load.Save.Title", + yesI18n: "Popup.Load.Save.confirm" + }); + return j(); + } + + // 文件读取 + let fileReader = new FileReader(); + fileReader.readAsText(file); + fileReader.onload = () => { + + const loadFunc = () => { + + // 进行转换 + let errorMessage = status.archive.load(status.model, fileReader.result as string, file.name); + if (errorMessage) { + status.popup.showPopup(ConfirmPopup, { + infoI18n: "Popup.Load.Save.Error.Parse", + infoI18nOption: { why: errorMessage }, + titleI18N: "Popup.Load.Save.Title", + yesI18n: "Popup.Load.Save.confirm" + }); + j(); + } + + else { + r(undefined); + } + } + + // 如果保存进行提示 + if (!status.archive.isSaved) { + status.popup.showPopup(ConfirmPopup, { + infoI18n: "Popup.Load.Save.Overwrite.Info", + titleI18N: "Popup.Load.Save.Title", + yesI18n: "Popup.Load.Save.Overwrite", + noI18n: "Popup.Action.No", + red: "yes", + yes: () => { + loadFunc(); + }, + no: () => { + j(); + } + }); + } + + else { + loadFunc(); + } + } + fileReader.onerror = () => { + status.popup.showPopup(ConfirmPopup, { + infoI18n: "Popup.Load.Save.Error.Parse", + infoI18nOption: { why: "Unknown error" }, + titleI18N: "Popup.Load.Save.Title", + yesI18n: "Popup.Load.Save.confirm" + }); + j(); + } + }); +} + +const LoadFileView: FunctionComponent = (props) => { + + const [{ isOver }, drop] = useDrop(() => ({ + accept: NativeTypes.FILE, + drop: (item: { files: File[] }) => { + if (props.status) { + fileChecker(props.status, item.files[0]).catch((e) => undefined); + } + }, + collect: (monitor) => ({ + isOver: monitor.isOver() + }) + })); + + return <> + { + isOver ? : null + } +
+ {props.children} +
+ +} + +const LoadFile = useStatus(LoadFileView); + export { LoadFile }; \ No newline at end of file diff --git a/source/Context/Status.tsx b/source/Context/Status.tsx index 0d702ca..0a7d1d8 100644 --- a/source/Context/Status.tsx +++ b/source/Context/Status.tsx @@ -161,6 +161,11 @@ class Status extends Emitter { this.emit("labelChange"); this.emit("behaviorChange"); + // 清除焦点对象 + this.setBehaviorObject(); + this.setFocusObject(new Set()); + this.setLabelObject(); + // 映射 this.emit("fileLoad"); }); diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index e8e756b..bde9d05 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -65,6 +65,13 @@ const EN_US = { "Popup.Delete.Behavior.Confirm": "Are you sure you want to delete this behavior? The behavior is deleted and cannot be recalled.", "Popup.Restore.Behavior.Confirm": "Are you sure you want to reset all parameters of this behavior? This operation cannot be recalled.", "Popup.Setting.Title": "Preferences setting", + "Popup.Load.Save.Title": "Load save", + "Popup.Load.Save.confirm": "Got it", + "Popup.Load.Save.Overwrite": "Overwrite and continue", + "Popup.Load.Save.Overwrite.Info": "The current workspace will be overwritten after the archive is loaded, and all unsaved progress will be lost. Are you sure you want to continue?", + "Popup.Load.Save.Error.Empty": "File information acquisition error. The file has been lost or moved.", + "Popup.Load.Save.Error.Type": "The file with extension name \"{ext}\" cannot be loaded temporarily", + "Popup.Load.Save.Error.Parse": "Archive parsing error, detailed reason: \n{why}", "Popup.Add.Behavior.Title": "Add behavior", "Popup.Add.Behavior.Action.Add": "Add all select behavior", "Popup.Add.Behavior.Select.Counter": "Selected {count} behavior", diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index 6b24c28..fb9de81 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -65,6 +65,13 @@ const ZH_CN = { "Popup.Delete.Behavior.Confirm": "你确定要删除这个行为吗?行为被删除将无法撤回。", "Popup.Restore.Behavior.Confirm": "你确定要重置此行为的全部参数吗?此操作无法撤回。", "Popup.Setting.Title": "首选项设置", + "Popup.Load.Save.Title": "加载存档", + "Popup.Load.Save.confirm": "我知道了", + "Popup.Load.Save.Overwrite": "覆盖并继续", + "Popup.Load.Save.Overwrite.Info": "存档加载后将覆盖当前工作区,未保存的进度将全部丢失,确定要继续吗?", + "Popup.Load.Save.Error.Empty": "文件信息获取错误,文件已丢失或已被移动", + "Popup.Load.Save.Error.Type": "暂时无法加载拓展名为 \"{ext}\" 的文件", + "Popup.Load.Save.Error.Parse": "存档解析错误,详细原因: \n{why}", "Popup.Add.Behavior.Title": "添加行为", "Popup.Add.Behavior.Action.Add": "添加全部选中行为", "Popup.Add.Behavior.Select.Counter": "已选择 {count} 个行为", diff --git a/source/Model/Archive.ts b/source/Model/Archive.ts index ed044d1..8c54b59 100644 --- a/source/Model/Archive.ts +++ b/source/Model/Archive.ts @@ -100,7 +100,7 @@ class Archive extends Emitter { // 解析为 JSON 对象 const archive: IArchiveObject = JSON.parse(data); - console.log(archive); + // console.log(archive); // 实例化全部对象 const objectPool: CtrlObject[] = []; @@ -254,7 +254,7 @@ class Archive extends Emitter { * 加载文件为模型 * @return Model */ - public load(model: Model, data: string): string | undefined { + public load(model: Model, data: string, name: string, url?: string): string | undefined { try { this.loadArchiveIntoModel(model, data); @@ -262,8 +262,11 @@ class Archive extends Emitter { return e as string; } - this.isSaved = true; this.emit("fileLoad", this); + this.fileName = name; + this.isSaved = true; + this.isNewFile = false; + this.emit("fileSave", this); }; public constructor() { diff --git a/source/Page/SimulatorWeb/SimulatorWeb.tsx b/source/Page/SimulatorWeb/SimulatorWeb.tsx index 53271a4..255b201 100644 --- a/source/Page/SimulatorWeb/SimulatorWeb.tsx +++ b/source/Page/SimulatorWeb/SimulatorWeb.tsx @@ -1,4 +1,6 @@ import { Component, ReactNode } from "react"; +import { DndProvider } from 'react-dnd' +import { HTML5Backend } from 'react-dnd-html5-backend' import { SettingProvider, Setting, Platform } from "@Context/Setting"; import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme"; import { StatusProvider, Status } from "@Context/Status"; @@ -7,12 +9,10 @@ import { initializeIcons } from '@fluentui/font-icons-mdl2'; import { RootContainer } from "@Component/Container/RootContainer"; import { LayoutDirection } from "@Context/Layout"; import { LoadFile } from "@Component/LoadFile/LoadFile"; -import { AllBehaviors, getBehaviorById } from "@Behavior/Behavior"; import { CommandBar } from "@Component/CommandBar/CommandBar"; import { HeaderBar } from "@Component/HeaderBar/HeaderBar"; import { Popup } from "@Component/Popup/Popup"; import { Entry } from "../Entry/Entry"; -import { Group } from "@Model/Group"; import "./SimulatorWeb.scss"; initializeIcons("https://img.mrkbear.com/fabric-cdn-prod_20210407.001/"); @@ -42,118 +42,6 @@ class SimulatorWeb extends Component { this.status.bindRenderer(classicRender); this.status.setting = this.setting; - const randomPosition = (group: Group) => { - group.individuals.forEach((individual) => { - individual.position[0] = (Math.random() - .5) * 2; - individual.position[1] = (Math.random() - .5) * 2; - individual.position[2] = (Math.random() - .5) * 2; - }) - }; - - // 测试代码 - if (false) { - let group = this.status.newGroup(); - let range = this.status.newRange(); - range.color = [.1, .5, .9]; - group.new(100); - group.color = [.8, .1, .6]; - randomPosition(group); - this.status.model.update(0); - this.status.newLabel().name = "New Label"; - this.status.newLabel().name = "Test Label 01"; - let template = this.status.model.addBehavior(AllBehaviors[0]); - template.name = "Template"; template.color = [150, 20, 220]; - let dynamic = this.status.model.addBehavior(AllBehaviors[1]); - dynamic.name = "Dynamic"; dynamic.color = [250, 200, 80]; - let brownian = this.status.model.addBehavior(AllBehaviors[2]); - brownian.name = "Brownian"; brownian.color = [200, 80, 250]; - let boundary = this.status.model.addBehavior(AllBehaviors[3]); - boundary.name = "Boundary"; boundary.color = [80, 200, 250]; - boundary.parameter.range.picker = this.status.model.allRangeLabel; - group.addBehavior(template); - group.addBehavior(dynamic); - group.addBehavior(brownian); - group.addBehavior(boundary); - } - - // 鱼群模型测试 - if (false) { - let fish1 = this.status.newGroup(); - let fish2 = this.status.newGroup(); - let shark = this.status.newGroup(); - let range = this.status.newRange(); - - range.displayName = "Experimental site"; - range.color = [.8, .1, .6]; - - fish1.new(100); - fish1.displayName = "Fish A"; - fish1.color = [.1, .5, .9]; - randomPosition(fish1); - - fish2.new(50); - fish2.displayName = "Fish B"; - fish2.color = [.3, .2, .9]; - randomPosition(fish2); - - shark.new(3); - shark.displayName = "Shark"; - shark.color = [.8, .2, .3]; - shark.renderParameter.size = 100; - shark.renderParameter.shape = "5"; - randomPosition(shark); - - this.status.model.update(0); - let fishLabel = this.status.newLabel(); - fishLabel.name = "Fish"; - fish1.addLabel(fishLabel); - fish2.addLabel(fishLabel); - - let template = this.status.model.addBehavior(getBehaviorById("Template")); - template.name = "Template"; template.color = [150, 20, 220]; - - let dynamicFish = this.status.model.addBehavior(getBehaviorById("PhysicsDynamics")); - dynamicFish.name = "Dynamic Fish"; dynamicFish.color = [250, 200, 80]; - - let dynamicShark = this.status.model.addBehavior(getBehaviorById("PhysicsDynamics")); - dynamicShark.name = "Dynamic Shark"; dynamicShark.color = [250, 200, 80]; - dynamicShark.parameter.maxAcceleration = 8.5; - dynamicShark.parameter.maxVelocity = 15.8; - dynamicShark.parameter.resistance = 3.6; - - let brownian = this.status.model.addBehavior(getBehaviorById("Brownian")); - brownian.name = "Brownian"; brownian.color = [200, 80, 250]; - - let boundary = this.status.model.addBehavior(getBehaviorById("BoundaryConstraint")); - boundary.name = "Boundary"; boundary.color = [80, 200, 250]; - boundary.parameter.range.picker = this.status.model.allRangeLabel; - - let tracking = this.status.model.addBehavior(getBehaviorById("Tracking")); - tracking.name = "Tracking"; tracking.color = [80, 200, 250]; - tracking.parameter.target.picker = fishLabel; - - let attacking = this.status.model.addBehavior(getBehaviorById("ContactAttacking")); - attacking.name = "Contact Attacking"; attacking.color = [120, 100, 250]; - attacking.parameter.target.picker = fishLabel; - - fish1.addBehavior(dynamicFish); - fish1.addBehavior(brownian); - fish1.addBehavior(boundary); - - fish2.addBehavior(dynamicFish); - fish2.addBehavior(brownian); - fish2.addBehavior(boundary); - - shark.addBehavior(dynamicShark); - shark.addBehavior(boundary); - shark.addBehavior(tracking); - shark.addBehavior(attacking); - - setTimeout(() => { - this.status.model.updateBehaviorParameter(); - }, 200) - } - (window as any).LT = { status: this.status, setting: this.setting @@ -193,7 +81,9 @@ class SimulatorWeb extends Component { public render(): ReactNode { return - {this.renderContent()} + + {this.renderContent()} + } @@ -204,15 +94,16 @@ class SimulatorWeb extends Component { backgroundLevel={BackgroundLevel.Level5} fontLevel={FontLevel.Level3} > - - -
- - -
+ + +
+ + +
+
} } From ec3aaa5e3ae1cfb7f75f21962484499e0ce4bda9 Mon Sep 17 00:00:00 2001 From: MrKBear Date: Tue, 26 Apr 2022 15:17:24 +0800 Subject: [PATCH 4/5] Add desktop save function --- source/Context/Archive.tsx | 48 +++++++++++++++-- source/Context/Electron.ts | 22 +++++++- source/Electron/Electron.ts | 54 ++++++++++++++++++- source/Electron/SimulatorAPI.ts | 6 +++ source/Electron/SimulatorWindow.ts | 45 ++++++---------- source/Localization/EN-US.ts | 3 ++ source/Localization/ZH-CN.ts | 3 ++ .../SimulatorDesktop/SimulatorDesktop.tsx | 17 +----- 8 files changed, 147 insertions(+), 51 deletions(-) diff --git a/source/Context/Archive.tsx b/source/Context/Archive.tsx index cef0fe8..8e27be2 100644 --- a/source/Context/Archive.tsx +++ b/source/Context/Archive.tsx @@ -1,8 +1,9 @@ import { FunctionComponent, useEffect } from "react"; -import * as download from "downloadjs"; import { useSetting, IMixinSettingProps, Platform } from "@Context/Setting"; import { useStatus, IMixinStatusProps } from "@Context/Status"; +import { useElectron, IMixinElectronProps } from "@Context/Electron"; import { I18N } from "@Component/Localization/Localization"; +import * as download from "downloadjs"; interface IFileInfo { fileName: string; @@ -21,14 +22,14 @@ interface ICallBackProps { then: () => any; } -const ArchiveSaveDownloadView: FunctionComponent = function ArchiveSave(props) { +const ArchiveSaveDownloadView: FunctionComponent = function ArchiveSaveDownloadView(props) { const runner = async () => { const file = await props.fileData(); setTimeout(() => { download(file, props.fileName, "text/json"); props.then(); - }, 100); + }, 10); } useEffect(() => { runner() }, []); @@ -38,6 +39,45 @@ const ArchiveSaveDownloadView: FunctionComponent = f const ArchiveSaveDownload = ArchiveSaveDownloadView; +const ArchiveSaveFsView: FunctionComponent = +function ArchiveSaveFsView(props) { + + const runner = async () => { + const file = await props.fileData(); + if (props.electron) { + props.electron.fileSave( + file, + I18N(props, "Popup.Load.Save.Select.File.Name"), + I18N(props, "Popup.Load.Save.Select.Path.Title"), + I18N(props, "Popup.Load.Save.Select.Path.Button"), + props.fileUrl + ); + } + } + + const saveEvent = ({name, url, success} : {name: string, url: string, success: boolean}) => { + if (success && props.status) { + props.status.archive.fileUrl = url; + props.status.archive.fileName = name; + props.status.archive.isNewFile = false; + props.status.archive.emit("fileSave", props.status.archive); + } + props.then(); + } + + useEffect(() => { + runner(); + props.electron?.on("fileSave", saveEvent); + return () => { + props.electron?.off("fileSave", saveEvent); + }; + }, []); + + return <>; +} + +const ArchiveSaveFs = useSetting(useElectron(useStatus(ArchiveSaveFsView))); + /** * 保存存档文件 */ @@ -81,7 +121,7 @@ const ArchiveSaveView: FunctionComponent : - <> + } } diff --git a/source/Context/Electron.ts b/source/Context/Electron.ts index ad7787a..76147fa 100644 --- a/source/Context/Electron.ts +++ b/source/Context/Electron.ts @@ -1,4 +1,5 @@ import { createContext } from "react"; +import { Emitter } from "@Model/Emitter"; import { superConnect, superConnectWithEvent } from "@Context/Context"; import { ISimulatorAPI, IApiEmitterEvent } from "@Electron/SimulatorAPI"; @@ -6,6 +7,25 @@ interface IMixinElectronProps { electron?: ISimulatorAPI; } +const getElectronAPI: () => ISimulatorAPI = () => { + const API = (window as any).API; + const mapperEmitter = new Emitter(); + const ClassElectron: new () => ISimulatorAPI = function (this: Record) { + this.resetAll = () => mapperEmitter.resetAll(); + this.reset = (type: string) => mapperEmitter.reset(type); + this.on = (type: string, handel: any) => mapperEmitter.on(type, handel); + this.off = (type: string, handel: any) => mapperEmitter.off(type, handel); + this.emit = (type: string, data: any) => mapperEmitter.emit(type, data); + } as any; + ClassElectron.prototype = API; + + // Emitter Mapper + API.mapEmit((...p: any) => { + mapperEmitter.emit(...p); + }); + return new ClassElectron(); +} + const ElectronContext = createContext((window as any).API ?? {} as ISimulatorAPI); ElectronContext.displayName = "Electron"; @@ -19,4 +39,4 @@ const useElectron = superConnect(ElectronConsumer, "electron"); const useElectronWithEvent = superConnectWithEvent(ElectronConsumer, "electron"); -export { useElectron, ElectronProvider, IMixinElectronProps, ISimulatorAPI, useElectronWithEvent }; \ No newline at end of file +export { useElectron, ElectronProvider, IMixinElectronProps, ISimulatorAPI, useElectronWithEvent, getElectronAPI }; \ No newline at end of file diff --git a/source/Electron/Electron.ts b/source/Electron/Electron.ts index e62db7a..3132ab1 100644 --- a/source/Electron/Electron.ts +++ b/source/Electron/Electron.ts @@ -1,6 +1,7 @@ -import { app, BrowserWindow, ipcMain } from "electron"; +import { app, BrowserWindow, ipcMain, dialog } from "electron"; import { Service } from "@Service/Service"; import { join as pathJoin } from "path"; +import { writeFile } from "fs"; const ENV = process.env ?? {}; class ElectronApp { @@ -55,6 +56,7 @@ class ElectronApp { this.simulatorWindow.loadURL(this.serviceUrl + (ENV.LIVING_TOGETHER_WEB_PATH ?? "/resources/app.asar/")); this.handelSimulatorWindowBehavior(); + this.handelFileChange(); app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() @@ -90,6 +92,56 @@ class ElectronApp { this.simulatorWindow?.on("maximize", sendWindowsChangeMessage); this.simulatorWindow?.on("unmaximize", sendWindowsChangeMessage); } + + private handelFileChange() { + + // 文件保存 + const saveFile = async (path: string, text: string) => { + return new Promise((r) => { + writeFile(path ?? "", text, {}, (e) => { + this.simulatorWindow?.webContents.send( + "windows.EndFileSave", + (path.match(/.+(\/|\\)(.+)$/) ?? [])[2], + path, !e + ); + r(undefined); + }); + }) + }; + + // 处理文件保存事件 + ipcMain.on("windows.fileSave", + (_, text: string, name: string, title: string, button: string, url?: string) => { + + // 如果没有路径,询问新的路径 + if (url) { + saveFile(url, text); + } + + // 询问保存位置 + else { + dialog.showSaveDialog(this.simulatorWindow!, { + title: title, + buttonLabel: button, + filters: [ + { name: name, extensions: ["ltss"] } + ] + }).then(res => { + + // 用户选择后继续保存 + if (!res.canceled && res.filePath) { + saveFile(res.filePath, text); + } else { + this.simulatorWindow?.webContents.send( + "windows.EndFileSave", + undefined, undefined, false + ); + } + }); + } + } + ); + } } new ElectronApp().runMainThread(); \ No newline at end of file diff --git a/source/Electron/SimulatorAPI.ts b/source/Electron/SimulatorAPI.ts index 6581821..cfeeea9 100644 --- a/source/Electron/SimulatorAPI.ts +++ b/source/Electron/SimulatorAPI.ts @@ -2,6 +2,7 @@ import { Emitter } from "@Model/Emitter"; type IApiEmitterEvent = { windowsSizeStateChange: void; + fileSave: {success: boolean, name: string, url: string}; } interface ISimulatorAPI extends Emitter { @@ -30,6 +31,11 @@ interface ISimulatorAPI extends Emitter { * 是否处于最大化状态 */ minimize: () => void; + + /** + * 存档 + */ + fileSave: (text: string, name: string, title: string, button: string, url?: string) => void; } export { ISimulatorAPI, IApiEmitterEvent } \ No newline at end of file diff --git a/source/Electron/SimulatorWindow.ts b/source/Electron/SimulatorWindow.ts index 4012366..66a5430 100644 --- a/source/Electron/SimulatorWindow.ts +++ b/source/Electron/SimulatorWindow.ts @@ -1,23 +1,11 @@ import { contextBridge, ipcRenderer } from "electron"; -import { ISimulatorAPI } from "@Electron/SimulatorAPI" +import { ISimulatorAPI } from "@Electron/SimulatorAPI"; -const emitterMap: Array<[key: string, value: Function[]]> = []; -const queryEmitter = (key: string) => { - let res: (typeof emitterMap)[0] | undefined; - emitterMap.forEach((item) => { - if (item[0] === key) res = item; - }); +const emitterMap: { fn?: Function } = { fn: undefined }; - if (res) { - if (Array.isArray(res[1])) return res[1]; - res[1] = []; - return res[1]; - } - - else { - res = [key, []]; - emitterMap.push(res); - return res[1]; +const emit = (type: string, evt?: any) => { + if (emitterMap.fn) { + emitterMap.fn(type, evt); } } @@ -43,22 +31,19 @@ const API: ISimulatorAPI = { ipcRenderer.send("windows.minimize"); }, - all: new Map() as any, - - resetAll: () => emitterMap.splice(0), - reset: (type) => queryEmitter(type).splice(0), - on: (type, handler) => queryEmitter(type).push(handler), - off: (type, handler) => { - const handlers = queryEmitter(type); - handlers.splice(handlers.indexOf(handler!) >>> 0, 1); + fileSave(text: string, name: string, title: string, button: string, url?: string) { + ipcRenderer.send("windows.fileSave", text, name, title, button, url); }, - emit: ((type: string, evt: any) => { - queryEmitter(type).slice().map((handler: any) => { handler(evt) }); - }) as any, -} + + mapEmit: (fn: Function) => { emitterMap.fn = fn }, +} as any; ipcRenderer.on("windows.windowsSizeStateChange", () => { - API.emit("windowsSizeStateChange"); + emit("windowsSizeStateChange"); +}); + +ipcRenderer.on("windows.EndFileSave", (_, name: string, url: string, success: boolean) => { + emit("fileSave", {name, url, success}); }); contextBridge.exposeInMainWorld("API", API); \ No newline at end of file diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index bde9d05..2138ff9 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -72,6 +72,9 @@ const EN_US = { "Popup.Load.Save.Error.Empty": "File information acquisition error. The file has been lost or moved.", "Popup.Load.Save.Error.Type": "The file with extension name \"{ext}\" cannot be loaded temporarily", "Popup.Load.Save.Error.Parse": "Archive parsing error, detailed reason: \n{why}", + "Popup.Load.Save.Select.Path.Title": "Please select an archive location", + "Popup.Load.Save.Select.Path.Button": "Save", + "Popup.Load.Save.Select.File.Name": "Living Together Simulator Save", "Popup.Add.Behavior.Title": "Add behavior", "Popup.Add.Behavior.Action.Add": "Add all select behavior", "Popup.Add.Behavior.Select.Counter": "Selected {count} behavior", diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index fb9de81..1ee7e39 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -72,6 +72,9 @@ const ZH_CN = { "Popup.Load.Save.Error.Empty": "文件信息获取错误,文件已丢失或已被移动", "Popup.Load.Save.Error.Type": "暂时无法加载拓展名为 \"{ext}\" 的文件", "Popup.Load.Save.Error.Parse": "存档解析错误,详细原因: \n{why}", + "Popup.Load.Save.Select.Path.Title": "请选择存档保存位置", + "Popup.Load.Save.Select.Path.Button": "保存", + "Popup.Load.Save.Select.File.Name": "群生共进存档", "Popup.Add.Behavior.Title": "添加行为", "Popup.Add.Behavior.Action.Add": "添加全部选中行为", "Popup.Add.Behavior.Select.Counter": "已选择 {count} 个行为", diff --git a/source/Page/SimulatorDesktop/SimulatorDesktop.tsx b/source/Page/SimulatorDesktop/SimulatorDesktop.tsx index b1f74a7..62ba58c 100644 --- a/source/Page/SimulatorDesktop/SimulatorDesktop.tsx +++ b/source/Page/SimulatorDesktop/SimulatorDesktop.tsx @@ -3,7 +3,7 @@ import { SettingProvider, Setting, Platform } from "@Context/Setting"; import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme"; import { ISimulatorAPI } from "@Electron/SimulatorAPI"; import { StatusProvider, Status } from "@Context/Status"; -import { ElectronProvider } from "@Context/Electron"; +import { ElectronProvider, getElectronAPI } from "@Context/Electron"; import { ClassicRenderer } from "@GLRender/ClassicRenderer"; import { initializeIcons } from '@fluentui/font-icons-mdl2'; import { RootContainer } from "@Component/Container/RootContainer"; @@ -47,25 +47,12 @@ class SimulatorDesktop extends Component { this.status.bindRenderer(classicRender); this.status.setting = this.setting; - const randomPosition = (group: Group) => { - group.individuals.forEach((individual) => { - individual.position[0] = (Math.random() - .5) * 2; - individual.position[1] = (Math.random() - .5) * 2; - individual.position[2] = (Math.random() - .5) * 2; - }) - }; - (window as any).LT = { status: this.status, setting: this.setting }; - this.electron = {} as ISimulatorAPI; - if ((window as any).API) { - this.electron = (window as any).API; - } else { - console.error("SimulatorDesktop: Can't find electron API"); - } + this.electron = getElectronAPI(); } public componentDidMount() { From 23ebbeb12014b9741b2fde8eee3b2e587b7c1c9c Mon Sep 17 00:00:00 2001 From: MrKBear Date: Tue, 26 Apr 2022 16:42:02 +0800 Subject: [PATCH 5/5] Rebuild commond bar component --- source/Component/CommandBar/CommandBar.scss | 43 ++++-- source/Component/CommandBar/CommandBar.tsx | 131 ++++++++++-------- source/Component/LoadFile/LoadFile.tsx | 2 +- source/Context/Archive.tsx | 22 +-- source/Model/Archive.ts | 1 + .../SimulatorDesktop/SimulatorDesktop.tsx | 24 ++-- 6 files changed, 133 insertions(+), 90 deletions(-) diff --git a/source/Component/CommandBar/CommandBar.scss b/source/Component/CommandBar/CommandBar.scss index dc36908..9cfee02 100644 --- a/source/Component/CommandBar/CommandBar.scss +++ b/source/Component/CommandBar/CommandBar.scss @@ -1,3 +1,5 @@ +@import "../Theme/Theme.scss"; + div.command-bar { height: 100%; user-select: none; @@ -5,32 +7,53 @@ div.command-bar { flex-direction: column; justify-content: space-between; - button.ms-Button.command-button { + div.command-button { width: 100%; - text-align: center; display: flex; justify-content: center; + align-items: center; transition: all 100ms ease-in-out; - color: inherit; + cursor: pointer; - span.ms-Button-flexContainer i.ms-Icon { - font-size: 25px; + i { + font-size: 22px; + } + + div.command-button-loading { + + div.ms-Spinner-circle { + border-width: 2px; + } } } - button.ms-Button.command-button.on-end { + div.command-button.on-end { align-self: flex-end; } } -div.command-bar.dark button.ms-Button.command-button.active, -div.command-bar.dark button.ms-Button.command-button:hover { +div.command-bar.dark div.command-button div.command-button-loading div.ms-Spinner-circle { + border-top-color: rgba($color: #FFFFFF, $alpha: .9); + border-left-color: rgba($color: #FFFFFF, $alpha: .4); + border-bottom-color: rgba($color: #FFFFFF, $alpha: .4); + border-right-color: rgba($color: #FFFFFF, $alpha: .4); +} + +div.command-bar.light div.command-button div.command-button-loading div.ms-Spinner-circle { + border-top-color: rgba($color: #000000, $alpha: .9); + border-left-color: rgba($color: #000000, $alpha: .4); + border-bottom-color: rgba($color: #000000, $alpha: .4); + border-right-color: rgba($color: #000000, $alpha: .4); +} + +div.command-bar.dark div.command-button.active, +div.command-bar.dark div.command-button:hover { background-color: rgba($color: #FFFFFF, $alpha: .2); color: rgba($color: #FFFFFF, $alpha: 1); } -div.command-bar.light button.ms-Button.command-button.active, -div.command-bar.light button.ms-Button.command-button:hover { +div.command-bar.light div.command-button.active, +div.command-bar.light div.command-button:hover { background-color: rgba($color: #000000, $alpha: .08); color: rgba($color: #000000, $alpha: 1); } diff --git a/source/Component/CommandBar/CommandBar.tsx b/source/Component/CommandBar/CommandBar.tsx index d0550f8..59b68a4 100644 --- a/source/Component/CommandBar/CommandBar.tsx +++ b/source/Component/CommandBar/CommandBar.tsx @@ -1,5 +1,5 @@ -import { Component, ReactNode } from "react"; -import { DirectionalHint, IconButton } from "@fluentui/react"; +import { Component, ReactNode, FunctionComponent } from "react"; +import { DirectionalHint, Icon, Spinner } from "@fluentui/react"; import { useSetting, IMixinSettingProps } from "@Context/Setting"; import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; import { BackgroundLevel, Theme } from "@Component/Theme/Theme"; @@ -18,23 +18,28 @@ interface IRenderButtonParameter { iconName?: string; click?: () => void; active?: boolean; + isLoading?: boolean; } interface ICommandBarState { isSaveRunning: boolean; } -function getRenderButton(param: IRenderButtonParameter): ReactNode { +const CommandButton: FunctionComponent = (param) => { return - + > + {param.isLoading ? + : + + } +
} @useSetting @@ -68,78 +73,84 @@ class CommandBar extends Component - {getRenderButton({ - iconName: "Save", - i18NKey: "Command.Bar.Save.Info", - click: () => { + { this.setState({ isSaveRunning: true }); - } - })} + }} + /> - {getRenderButton({ - iconName: this.props.status?.actuator.start() ? "Pause" : "Play", - i18NKey: "Command.Bar.Play.Info", - click: () => this.props.status ? this.props.status.actuator.start( + this.props.status ? this.props.status.actuator.start( !this.props.status.actuator.start() - ) : undefined - })} + ) : undefined} + /> - {getRenderButton({ - iconName: "HandsFree", i18NKey: "Command.Bar.Drag.Info", - active: mouseMod === MouseMod.Drag, - click: () => this.props.status ? this.props.status.setMouseMod(MouseMod.Drag) : undefined - })} + this.props.status ? this.props.status.setMouseMod(MouseMod.Drag) : undefined} + /> - {getRenderButton({ - iconName: "TouchPointer", i18NKey: "Command.Bar.Select.Info", - active: mouseMod === MouseMod.click, - click: () => this.props.status ? this.props.status.setMouseMod(MouseMod.click) : undefined - })} + this.props.status ? this.props.status.setMouseMod(MouseMod.click) : undefined} + /> - {getRenderButton({ - iconName: "WebAppBuilderFragmentCreate", - i18NKey: "Command.Bar.Add.Group.Info", - click: () => { + { this.props.status ? this.props.status.newGroup() : undefined; - } - })} + }} + /> - {getRenderButton({ - iconName: "ProductVariant", - i18NKey: "Command.Bar.Add.Range.Info", - click: () => { + { this.props.status ? this.props.status.newRange() : undefined; - } - })} + }} + /> - {getRenderButton({ - iconName: "Running", - i18NKey: "Command.Bar.Add.Behavior.Info", - click: () => { + { this.props.status?.popup.showPopup(BehaviorPopup, {}); - } - })} + }} + /> - {getRenderButton({ - iconName: "Tag", - i18NKey: "Command.Bar.Add.Tag.Info", - click: () => { + { this.props.status ? this.props.status.newLabel() : undefined; - } - })} + }} + /> - {getRenderButton({ iconName: "Camera", i18NKey: "Command.Bar.Camera.Info" })} +
- {getRenderButton({ - iconName: "Settings", - i18NKey: "Command.Bar.Setting.Info", - click: () => { + { this.props.status?.popup.showPopup(SettingPopup, {}); - } - })} + }} + />
} diff --git a/source/Component/LoadFile/LoadFile.tsx b/source/Component/LoadFile/LoadFile.tsx index fdb7ed4..ef1ea24 100644 --- a/source/Component/LoadFile/LoadFile.tsx +++ b/source/Component/LoadFile/LoadFile.tsx @@ -64,7 +64,7 @@ async function fileChecker(status: Status, file?: File) { const loadFunc = () => { // 进行转换 - let errorMessage = status.archive.load(status.model, fileReader.result as string, file.name); + let errorMessage = status.archive.load(status.model, fileReader.result as string, file.name, file.path); if (errorMessage) { status.popup.showPopup(ConfirmPopup, { infoI18n: "Popup.Load.Save.Error.Parse", diff --git a/source/Context/Archive.tsx b/source/Context/Archive.tsx index 8e27be2..0a361c6 100644 --- a/source/Context/Archive.tsx +++ b/source/Context/Archive.tsx @@ -29,7 +29,7 @@ const ArchiveSaveDownloadView: FunctionComponent = f setTimeout(() => { download(file, props.fileName, "text/json"); props.then(); - }, 10); + }, 100); } useEffect(() => { runner() }, []); @@ -44,15 +44,17 @@ function ArchiveSaveFsView(props) { const runner = async () => { const file = await props.fileData(); - if (props.electron) { - props.electron.fileSave( - file, - I18N(props, "Popup.Load.Save.Select.File.Name"), - I18N(props, "Popup.Load.Save.Select.Path.Title"), - I18N(props, "Popup.Load.Save.Select.Path.Button"), - props.fileUrl - ); - } + setTimeout(() => { + if (props.electron) { + props.electron.fileSave( + file, + I18N(props, "Popup.Load.Save.Select.File.Name"), + I18N(props, "Popup.Load.Save.Select.Path.Title"), + I18N(props, "Popup.Load.Save.Select.Path.Button"), + props.fileUrl + ); + } + }, 100); } const saveEvent = ({name, url, success} : {name: string, url: string, success: boolean}) => { diff --git a/source/Model/Archive.ts b/source/Model/Archive.ts index 8c54b59..92688a1 100644 --- a/source/Model/Archive.ts +++ b/source/Model/Archive.ts @@ -266,6 +266,7 @@ class Archive extends Emitter { this.fileName = name; this.isSaved = true; this.isNewFile = false; + this.fileUrl = url; this.emit("fileSave", this); }; diff --git a/source/Page/SimulatorDesktop/SimulatorDesktop.tsx b/source/Page/SimulatorDesktop/SimulatorDesktop.tsx index 62ba58c..63b0e8d 100644 --- a/source/Page/SimulatorDesktop/SimulatorDesktop.tsx +++ b/source/Page/SimulatorDesktop/SimulatorDesktop.tsx @@ -1,4 +1,6 @@ import { Component, ReactNode } from "react"; +import { DndProvider } from 'react-dnd' +import { HTML5Backend } from 'react-dnd-html5-backend' import { SettingProvider, Setting, Platform } from "@Context/Setting"; import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme"; import { ISimulatorAPI } from "@Electron/SimulatorAPI"; @@ -9,10 +11,10 @@ import { initializeIcons } from '@fluentui/font-icons-mdl2'; import { RootContainer } from "@Component/Container/RootContainer"; import { LayoutDirection } from "@Context/Layout"; import { CommandBar } from "@Component/CommandBar/CommandBar"; +import { LoadFile } from "@Component/LoadFile/LoadFile"; import { HeaderBar } from "@Component/HeaderBar/HeaderBar"; import { Popup } from "@Component/Popup/Popup"; import { Entry } from "../Entry/Entry"; -import { Group } from "@Model/Group"; import "./SimulatorDesktop.scss"; initializeIcons("./font-icon/"); @@ -89,7 +91,9 @@ class SimulatorDesktop extends Component { return - {this.renderContent()} + + {this.renderContent()} + @@ -102,13 +106,15 @@ class SimulatorDesktop extends Component { fontLevel={FontLevel.Level3} > - -
- - -
+ + +
+ + +
+
} }