Commit 35ade7cb authored by BBeretta's avatar BBeretta

feat/language-translation (merge conflicts done)

parents 3ed6bdd4 b4f8fe7e
Pipeline #27824 failed with stages
in 7 minutes and 54 seconds
...@@ -28,3 +28,5 @@ yarn-error.log* ...@@ -28,3 +28,5 @@ yarn-error.log*
# scss type # scss type
*.module.scss.d.ts *.module.scss.d.ts
*.yrp3d
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <path opacity="0.5" d="M17 9.00195C19.175 9.01406 20.3529 9.11051 21.1213 9.8789C22 10.7576 22 12.1718 22 15.0002V16.0002C22 18.8286 22 20.2429 21.1213 21.1215C20.2426 22.0002 18.8284 22.0002 16 22.0002H8C5.17157 22.0002 3.75736 22.0002 2.87868 21.1215C2 20.2429 2 18.8286 2 16.0002L2 15.0002C2 12.1718 2 10.7576 2.87868 9.87889C3.64706 9.11051 4.82497 9.01406 7 9.00195" stroke="#d9d9d9" stroke-width="1.5" stroke-linecap="round"/> <path d="M12 2L12 15M12 15L9 11.5M12 15L15 11.5" stroke="#d9d9d9" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </g>
</svg>
\ No newline at end of file
{ {
"version": 4960, "version": 4961,
"servers": [{ "servers": [{
"name": "koishi", "name": "koishi",
"ip": "koishi.momobako.com", "ip": "koishi.momobako.com",
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
"athleticWatchUrl": "wss://tiramisu.moecube.com:8923", "athleticWatchUrl": "wss://tiramisu.moecube.com:8923",
"entertainWatchUrl": "wss://tiramisu.moecube.com:7923", "entertainWatchUrl": "wss://tiramisu.moecube.com:7923",
"userApi": "https://sapi.moecube.com:444/accounts/users/{username}.json", "userApi": "https://sapi.moecube.com:444/accounts/users/{username}.json",
"mdproServer": "https://rarnu.xyz:38443",
"streamInterval": 20, "streamInterval": 20,
"startDelay": 1000, "startDelay": 1000,
"ui": { "ui": {
......
{ {
"version": 4960, "version": 4961,
"servers": [{ "servers": [{
"name": "koishi", "name": "koishi",
"ip": "koishi.momobako.com", "ip": "koishi.momobako.com",
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
"athleticWatchUrl": "wss://tiramisu.moecube.com:8923", "athleticWatchUrl": "wss://tiramisu.moecube.com:8923",
"entertainWatchUrl": "wss://tiramisu.moecube.com:7923", "entertainWatchUrl": "wss://tiramisu.moecube.com:7923",
"userApi": "https://sapi.moecube.com:444/accounts/users/{username}.json", "userApi": "https://sapi.moecube.com:444/accounts/users/{username}.json",
"mdproServer": "https://rarnu.xyz:38443",
"streamInterval": 20, "streamInterval": 20,
"startDelay": 1000, "startDelay": 1000,
"ui": { "ui": {
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
"classnames": "^2.3.2", "classnames": "^2.3.2",
"cookies-ts": "^1.0.5", "cookies-ts": "^1.0.5",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"fuse.js": "^7.0.0",
"google-protobuf": "^3.21.2", "google-protobuf": "^3.21.2",
"i18next": "^23.11.4", "i18next": "^23.11.4",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
...@@ -58,6 +59,7 @@ ...@@ -58,6 +59,7 @@
"sass": "^1.65.1", "sass": "^1.65.1",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"vite": "^4.4.9", "vite": "^4.4.9",
"vite-plugin-arraybuffer": "^0.0.6",
"vite-plugin-sass-dts": "^1.3.9", "vite-plugin-sass-dts": "^1.3.9",
"vite-plugin-wasm-pack": "^0.1.12", "vite-plugin-wasm-pack": "^0.1.12",
"vite-tsconfig-paths": "^4.2.0", "vite-tsconfig-paths": "^4.2.0",
...@@ -423,81 +425,17 @@ ...@@ -423,81 +425,17 @@
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.22.10", "version": "7.24.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
"integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
"dependencies": { "dependencies": {
"@babel/highlight": "^7.22.10", "@babel/highlight": "^7.24.2",
"chalk": "^2.4.2" "picocolors": "^1.0.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/code-frame/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/code-frame/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/code-frame/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/compat-data": { "node_modules/@babel/compat-data": {
"version": "7.22.9", "version": "7.22.9",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz",
...@@ -544,13 +482,13 @@ ...@@ -544,13 +482,13 @@
} }
}, },
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.22.10", "version": "7.24.4",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz",
"integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==",
"dependencies": { "dependencies": {
"@babel/types": "^7.22.10", "@babel/types": "^7.24.0",
"@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.17", "@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^2.5.1" "jsesc": "^2.5.1"
}, },
"engines": { "engines": {
...@@ -581,20 +519,20 @@ ...@@ -581,20 +519,20 @@
} }
}, },
"node_modules/@babel/helper-environment-visitor": { "node_modules/@babel/helper-environment-visitor": {
"version": "7.22.5", "version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
"integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-function-name": { "node_modules/@babel/helper-function-name": {
"version": "7.22.5", "version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
"integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dependencies": { "dependencies": {
"@babel/template": "^7.22.5", "@babel/template": "^7.22.15",
"@babel/types": "^7.22.5" "@babel/types": "^7.23.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
...@@ -672,17 +610,17 @@ ...@@ -672,17 +610,17 @@
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
"version": "7.22.5", "version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-validator-identifier": { "node_modules/@babel/helper-validator-identifier": {
"version": "7.22.5", "version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
...@@ -709,13 +647,14 @@ ...@@ -709,13 +647,14 @@
} }
}, },
"node_modules/@babel/highlight": { "node_modules/@babel/highlight": {
"version": "7.22.10", "version": "7.24.2",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz",
"integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==",
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2", "chalk": "^2.4.2",
"js-tokens": "^4.0.0" "js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
...@@ -786,9 +725,9 @@ ...@@ -786,9 +725,9 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.22.10", "version": "7.24.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz",
"integrity": "sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==", "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==",
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
}, },
...@@ -838,32 +777,32 @@ ...@@ -838,32 +777,32 @@
} }
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.22.5", "version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
"integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.22.5", "@babel/code-frame": "^7.23.5",
"@babel/parser": "^7.22.5", "@babel/parser": "^7.24.0",
"@babel/types": "^7.22.5" "@babel/types": "^7.24.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/traverse": { "node_modules/@babel/traverse": {
"version": "7.22.10", "version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz",
"integrity": "sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==", "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.22.10", "@babel/code-frame": "^7.24.1",
"@babel/generator": "^7.22.10", "@babel/generator": "^7.24.1",
"@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.22.5", "@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6", "@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.22.10", "@babel/parser": "^7.24.1",
"@babel/types": "^7.22.10", "@babel/types": "^7.24.0",
"debug": "^4.1.0", "debug": "^4.3.1",
"globals": "^11.1.0" "globals": "^11.1.0"
}, },
"engines": { "engines": {
...@@ -871,12 +810,12 @@ ...@@ -871,12 +810,12 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.22.10", "version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
"integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.22.5", "@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0" "to-fast-properties": "^2.0.0"
}, },
"engines": { "engines": {
...@@ -1410,13 +1349,13 @@ ...@@ -1410,13 +1349,13 @@
"dev": true "dev": true
}, },
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dependencies": { "dependencies": {
"@jridgewell/set-array": "^1.0.1", "@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9" "@jridgewell/trace-mapping": "^0.3.24"
}, },
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
...@@ -1431,9 +1370,9 @@ ...@@ -1431,9 +1370,9 @@
} }
}, },
"node_modules/@jridgewell/set-array": { "node_modules/@jridgewell/set-array": {
"version": "1.1.2", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
...@@ -1455,12 +1394,12 @@ ...@@ -1455,12 +1394,12 @@
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.17", "version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
...@@ -3809,6 +3748,14 @@ ...@@ -3809,6 +3748,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/fuse.js": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz",
"integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==",
"engines": {
"node": ">=10"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
...@@ -4760,9 +4707,9 @@ ...@@ -4760,9 +4707,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.6", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
...@@ -5132,9 +5079,9 @@ ...@@ -5132,9 +5079,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.27", "version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
...@@ -5150,9 +5097,9 @@ ...@@ -5150,9 +5097,9 @@
} }
], ],
"dependencies": { "dependencies": {
"nanoid": "^3.3.6", "nanoid": "^3.3.7",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.2.0"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
...@@ -6462,9 +6409,9 @@ ...@@ -6462,9 +6409,9 @@
} }
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.0.2", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
...@@ -6988,9 +6935,9 @@ ...@@ -6988,9 +6935,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.4.9", "version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
"postcss": "^8.4.27", "postcss": "^8.4.27",
...@@ -7041,6 +6988,12 @@ ...@@ -7041,6 +6988,12 @@
} }
} }
}, },
"node_modules/vite-plugin-arraybuffer": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/vite-plugin-arraybuffer/-/vite-plugin-arraybuffer-0.0.6.tgz",
"integrity": "sha512-2TXAUuHREy29seo6qgm/lGjUpYxJjzsIGHi7cOScMALK6GHIpV2/3KHZ6uaLVAUGwbYQ8rDhNqYgnPkFLxZgnQ==",
"dev": true
},
"node_modules/vite-plugin-sass-dts": { "node_modules/vite-plugin-sass-dts": {
"version": "1.3.9", "version": "1.3.9",
"resolved": "https://registry.npmjs.org/vite-plugin-sass-dts/-/vite-plugin-sass-dts-1.3.9.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-sass-dts/-/vite-plugin-sass-dts-1.3.9.tgz",
...@@ -7494,63 +7447,12 @@ ...@@ -7494,63 +7447,12 @@
} }
}, },
"@babel/code-frame": { "@babel/code-frame": {
"version": "7.22.10", "version": "7.24.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
"integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
"requires": { "requires": {
"@babel/highlight": "^7.22.10", "@babel/highlight": "^7.24.2",
"chalk": "^2.4.2" "picocolors": "^1.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": {
"has-flag": "^3.0.0"
}
}
} }
}, },
"@babel/compat-data": { "@babel/compat-data": {
...@@ -7588,13 +7490,13 @@ ...@@ -7588,13 +7490,13 @@
} }
}, },
"@babel/generator": { "@babel/generator": {
"version": "7.22.10", "version": "7.24.4",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz",
"integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==",
"requires": { "requires": {
"@babel/types": "^7.22.10", "@babel/types": "^7.24.0",
"@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.17", "@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^2.5.1" "jsesc": "^2.5.1"
} }
}, },
...@@ -7618,17 +7520,17 @@ ...@@ -7618,17 +7520,17 @@
} }
}, },
"@babel/helper-environment-visitor": { "@babel/helper-environment-visitor": {
"version": "7.22.5", "version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
"integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==" "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA=="
}, },
"@babel/helper-function-name": { "@babel/helper-function-name": {
"version": "7.22.5", "version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
"integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"requires": { "requires": {
"@babel/template": "^7.22.5", "@babel/template": "^7.22.15",
"@babel/types": "^7.22.5" "@babel/types": "^7.23.0"
} }
}, },
"@babel/helper-hoist-variables": { "@babel/helper-hoist-variables": {
...@@ -7682,14 +7584,14 @@ ...@@ -7682,14 +7584,14 @@
} }
}, },
"@babel/helper-string-parser": { "@babel/helper-string-parser": {
"version": "7.22.5", "version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ=="
}, },
"@babel/helper-validator-identifier": { "@babel/helper-validator-identifier": {
"version": "7.22.5", "version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==" "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A=="
}, },
"@babel/helper-validator-option": { "@babel/helper-validator-option": {
"version": "7.22.5", "version": "7.22.5",
...@@ -7707,13 +7609,14 @@ ...@@ -7707,13 +7609,14 @@
} }
}, },
"@babel/highlight": { "@babel/highlight": {
"version": "7.22.10", "version": "7.24.2",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz",
"integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==",
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2", "chalk": "^2.4.2",
"js-tokens": "^4.0.0" "js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
}, },
"dependencies": { "dependencies": {
"ansi-styles": { "ansi-styles": {
...@@ -7768,9 +7671,9 @@ ...@@ -7768,9 +7671,9 @@
} }
}, },
"@babel/parser": { "@babel/parser": {
"version": "7.22.10", "version": "7.24.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz",
"integrity": "sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==" "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg=="
}, },
"@babel/plugin-transform-react-jsx-self": { "@babel/plugin-transform-react-jsx-self": {
"version": "7.22.5", "version": "7.22.5",
...@@ -7799,39 +7702,39 @@ ...@@ -7799,39 +7702,39 @@
} }
}, },
"@babel/template": { "@babel/template": {
"version": "7.22.5", "version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
"integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==",
"requires": { "requires": {
"@babel/code-frame": "^7.22.5", "@babel/code-frame": "^7.23.5",
"@babel/parser": "^7.22.5", "@babel/parser": "^7.24.0",
"@babel/types": "^7.22.5" "@babel/types": "^7.24.0"
} }
}, },
"@babel/traverse": { "@babel/traverse": {
"version": "7.22.10", "version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz",
"integrity": "sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==", "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==",
"requires": { "requires": {
"@babel/code-frame": "^7.22.10", "@babel/code-frame": "^7.24.1",
"@babel/generator": "^7.22.10", "@babel/generator": "^7.24.1",
"@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.22.5", "@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6", "@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.22.10", "@babel/parser": "^7.24.1",
"@babel/types": "^7.22.10", "@babel/types": "^7.24.0",
"debug": "^4.1.0", "debug": "^4.3.1",
"globals": "^11.1.0" "globals": "^11.1.0"
} }
}, },
"@babel/types": { "@babel/types": {
"version": "7.22.10", "version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
"integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
"requires": { "requires": {
"@babel/helper-string-parser": "^7.22.5", "@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0" "to-fast-properties": "^2.0.0"
} }
}, },
...@@ -8108,13 +8011,13 @@ ...@@ -8108,13 +8011,13 @@
"dev": true "dev": true
}, },
"@jridgewell/gen-mapping": { "@jridgewell/gen-mapping": {
"version": "0.3.3", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"requires": { "requires": {
"@jridgewell/set-array": "^1.0.1", "@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9" "@jridgewell/trace-mapping": "^0.3.24"
} }
}, },
"@jridgewell/resolve-uri": { "@jridgewell/resolve-uri": {
...@@ -8123,9 +8026,9 @@ ...@@ -8123,9 +8026,9 @@
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
}, },
"@jridgewell/set-array": { "@jridgewell/set-array": {
"version": "1.1.2", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="
}, },
"@jridgewell/source-map": { "@jridgewell/source-map": {
"version": "0.3.2", "version": "0.3.2",
...@@ -8144,12 +8047,12 @@ ...@@ -8144,12 +8047,12 @@
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
}, },
"@jridgewell/trace-mapping": { "@jridgewell/trace-mapping": {
"version": "0.3.17", "version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"requires": { "requires": {
"@jridgewell/resolve-uri": "3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"@nodelib/fs.scandir": { "@nodelib/fs.scandir": {
...@@ -9777,6 +9680,11 @@ ...@@ -9777,6 +9680,11 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"fuse.js": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz",
"integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q=="
},
"gensync": { "gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
...@@ -10453,9 +10361,9 @@ ...@@ -10453,9 +10361,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"nanoid": { "nanoid": {
"version": "3.3.6", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
}, },
"narrowing": { "narrowing": {
"version": "1.5.0", "version": "1.5.0",
...@@ -10716,13 +10624,13 @@ ...@@ -10716,13 +10624,13 @@
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
}, },
"postcss": { "postcss": {
"version": "8.4.27", "version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"requires": { "requires": {
"nanoid": "^3.3.6", "nanoid": "^3.3.7",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.2.0"
} }
}, },
"postcss-js": { "postcss-js": {
...@@ -11599,9 +11507,9 @@ ...@@ -11599,9 +11507,9 @@
"peer": true "peer": true
}, },
"source-map-js": { "source-map-js": {
"version": "1.0.2", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg=="
}, },
"source-map-support": { "source-map-support": {
"version": "0.5.21", "version": "0.5.21",
...@@ -11954,9 +11862,9 @@ ...@@ -11954,9 +11862,9 @@
} }
}, },
"vite": { "vite": {
"version": "4.4.9", "version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"requires": { "requires": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
"fsevents": "~2.3.2", "fsevents": "~2.3.2",
...@@ -11964,6 +11872,12 @@ ...@@ -11964,6 +11872,12 @@
"rollup": "^3.27.1" "rollup": "^3.27.1"
} }
}, },
"vite-plugin-arraybuffer": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/vite-plugin-arraybuffer/-/vite-plugin-arraybuffer-0.0.6.tgz",
"integrity": "sha512-2TXAUuHREy29seo6qgm/lGjUpYxJjzsIGHi7cOScMALK6GHIpV2/3KHZ6uaLVAUGwbYQ8rDhNqYgnPkFLxZgnQ==",
"dev": true
},
"vite-plugin-sass-dts": { "vite-plugin-sass-dts": {
"version": "1.3.9", "version": "1.3.9",
"resolved": "https://registry.npmjs.org/vite-plugin-sass-dts/-/vite-plugin-sass-dts-1.3.9.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-sass-dts/-/vite-plugin-sass-dts-1.3.9.tgz",
......
import { useConfig } from "@/config";
import sqliteMiddleWare, { sqliteCmd } from "@/middleware/sqlite"; import sqliteMiddleWare, { sqliteCmd } from "@/middleware/sqlite";
import { FtsParams } from "@/middleware/sqlite/fts"; import { FtsParams } from "@/middleware/sqlite/fts";
import { isSuperReleaseCard } from "./superPreRelease";
const NeosConfig = useConfig();
export interface CardMeta { export interface CardMeta {
id: number; id: number;
data: CardData; data: CardData;
...@@ -71,3 +76,32 @@ export function getCardStr(meta: CardMeta, idx: number): string | undefined { ...@@ -71,3 +76,32 @@ export function getCardStr(meta: CardMeta, idx: number): string | undefined {
const str = `str${idx + 1}` as CardStrRange; const str = `str${idx + 1}` as CardStrRange;
return meta.text[str]; return meta.text[str];
} }
export function getCardImgUrl(code: number, back = false) {
const ASSETS_BASE =
import.meta.env.BASE_URL === "/"
? NeosConfig.assetsPath
: `${import.meta.env.BASE_URL}${NeosConfig.assetsPath}`;
if (back || code === 0) {
return `${ASSETS_BASE}/card_back.jpg`;
}
if (isSuperReleaseCard(code)) {
return `${NeosConfig.preReleaseImgUrl}/${code}.jpg`;
} else {
const language = localStorage.getItem("language");
if (
language === "en" ||
language === "br" ||
language === "pt" ||
language === "fr" ||
language === "es"
) {
NeosConfig.releaseImgUrl = NeosConfig.releaseImgUrl.replace(
"zh-CN",
"en-US",
);
}
return `${NeosConfig.releaseImgUrl}/${code}.jpg`;
}
}
export * from "./cards"; export * from "./cards";
export * from "./cookies"; export * from "./cookies";
export * from "./forbiddens"; export * from "./forbiddens";
export * from "./mdproDeck";
export * from "./mycard"; export * from "./mycard";
export * from "./ocgcore/idl/ocgcore"; export * from "./ocgcore/idl/ocgcore";
export * from "./ocgcore/ocgHelper"; export * from "./ocgcore/ocgHelper";
......
import { useConfig } from "@/config";
import { MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/sync/single";
interface DeleteReq {
userId: number;
deck: {
deckId: string;
isDelete: boolean;
};
}
export async function deleteDeck(
userID: number,
token: string,
deckID: string,
): Promise<MdproResp<boolean> | undefined> {
const myHeaders = mdproHeaders();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("token", token);
const req: DeleteReq = {
userId: userID,
deck: {
deckId: deckID,
isDelete: true,
},
};
const resp = await fetch(`${mdproServer}/${API_PATH}`, {
method: "POST",
headers: myHeaders,
body: JSON.stringify(req),
redirect: "follow",
});
return await handleHttps(resp, API_PATH);
}
import { useConfig } from "@/config";
import { MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/deck/deckId";
export async function generateDeck(): Promise<MdproResp<string> | undefined> {
const myHeaders = mdproHeaders();
const resp = await fetch(`${mdproServer}/${API_PATH}`, {
method: "GET",
headers: myHeaders,
redirect: "follow",
});
return await handleHttps(resp, API_PATH);
}
export * from "./delete";
export * from "./generate";
export * from "./mget";
export * from "./personalList";
export * from "./pull";
export * from "./sync";
export * from "./updatePulibc";
export * from "./upload";
import { useConfig } from "@/config";
import { MdproDeck, MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/deck";
export async function mgetDeck(
id: string,
): Promise<MdproResp<MdproDeck> | undefined> {
const myHeaders = mdproHeaders();
const resp = await fetch(`${mdproServer}/${API_PATH}/${id}`, {
method: "GET",
headers: myHeaders,
redirect: "follow",
});
return await handleHttps(resp, API_PATH);
}
import { useConfig } from "@/config";
import { MdproDeck, MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/sync/";
export interface PersonalListReq {
/* ID of MyCard Account */
userID: number;
/* Token of MyCard Account */
token: string;
}
export async function getPersonalList(
req: PersonalListReq,
): Promise<MdproResp<MdproDeck[]> | undefined> {
const myHeaders = mdproHeaders();
myHeaders.append("token", req.token);
const resp = await fetch(`${mdproServer}/${API_PATH}/${req.userID}`, {
method: "GET",
headers: myHeaders,
redirect: "follow",
});
return await handleHttps(resp, API_PATH);
}
import { useConfig } from "@/config";
import { MdproDeckLike, MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
const { mdproServer } = useConfig();
const API_PATH = "api/mdpro3/deck/list";
export interface PullReq {
page?: number;
size?: number;
keyWord?: string;
sortLike?: boolean;
sortRank?: boolean;
contributor?: string;
}
const defaultPullReq: PullReq = {
page: 1,
size: 20,
};
interface RespData {
current: number;
size: number;
total: number;
pages: number;
records: MdproDeckLike[];
}
export async function pullDecks(
req: PullReq = defaultPullReq,
): Promise<MdproResp<RespData> | undefined> {
const myHeaders = mdproHeaders();
const params = new URLSearchParams();
Object.entries(req).forEach(([key, value]) => {
if (value !== undefined) {
params.append(key, String(value));
}
});
const url = new URL(`${mdproServer}/${API_PATH}`);
url.search = params.toString();
const resp = await fetch(url.toString(), {
method: "GET",
headers: myHeaders,
redirect: "follow",
});
return await handleHttps(resp, API_PATH);
}
export interface MdproResp<T> {
code: number;
message: string;
data?: T;
}
export interface MdproDeck {
/*
*`ID` of the online deck.
* It is required when updating the deck, and optional
* when adding new deck. However, for the convenience,
* it is defined required here and it would be set to zero
* when adding new deck.
*/
deckId: string;
/* Contributor of the deck. */
deckContributor: string;
/* Name of the deck. */
deckName: string;
deckRank?: number;
deckLike?: number;
deckUploadDate?: string;
deckUpdateDate?: string;
/* Content of the deck. */
deckYdk?: string;
deckCase: number;
/* User ID of MyCard Account */
userId: number;
}
export interface MdproDeckLike {
deckId: string;
deckContributor: string;
deckName: string;
deckLike?: number;
deckCase: number;
lastDate?: string;
}
import { useConfig } from "@/config";
import { MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/sync/single";
export interface SyncReq {
userId: number;
deckContributor: string;
deck: {
deckId: string;
deckName: string;
deckCase: number;
deckYdk: string;
};
}
export async function syncDeck(
req: SyncReq,
token: string,
): Promise<MdproResp<boolean> | undefined> {
const myHeaders = mdproHeaders();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("token", token);
const resp = await fetch(`${mdproServer}/${API_PATH}`, {
method: "POST",
headers: myHeaders,
body: JSON.stringify(req),
redirect: "follow",
});
return await handleHttps(resp, API_PATH);
}
import { useConfig } from "@/config";
import { MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/deck/public";
export interface UpdatePublicReq {
userId: number;
deckId: string;
isPublic: boolean;
}
export async function updatePublic(
req: UpdatePublicReq,
token: string,
): Promise<MdproResp<void> | undefined> {
const myHeaders = mdproHeaders();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("token", token);
const resp = await fetch(`${mdproServer}/${API_PATH}`, {
method: "POST",
headers: myHeaders,
body: JSON.stringify(req),
redirect: "follow",
});
return await handleHttps(resp, API_PATH);
}
import { generateDeck } from "./generate";
import { MdproResp } from "./schema";
import { syncDeck } from "./sync";
import { updatePublic } from "./updatePulibc";
export interface UploadReq {
userId: number;
token: string;
deckContributor: string;
deck: {
deckName: string;
deckCase: number;
deckYdk: string;
};
}
export async function uploadDeck(
req: UploadReq,
): Promise<MdproResp<void> | undefined> {
const generateResp = await generateDeck();
if (generateResp === undefined) return undefined;
if (generateResp.code !== 0 || generateResp.data === undefined)
return { code: generateResp.code, message: generateResp.message };
const deckId = generateResp.data;
const syncRes = await syncDeck(
{
userId: req.userId,
deckContributor: req.deckContributor,
deck: {
deckId,
deckName: req.deck.deckName,
deckCase: req.deck.deckCase,
deckYdk: req.deck.deckYdk,
},
},
req.token,
);
if (syncRes === undefined) return undefined;
if (syncRes.code === 0 && syncRes.data === true) {
// succeed in syncing
return await updatePublic(
{
userId: req.userId,
deckId,
isPublic: true,
},
req.token,
);
} else {
return { code: syncRes.code, message: syncRes.message };
}
}
export function mdproHeaders(): Headers {
const myHeaders = new Headers();
myHeaders.append("ReqSource", "MDPro3");
return myHeaders;
}
export async function handleHttps<T>(
resp: Response,
api: string,
): Promise<T | undefined> {
if (!resp.ok) {
console.error(
`[Mdpro] Https error from api ${api}! status: ${resp.status}`,
);
return undefined;
} else {
return await resp.json();
}
}
...@@ -21,6 +21,6 @@ export default (data: Uint8Array) => { ...@@ -21,6 +21,6 @@ export default (data: Uint8Array) => {
code, code,
from: fromLocation, from: fromLocation,
to: toLocation, to: toLocation,
reason: reader.inner.readUint8(), reason: reader.inner.readUint32(),
}); });
}; };
...@@ -220,7 +220,7 @@ export const Race2StringCodeMap: Map<number, number> = new Map([ ...@@ -220,7 +220,7 @@ export const Race2StringCodeMap: Map<number, number> = new Map([
[RACE_CYBERSE, 1044], [RACE_CYBERSE, 1044],
]); ]);
// const REASON_DESTROY = 0x1; // export const REASON_DESTROY = 0x1; //
// const REASON_RELEASE = 0x2; // // const REASON_RELEASE = 0x2; //
// const REASON_TEMPORARY = 0x4; // // const REASON_TEMPORARY = 0x4; //
export const REASON_MATERIAL = 0x8; // export const REASON_MATERIAL = 0x8; //
......
import EventEmitter from "eventemitter3";
export class NeosAudioContext extends EventEmitter<AudioScheduledSourceNodeEventMap> {
private _musicAudioContext = new AudioContext();
private _gainNode = this._musicAudioContext.createGain();
private _isClosed = false;
constructor(volume = 1) {
super();
this._gainNode.gain.value = volume;
}
public get state() {
return this._musicAudioContext.state;
}
public get closed() {
return this._isClosed;
}
/**
* 触发自动播放
*/
private _triggerAutoPlay() {
if (this.state === "suspended") {
const autoPlay = () => {
document.removeEventListener("click", autoPlay);
this.resume();
};
document.addEventListener("click", autoPlay);
}
}
/**
* 监听未启播
*/
private _observerPlayState(source: AudioBufferSourceNode) {
// 50ms 未启播,说明播放失败了,重新尝试播放
const timeout = setTimeout(async () => {
if (source.loop) return;
if (this.state === "closed") return;
if (source.context.currentTime === 0) {
await this.suspend();
await this.resume();
}
}, 50);
// 播放结束后关闭音频
const Ended = () => {
source.removeEventListener("ended", Ended);
this.emit("ended");
clearTimeout(timeout);
this.close();
};
source.addEventListener("ended", Ended);
}
/**
* 播放音频
* @param audio 音频数据
*/
public async play(audio: ArrayBuffer) {
const source = this._musicAudioContext.createBufferSource();
const buffer = await this._musicAudioContext.decodeAudioData(audio);
source.buffer = buffer;
source.connect(this._gainNode).connect(this._musicAudioContext.destination);
source.start();
this._triggerAutoPlay();
this._observerPlayState(source);
}
public async resume() {
if (this.state !== "suspended") return;
return this._musicAudioContext.resume();
}
public async suspend() {
if (this.state !== "running") return;
return this._musicAudioContext.suspend();
}
public close() {
if (this._isClosed) return;
this._isClosed = true;
return this._musicAudioContext.close();
}
public updateVolume(volume: number) {
this._gainNode.gain.value = volume;
}
}
import { AudioActionType } from "../type";
import { NeosAudioContext } from "./context";
import {
getEffectName,
getMusicName,
loadAudio,
removeAudio,
} from "./resource";
class AudioManager {
private musicContext = new NeosAudioContext();
private effectContextSet: WeakSet<NeosAudioContext> = new WeakSet();
/** 当前播放的音频路径 */
private _currentMusicPath: string = "";
private _scene: AudioActionType | null = null;
private _musicVolume = 1;
public enableBGM = false;
public get scene() {
return this._scene;
}
public set scene(scene: AudioActionType | null) {
this._scene = scene;
if (this.enableBGM) {
this.playMusic();
}
}
public async playMusic() {
if (!this.enableBGM || !this._scene) return;
if (!this.musicContext.closed) {
this.musicContext.close();
}
this.musicContext = new NeosAudioContext(this._musicVolume);
this.musicContext.once("ended", () => {
this.playMusic();
});
const name = getMusicName(this._scene, this._currentMusicPath);
try {
const resource = await loadAudio(name);
await this.musicContext.play(resource);
this._currentMusicPath = name;
} catch {
// 音频资源有问题
removeAudio(name);
}
}
public async playEffect(effect: AudioActionType, volume = 1) {
let name = getEffectName(effect);
try {
const audioContext = new NeosAudioContext(volume);
audioContext.once("ended", () => {
this.effectContextSet.delete(audioContext);
});
this.effectContextSet.add(audioContext);
const resource = await loadAudio(name);
await audioContext.play(resource);
} catch {
// 音频资源有问题
removeAudio(name);
}
}
public updateMusicVolume(volume = 1) {
this._musicVolume = volume;
this.musicContext.updateVolume(volume);
}
public enableMusic() {
this.enableBGM = true;
this.playMusic();
}
public disableMusic() {
this.enableBGM = false;
this.musicContext.suspend();
}
public switchDisableMusic() {
if (this.enableBGM) {
this.disableMusic();
} else {
this.enableMusic();
}
}
}
export const audioContextManger = new AudioManager();
import { clear, createStore, del, get, set } from "idb-keyval";
import { useConfig } from "@/config";
import { AudioActionType } from "../type";
const AUDIO_DB_NAME = "audio";
const sourceDb = createStore(AUDIO_DB_NAME, "sources");
const { assetsPath } = useConfig();
/** 从网络加载音频资源 */
async function loadFromNet(name: string) {
const prefix = `${assetsPath}/sound/`;
const response = await fetch(`${prefix}${name}`);
const fileBlob = await response.arrayBuffer();
cacheResource(name, fileBlob);
return fileBlob;
}
/** 从缓存中加载音频资源 */
function loadFromCache(name: string) {
return get(name, sourceDb);
}
/** 缓存资源 */
function cacheResource(name: string, fileBlob: ArrayBuffer) {
set(name, fileBlob, sourceDb);
}
/** 加载音频资源 */
export async function loadAudio(name: string) {
// 从缓存资源获取
const cachedFile = await loadFromCache(name);
if (cachedFile) {
return cachedFile;
}
// 从网络获取
const fileBlob = await loadFromNet(name);
return fileBlob;
}
/** 移除音频资源 */
export async function removeAudio(name: string) {
try {
await del(name, sourceDb);
} catch {
// 资源未落库,不做处理
}
}
/** 清空音频缓存 */
export async function clearAudioCache() {
return clear(sourceDb);
}
/** 获取音效名称 */
export function getEffectName(effect: AudioActionType) {
switch (effect) {
/** ******************** effect ********************/
case AudioActionType.SOUND_SUMMON:
return "summon.wav";
case AudioActionType.SOUND_SPECIAL_SUMMON:
return "specialsummon.wav";
case AudioActionType.SOUND_ACTIVATE:
return "activate.wav";
case AudioActionType.SOUND_SET:
return "set.wav";
case AudioActionType.SOUND_FILP:
return "flip.wav";
case AudioActionType.SOUND_REVEAL:
return "reveal.wav";
case AudioActionType.SOUND_EQUIP:
return "equip.wav";
case AudioActionType.SOUND_DESTROYED:
return "destroyed.wav";
case AudioActionType.SOUND_BANISHED:
return "banished.wav";
case AudioActionType.SOUND_TOKEN:
return "token.wav";
case AudioActionType.SOUND_ATTACK:
return "attack.wav";
case AudioActionType.SOUND_DIRECT_ATTACK:
return "directattack.wav";
case AudioActionType.SOUND_DRAW:
return "draw.wav";
case AudioActionType.SOUND_SHUFFLE:
return "shuffle.wav";
case AudioActionType.SOUND_DAMAGE:
return "damage.wav";
case AudioActionType.SOUND_RECOVER:
return "gainlp.wav";
case AudioActionType.SOUND_COUNTER_ADD:
return "addcounter.wav";
case AudioActionType.SOUND_COUNTER_REMOVE:
return "removecounter.wav";
case AudioActionType.SOUND_COIN:
return "coinflip.wav";
case AudioActionType.SOUND_DICE:
return "diceroll.wav";
case AudioActionType.SOUND_NEXT_TURN:
return "nextturn.wav";
case AudioActionType.SOUND_PHASE:
return "phase.wav";
case AudioActionType.SOUND_MENU:
return "menu.wav";
case AudioActionType.SOUND_BUTTON:
return "button.wav";
case AudioActionType.SOUND_INFO:
return "info.wav";
case AudioActionType.SOUND_QUESTION:
return "question.wav";
case AudioActionType.SOUND_CARD_PICK:
return "cardpick.wav";
case AudioActionType.SOUND_CARD_DROP:
return "carddrop.wav";
case AudioActionType.SOUND_PLAYER_ENTER:
return "playerenter.wav";
case AudioActionType.SOUND_CHAT:
return "chatmessage.wav";
default:
return "";
}
}
/** 获取音乐名称 */
export function getMusicName(music: AudioActionType, current?: string) {
let res: string[] = [];
switch (music) {
/** ******************** bgm ********************/
case AudioActionType.BGM_MENU:
res = [
"BGM/menu/福田康文 - ディスク:1.mp3",
"BGM/menu/光宗信吉 - 伝説の决闘(デュエル)(D3).mp3",
"BGM/menu/蓑部雄崇 - 十代のテーマ.mp3",
];
break;
case AudioActionType.BGM_DECK:
res = ["BGM/deck/bgm_deck.mp3", "BGM/deck/bgm_shop.mp3"];
break;
case AudioActionType.BGM_DUEL:
res = [
"BGM/duel/蓑部雄崇 - 悲しいデュエル.mp3",
"BGM/duel/蓑部雄崇 - 鬼柳京介.mp3",
"BGM/duel/蓑部雄崇 - 游星バトル.mp3",
"BGM/duel/蓑部雄崇 - スピードワールド.mp3",
"BGM/duel/中川幸太郎 - 不動のデュエル.mp3",
"BGM/duel/中川幸太郎 - 反逆のデュエル.mp3",
];
break;
case AudioActionType.BGM_ADVANTAGE:
res = [
"BGM/advantage/池頼広 - 熱き決闘者たち (Re-arranged).mp3",
"BGM/advantage/蓑部雄崇 - 游星テーマ.mp3",
"BGM/advantage/蓑部雄崇 - ピンチ!.mp3",
];
break;
case AudioActionType.BGM_DISADVANTAGE:
res = [
"BGM/disadvantage/池頼広 - 神の怒り (Re-arranged:type one).mp3",
"BGM/disadvantage/光宗信吉 - 热き决闘者たち.mp3",
"BGM/disadvantage/蓑部雄崇 - 逆転の一手!.mp3",
];
break;
case AudioActionType.BGM_WIN:
res = ["BGM/win/bgm_result.mp3"];
break;
case AudioActionType.BGM_LOSE:
res = ["BGM/lose/bgm_result_lose1.mp3"];
break;
default:
break;
}
const filterRes = res.filter((name) => name !== current);
return filterRes[Math.floor(Math.random() * filterRes.length)];
}
# 这里记录一下未实现音效,需要一起研究下什么时机插入该音效
- Connect timeout SOUND_INFO
- Client event change SOUND_INFO
- STOC_HS_PLAYER_CHANGE PLAYER_ENTER
- confirm_cards 未分类,待确定是否包含 decktop extratop cards
- Shuffle extra SOUND_SHUFFLE
- Random selected SOUND_DICE
- EQUIP
import { settingStore } from "@/stores/settingStore";
import { audioContextManger } from "../core/manager";
import { AudioActionType } from "../type";
export function playEffect(effect: AudioActionType) {
if (!settingStore.audio.enableSoundEffects) return;
return audioContextManger.playEffect(
effect,
settingStore.audio.soundEffectsVolume,
);
}
export * from "./usePlayEffect";
import { useEffect, useRef } from "react";
import { AudioActionType, playEffect } from "@/infra/audio";
export function usePlayEffect<T extends HTMLElement>(effect: AudioActionType) {
const effectRef = useRef<T | null>(null);
useEffect(() => {
const handleClick = () => {
playEffect(effect);
};
effectRef.current?.addEventListener("click", handleClick);
return () => {
effectRef.current?.removeEventListener("click", handleClick);
};
}, [playEffect]);
// 留一些扩展性,并且方便修改引用 key
return [effectRef] as const;
}
export * from "./effect";
export * from "./hooks";
export * from "./music";
export * from "./type";
import { subscribe } from "valtio";
import { settingStore } from "@/stores/settingStore";
import { audioContextManger } from "../core/manager";
import { AudioActionType } from "../type";
// 监听设置改动
subscribe(settingStore.audio, (opts) => {
for (let [_op, path, newValue] of opts) {
if (
path.includes("enableMusic") &&
newValue !== audioContextManger.enableBGM
) {
audioContextManger.switchDisableMusic();
}
if (path.includes("musicVolume")) {
audioContextManger.updateMusicVolume(newValue as number);
}
}
});
// 切换场景
export function changeScene(scene: AudioActionType) {
if (
// 场景切换
audioContextManger.scene !== scene &&
// 允许跟随场景切换音乐
settingStore.audio.enableMusicSwitchByEnv
) {
audioContextManger.scene = scene;
}
}
// 初始化音频设置
function initAudioSetting() {
audioContextManger.updateMusicVolume(settingStore.audio.musicVolume);
audioContextManger.enableBGM = settingStore.audio.enableMusic ?? false;
}
initAudioSetting();
/** 音效类型 */
export enum AudioActionType {
/** 召唤 */
SOUND_SUMMON = 101,
/** 特殊召唤 */
SOUND_SPECIAL_SUMMON = 102,
/** 发动 */
SOUND_ACTIVATE = 103,
/** 设置 */
SOUND_SET = 104,
/** 翻转 */
SOUND_FILP = 105,
/** 揭示 */
SOUND_REVEAL = 106,
/** 装备 */
SOUND_EQUIP = 107,
/** 破坏 */
SOUND_DESTROYED = 108,
/** 除外 */
SOUND_BANISHED = 109,
/** 生成 */
SOUND_TOKEN = 110,
/** 攻击 */
SOUND_ATTACK = 201,
/** 直接攻击 */
SOUND_DIRECT_ATTACK = 202,
/** 抽卡 */
SOUND_DRAW = 203,
/** 洗卡 */
SOUND_SHUFFLE = 204,
/** 伤害 */
SOUND_DAMAGE = 205,
/** 恢复 */
SOUND_RECOVER = 206,
/** 计数增加 */
SOUND_COUNTER_ADD = 207,
/** 计数减少 */
SOUND_COUNTER_REMOVE = 208,
/** 抛硬币 */
SOUND_COIN = 209,
/** 抛骰子 */
SOUND_DICE = 210,
/** 下一回合 */
SOUND_NEXT_TURN = 211,
/** 阶段 */
SOUND_PHASE = 212,
/** 菜单 */
SOUND_MENU = 301,
/** 按钮 */
SOUND_BUTTON = 302,
/** 信息 */
SOUND_INFO = 303,
/** 问题 */
SOUND_QUESTION = 304,
/** 选卡 */
SOUND_CARD_PICK = 305,
/** 移出卡片 */
SOUND_CARD_DROP = 306,
/** 玩家进入 */
SOUND_PLAYER_ENTER = 307,
/** 聊天 */
SOUND_CHAT = 308,
/** 所有 */
BGM_ALL = 0,
/** 决斗 */
BGM_DUEL = 1,
/** 菜单 */
BGM_MENU = 2,
/** 卡组 */
BGM_DECK = 3,
/** 优势 */
BGM_ADVANTAGE = 4,
/** 劣势 */
BGM_DISADVANTAGE = 5,
/** 胜利 */
BGM_WIN = 6,
/** 失败 */
BGM_LOSE = 7,
}
...@@ -3,10 +3,6 @@ ...@@ -3,10 +3,6 @@
// //
// 因此封装了一个`WebSocketStream`类,当每次Websocket连接中有消息到达时,往流中添加event, // 因此封装了一个`WebSocketStream`类,当每次Websocket连接中有消息到达时,往流中添加event,
import { useConfig } from "@/config";
import { sleep } from "./sleep";
// 同时执行器会不断地从流中获取event进行处理。 // 同时执行器会不断地从流中获取event进行处理。
export class WebSocketStream { export class WebSocketStream {
public ws: WebSocket; public ws: WebSocket;
...@@ -32,8 +28,9 @@ export class WebSocketStream { ...@@ -32,8 +28,9 @@ export class WebSocketStream {
ws.onmessage = (event) => { ws.onmessage = (event) => {
controller.enqueue(event); controller.enqueue(event);
}; };
ws.onclose = () => { ws.onclose = (ev) => {
console.info("Websocket closed."); // 后续可能根据断线原因做处理,先暴露出来
console.info("Websocket closed.", ev);
// 下面这行注释掉,因为虽然websocket关掉了,但是已经收到的数据可能还在处理中 // 下面这行注释掉,因为虽然websocket关掉了,但是已经收到的数据可能还在处理中
// controller.close(); // controller.close();
}; };
...@@ -68,7 +65,11 @@ export class WebSocketStream { ...@@ -68,7 +65,11 @@ export class WebSocketStream {
if (value) { if (value) {
// wait some time, and then handle message from server // wait some time, and then handle message from server
await sleep(useConfig().streamInterval); //
// but now it seems that we don't need wait any more,
// so comment the following line and check if it's ok without it.
//
// await sleep(useConfig().streamInterval);
await onMessage(value); await onMessage(value);
} else { } else {
console.warn("value from ReadableStream is undefined!"); console.warn("value from ReadableStream is undefined!");
......
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { AudioActionType, playEffect } from "@/infra/audio";
import { cardStore, fetchEsHintMeta } from "@/stores"; import { cardStore, fetchEsHintMeta } from "@/stores";
import { callCardAttack } from "@/ui/Duel/PlayMat/Card"; import { callCardAttack } from "@/ui/Duel/PlayMat/Card";
...@@ -16,10 +17,12 @@ export default async (attack: ygopro.StocGameMessage.MsgAttack) => { ...@@ -16,10 +17,12 @@ export default async (attack: ygopro.StocGameMessage.MsgAttack) => {
if (attacker) { if (attacker) {
if (attack.direct_attack) { if (attack.direct_attack) {
playEffect(AudioActionType.SOUND_DIRECT_ATTACK);
await callCardAttack(attacker.uuid, { await callCardAttack(attacker.uuid, {
directAttack: true, directAttack: true,
}); });
} else { } else {
playEffect(AudioActionType.SOUND_ATTACK);
await callCardAttack(attacker.uuid, { await callCardAttack(attacker.uuid, {
directAttack: false, directAttack: false,
target: attack.target_location, target: attack.target_location,
......
import { fetchCard, ygopro } from "@/api"; import { fetchCard, ygopro } from "@/api";
import { AudioActionType, playEffect } from "@/infra/audio";
import { cardStore, fetchEsHintMeta, matStore, placeStore } from "@/stores"; import { cardStore, fetchEsHintMeta, matStore, placeStore } from "@/stores";
import { callCardFocus } from "@/ui/Duel/PlayMat/Card"; import { callCardFocus } from "@/ui/Duel/PlayMat/Card";
export default async (chaining: ygopro.StocGameMessage.MsgChaining) => { export default async (chaining: ygopro.StocGameMessage.MsgChaining) => {
playEffect(AudioActionType.SOUND_ACTIVATE);
fetchEsHintMeta({ fetchEsHintMeta({
originMsg: "「[?]」被发动时", originMsg: "「[?]」被发动时",
cardID: chaining.code, cardID: chaining.code,
......
import { fetchCard, ygopro } from "@/api"; import { fetchCard, ygopro } from "@/api";
import { sleep } from "@/infra"; import { sleep } from "@/infra";
import { AudioActionType, playEffect } from "@/infra/audio";
import { cardStore } from "@/stores"; import { cardStore } from "@/stores";
import { callCardFocus, callCardMove } from "@/ui/Duel/PlayMat/Card"; import { callCardFocus, callCardMove } from "@/ui/Duel/PlayMat/Card";
...@@ -10,6 +11,7 @@ const { FACEUP_ATTACK, FACEDOWN_ATTACK, FACEDOWN_DEFENSE, FACEDOWN } = ...@@ -10,6 +11,7 @@ const { FACEUP_ATTACK, FACEDOWN_ATTACK, FACEDOWN_DEFENSE, FACEDOWN } =
const WAIT_TIME = 100; const WAIT_TIME = 100;
export default async (confirmCards: ygopro.StocGameMessage.MsgConfirmCards) => { export default async (confirmCards: ygopro.StocGameMessage.MsgConfirmCards) => {
playEffect(AudioActionType.SOUND_REVEAL);
const cards = confirmCards.cards; const cards = confirmCards.cards;
console.color("pink")(`confirmCards: ${cards}`); console.color("pink")(`confirmCards: ${cards}`);
......
import { fetchCard, ygopro } from "@/api"; import { fetchCard, ygopro } from "@/api";
import { AudioActionType, playEffect } from "@/infra/audio";
import { cardStore, fetchEsHintMeta } from "@/stores"; import { cardStore, fetchEsHintMeta } from "@/stores";
import { callCardMove } from "@/ui/Duel/PlayMat/Card"; import { callCardMove } from "@/ui/Duel/PlayMat/Card";
...@@ -24,6 +25,8 @@ export default async (draw: ygopro.StocGameMessage.MsgDraw) => { ...@@ -24,6 +25,8 @@ export default async (draw: ygopro.StocGameMessage.MsgDraw) => {
card.location.sequence = Number(idx) + handsLength; card.location.sequence = Number(idx) + handsLength;
} }
playEffect(AudioActionType.SOUND_DRAW);
// 抽卡动画 // 抽卡动画
await Promise.all( await Promise.all(
cardStore cardStore
......
...@@ -2,6 +2,7 @@ import { ygopro } from "@/api"; ...@@ -2,6 +2,7 @@ import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores"; import { fetchEsHintMeta } from "@/stores";
export default (flipSummoning: ygopro.StocGameMessage.MsgFlipSummoning) => { export default (flipSummoning: ygopro.StocGameMessage.MsgFlipSummoning) => {
// playEffect(AudioActionType.SOUND_FILP);
fetchEsHintMeta({ fetchEsHintMeta({
originMsg: "「[?]」反转召唤宣言时", originMsg: "「[?]」反转召唤宣言时",
cardID: flipSummoning.code, cardID: flipSummoning.code,
......
import { fetchCard, ygopro } from "@/api"; import { fetchCard, ygopro } from "@/api";
import { AudioActionType, playEffect } from "@/infra/audio";
import { cardStore, CardType } from "@/stores"; import { cardStore, CardType } from "@/stores";
import { callCardMove } from "@/ui/Duel/PlayMat/Card"; import { callCardMove } from "@/ui/Duel/PlayMat/Card";
import { REASON_MATERIAL, TYPE_TOKEN } from "../../common"; import { REASON_DESTROY, REASON_MATERIAL, TYPE_TOKEN } from "../../common";
type MsgMove = ygopro.StocGameMessage.MsgMove; type MsgMove = ygopro.StocGameMessage.MsgMove;
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, TZONE } = ygopro.CardZone; const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE, TZONE } =
ygopro.CardZone;
const { FACEDOWN, FACEDOWN_ATTACK, FACEDOWN_DEFENSE } = ygopro.CardPosition;
const overlayStack: ygopro.CardLocation[] = []; const overlayStack: ygopro.CardLocation[] = [];
...@@ -161,7 +164,21 @@ export default async (move: MsgMove) => { ...@@ -161,7 +164,21 @@ export default async (move: MsgMove) => {
target.code = code; target.code = code;
target.location = to; target.location = to;
// 维护完了之后,开始动画 // 维护完了之后,开始播放音效和动画
if (to.zone === REMOVED) {
playEffect(AudioActionType.SOUND_BANISHED);
} else if (
(to.zone === MZONE || to.zone === SZONE) &&
(to.position === FACEDOWN ||
to.position === FACEDOWN_ATTACK ||
to.position === FACEDOWN_DEFENSE)
) {
playEffect(AudioActionType.SOUND_SET);
} else if (reason === REASON_DESTROY) {
playEffect(AudioActionType.SOUND_DESTROYED);
}
const p = callCardMove(target.uuid, { fromZone: from.zone }); const p = callCardMove(target.uuid, { fromZone: from.zone });
// 如果from或者to是手卡,那么需要刷新除了这张卡之外,这个玩家的所有手卡 // 如果from或者to是手卡,那么需要刷新除了这张卡之外,这个玩家的所有手卡
if ([from.zone, to.zone].includes(HAND)) { if ([from.zone, to.zone].includes(HAND)) {
......
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { AudioActionType, playEffect } from "@/infra/audio";
import { matStore } from "@/stores"; import { matStore } from "@/stores";
export default (newPhase: ygopro.StocGameMessage.MsgNewPhase) => { export default (newPhase: ygopro.StocGameMessage.MsgNewPhase) => {
playEffect(AudioActionType.SOUND_PHASE);
// ts本身还没有这么智能,所以需要手动指定类型 // ts本身还没有这么智能,所以需要手动指定类型
matStore.phase.currentPhase = newPhase.phase_type; matStore.phase.currentPhase = newPhase.phase_type;
}; };
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { AudioActionType, playEffect } from "@/infra/audio";
import { matStore } from "@/stores"; import { matStore } from "@/stores";
export default (newTurn: ygopro.StocGameMessage.MsgNewTurn) => { export default (newTurn: ygopro.StocGameMessage.MsgNewTurn) => {
playEffect(AudioActionType.SOUND_NEXT_TURN);
const player = newTurn.player; const player = newTurn.player;
matStore.currentPlayer = player; matStore.currentPlayer = player;
}; };
import { ygopro } from "@/api"; import { sendSelectPlaceResponse, ygopro } from "@/api";
import { InteractType, placeStore } from "@/stores"; import { InteractType, placeStore } from "@/stores";
type MsgSelectPlace = ygopro.StocGameMessage.MsgSelectPlace; type MsgSelectPlace = ygopro.StocGameMessage.MsgSelectPlace;
...@@ -9,6 +9,17 @@ export default (selectPlace: MsgSelectPlace) => { ...@@ -9,6 +9,17 @@ export default (selectPlace: MsgSelectPlace) => {
return; return;
} }
if (selectPlace.places.length === 1) {
const place = selectPlace.places[0];
sendSelectPlaceResponse({
controller: place.controller,
zone: place.zone,
sequence: place.sequence,
});
return;
}
for (const place of selectPlace.places) { for (const place of selectPlace.places) {
switch (place.zone) { switch (place.zone) {
case ygopro.CardZone.MZONE: case ygopro.CardZone.MZONE:
......
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { AudioActionType, playEffect } from "@/infra/audio";
import { cardStore } from "@/stores"; import { cardStore } from "@/stores";
export default (shuffleDeck: ygopro.StocGameMessage.MsgShuffleDeck) => { export default (shuffleDeck: ygopro.StocGameMessage.MsgShuffleDeck) => {
playEffect(AudioActionType.SOUND_SHUFFLE);
const player = shuffleDeck.player; const player = shuffleDeck.player;
for (const card of cardStore.at(ygopro.CardZone.DECK, player)) { for (const card of cardStore.at(ygopro.CardZone.DECK, player)) {
// 把数据抹掉就好了 // 把数据抹掉就好了
......
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { AudioActionType, playEffect } from "@/infra/audio";
import { cardStore } from "@/stores"; import { cardStore } from "@/stores";
import { callCardMove } from "@/ui/Duel/PlayMat/Card"; import { callCardMove } from "@/ui/Duel/PlayMat/Card";
type MsgShuffleHandExtra = ygopro.StocGameMessage.MsgShuffleHandExtra; type MsgShuffleHandExtra = ygopro.StocGameMessage.MsgShuffleHandExtra;
export default async (shuffleHandExtra: MsgShuffleHandExtra) => { export default async (shuffleHandExtra: MsgShuffleHandExtra) => {
playEffect(AudioActionType.SOUND_SHUFFLE);
const { cards: codes, player: controller, zone } = shuffleHandExtra; const { cards: codes, player: controller, zone } = shuffleHandExtra;
// 本质上是要将手卡/额外卡组的sequence变成和codes一样的顺序 // 本质上是要将手卡/额外卡组的sequence变成和codes一样的顺序
......
...@@ -2,9 +2,11 @@ import { ygopro } from "@/api"; ...@@ -2,9 +2,11 @@ import { ygopro } from "@/api";
import { cardStore } from "@/stores"; import { cardStore } from "@/stores";
import { callCardMove } from "@/ui/Duel/PlayMat/Card"; import { callCardMove } from "@/ui/Duel/PlayMat/Card";
import MsgShuffleSetCard = ygopro.StocGameMessage.MsgShuffleSetCard; import MsgShuffleSetCard = ygopro.StocGameMessage.MsgShuffleSetCard;
import { AudioActionType, playEffect } from "@/infra/audio";
// 后端传过来的`from_locations`的列表是切洗前场上卡的location,它们在列表里面按照切洗后的顺序排列 // 后端传过来的`from_locations`的列表是切洗前场上卡的location,它们在列表里面按照切洗后的顺序排列
export default async (shuffleSetCard: MsgShuffleSetCard) => { export default async (shuffleSetCard: MsgShuffleSetCard) => {
playEffect(AudioActionType.SOUND_SHUFFLE);
const from_locations = shuffleSetCard.from_locations; const from_locations = shuffleSetCard.from_locations;
const overlay_locations = shuffleSetCard.overlay_locations; const overlay_locations = shuffleSetCard.overlay_locations;
if (from_locations.length === 0) { if (from_locations.length === 0) {
......
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores"; import { fetchEsHintMeta } from "@/stores";
export default (spSummoning: ygopro.StocGameMessage.MsgSpSummoning) => { export default (spSummoning: ygopro.StocGameMessage.MsgSpSummoning) => {
// const card = fetchCard(spSummoning.code);
// if (card.data.type && card.data.type & TYPE_TOKEN) {
// playEffect(AudioActionType.SOUND_TOKEN);
// } else {
// playEffect(AudioActionType.SOUND_SPECIAL_SUMMON);
// }
fetchEsHintMeta({ fetchEsHintMeta({
originMsg: "「[?]」特殊召唤宣言时", originMsg: "「[?]」特殊召唤宣言时",
cardID: spSummoning.code, cardID: spSummoning.code,
......
...@@ -2,6 +2,10 @@ import { ygopro } from "@/api"; ...@@ -2,6 +2,10 @@ import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores"; import { fetchEsHintMeta } from "@/stores";
export default (summoning: ygopro.StocGameMessage.MsgSummoning) => { export default (summoning: ygopro.StocGameMessage.MsgSummoning) => {
/* 因为现在Neos动画架构的问题,这里播放音效的话会滞后于移动动画,
* 因此这里先注释掉,等解决掉上述问题后再加上召唤的音效。
* */
// playEffect(AudioActionType.SOUND_SUMMON);
fetchEsHintMeta({ fetchEsHintMeta({
originMsg: "「[?]」通常召唤宣言时", originMsg: "「[?]」通常召唤宣言时",
cardID: summoning.code, cardID: summoning.code,
......
...@@ -2,6 +2,7 @@ import { fetchStrings, Region, ygopro } from "@/api"; ...@@ -2,6 +2,7 @@ import { fetchStrings, Region, ygopro } from "@/api";
import { sleep } from "@/infra"; import { sleep } from "@/infra";
import { matStore } from "@/stores"; import { matStore } from "@/stores";
import MsgToss = ygopro.StocGameMessage.MsgToss; import MsgToss = ygopro.StocGameMessage.MsgToss;
import { AudioActionType, playEffect } from "@/infra/audio";
export default async (toss: MsgToss) => { export default async (toss: MsgToss) => {
const player = toss.player; const player = toss.player;
...@@ -11,8 +12,10 @@ export default async (toss: MsgToss) => { ...@@ -11,8 +12,10 @@ export default async (toss: MsgToss) => {
for (const x of toss.res) { for (const x of toss.res) {
if (tossType === MsgToss.TossType.DICE) { if (tossType === MsgToss.TossType.DICE) {
playEffect(AudioActionType.SOUND_DICE);
matStore.tossResult = prefix + fetchStrings(Region.System, 1624) + x; matStore.tossResult = prefix + fetchStrings(Region.System, 1624) + x;
} else if (tossType === MsgToss.TossType.COIN) { } else if (tossType === MsgToss.TossType.COIN) {
playEffect(AudioActionType.SOUND_COIN);
matStore.tossResult = matStore.tossResult =
prefix + prefix +
fetchStrings(Region.System, 1623) + fetchStrings(Region.System, 1623) +
......
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { AudioActionType, playEffect } from "@/infra/audio";
import { cardStore } from "@/stores"; import { cardStore } from "@/stores";
type MsgUpdateCounter = ygopro.StocGameMessage.MsgUpdateCounter; type MsgUpdateCounter = ygopro.StocGameMessage.MsgUpdateCounter;
...@@ -12,8 +13,10 @@ export default (updateCounter: MsgUpdateCounter) => { ...@@ -12,8 +13,10 @@ export default (updateCounter: MsgUpdateCounter) => {
case ygopro.StocGameMessage.MsgUpdateCounter.ActionType.ADD: { case ygopro.StocGameMessage.MsgUpdateCounter.ActionType.ADD: {
if (counterType in target.counters) { if (counterType in target.counters) {
target.counters[counterType] += count; target.counters[counterType] += count;
playEffect(AudioActionType.SOUND_COUNTER_ADD);
} else { } else {
target.counters[counterType] = count; target.counters[counterType] = count;
playEffect(AudioActionType.SOUND_COUNTER_REMOVE);
} }
break; break;
} }
......
...@@ -2,13 +2,22 @@ import { ygopro } from "@/api"; ...@@ -2,13 +2,22 @@ import { ygopro } from "@/api";
import { fetchEsHintMeta, matStore } from "@/stores"; import { fetchEsHintMeta, matStore } from "@/stores";
import MsgUpdateHp = ygopro.StocGameMessage.MsgUpdateHp; import MsgUpdateHp = ygopro.StocGameMessage.MsgUpdateHp;
import { AudioActionType, changeScene, playEffect } from "@/infra/audio";
export default (msgUpdateHp: MsgUpdateHp) => { export default (msgUpdateHp: MsgUpdateHp) => {
if (msgUpdateHp.type_ === MsgUpdateHp.ActionType.DAMAGE) { if (msgUpdateHp.type_ === MsgUpdateHp.ActionType.DAMAGE) {
playEffect(AudioActionType.SOUND_DAMAGE);
fetchEsHintMeta({ originMsg: "玩家收到伤害时" }); // TODO: i18n fetchEsHintMeta({ originMsg: "玩家收到伤害时" }); // TODO: i18n
matStore.initInfo.of(msgUpdateHp.player).life -= msgUpdateHp.value; matStore.initInfo.of(msgUpdateHp.player).life -= msgUpdateHp.value;
} else if (msgUpdateHp.type_ === MsgUpdateHp.ActionType.RECOVER) { } else if (msgUpdateHp.type_ === MsgUpdateHp.ActionType.RECOVER) {
playEffect(AudioActionType.SOUND_RECOVER);
fetchEsHintMeta({ originMsg: "玩家生命值回复时" }); // TODO: i18n fetchEsHintMeta({ originMsg: "玩家生命值回复时" }); // TODO: i18n
matStore.initInfo.of(msgUpdateHp.player).life += msgUpdateHp.value; matStore.initInfo.of(msgUpdateHp.player).life += msgUpdateHp.value;
} }
if (matStore.initInfo.me.life > matStore.initInfo.op.life * 2) {
changeScene(AudioActionType.BGM_ADVANTAGE);
}
if (matStore.initInfo.me.life * 2 < matStore.initInfo.op.life) {
changeScene(AudioActionType.BGM_DISADVANTAGE);
}
}; };
...@@ -2,6 +2,7 @@ import { fetchStrings, Region, ygopro } from "@/api"; ...@@ -2,6 +2,7 @@ import { fetchStrings, Region, ygopro } from "@/api";
import { matStore } from "@/stores"; import { matStore } from "@/stores";
import { displayEndModal } from "@/ui/Duel/Message"; import { displayEndModal } from "@/ui/Duel/Message";
import MsgWin = ygopro.StocGameMessage.MsgWin; import MsgWin = ygopro.StocGameMessage.MsgWin;
import { AudioActionType, changeScene } from "@/infra/audio";
export default async (win: MsgWin) => { export default async (win: MsgWin) => {
const { win_player, reason } = win; const { win_player, reason } = win;
...@@ -10,4 +11,10 @@ export default async (win: MsgWin) => { ...@@ -10,4 +11,10 @@ export default async (win: MsgWin) => {
matStore.isMe(win_player), matStore.isMe(win_player),
fetchStrings(Region.Victory, `0x${reason.toString(16)}`), fetchStrings(Region.Victory, `0x${reason.toString(16)}`),
); );
if (matStore.isMe(win_player)) {
changeScene(AudioActionType.BGM_WIN);
} else {
changeScene(AudioActionType.BGM_LOSE);
}
}; };
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { AudioActionType, playEffect } from "@/infra/audio";
import { chatStore } from "@/stores"; import { chatStore } from "@/stores";
export default function handleChat(pb: ygopro.YgoStocMsg) { export default function handleChat(pb: ygopro.YgoStocMsg) {
playEffect(AudioActionType.SOUND_CHAT);
const chat = pb.stoc_chat; const chat = pb.stoc_chat;
chatStore.message = chat.msg; chatStore.message = chat.msg;
chatStore.sender = chat.player; chatStore.sender = chat.player;
......
import { fetchCard, fetchStrings, Region, ygopro } from "@/api"; import { fetchCard, fetchStrings, Region, ygopro } from "@/api";
import { roomStore } from "@/stores"; import { roomStore } from "@/stores";
import ErrorType = ygopro.StocErrorMsg.ErrorType; import ErrorType = ygopro.StocErrorMsg.ErrorType;
import { AudioActionType, playEffect } from "@/infra/audio";
// TODO: 是时候需要一个统一管理国际化文案的模块了 // TODO: 是时候需要一个统一管理国际化文案的模块了
...@@ -22,6 +23,7 @@ const mainDeckWarining = ...@@ -22,6 +23,7 @@ const mainDeckWarining =
export default async function handleErrorMsg(errorMsg: ygopro.StocErrorMsg) { export default async function handleErrorMsg(errorMsg: ygopro.StocErrorMsg) {
const { error_type, error_code } = errorMsg; const { error_type, error_code } = errorMsg;
playEffect(AudioActionType.SOUND_INFO);
switch (error_type) { switch (error_type) {
case ErrorType.JOINERROR: { case ErrorType.JOINERROR: {
roomStore.errorMsg = fetchStrings(Region.System, 1403 + error_code); roomStore.errorMsg = fetchStrings(Region.System, 1403 + error_code);
......
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { AudioActionType, playEffect } from "@/infra/audio";
import { roomStore } from "@/stores"; import { roomStore } from "@/stores";
export default function handleHsPlayerEnter(pb: ygopro.YgoStocMsg) { export default function handleHsPlayerEnter(pb: ygopro.YgoStocMsg) {
playEffect(AudioActionType.SOUND_PLAYER_ENTER);
const name = pb.stoc_hs_player_enter.name; const name = pb.stoc_hs_player_enter.name;
const pos = pb.stoc_hs_player_enter.pos; const pos = pb.stoc_hs_player_enter.pos;
......
...@@ -50,6 +50,7 @@ const helper = async ( ...@@ -50,6 +50,7 @@ const helper = async (
level2, level2,
effectDesc, effectDesc,
response, response,
targeted: target?.targeted,
}; };
if (selected) { if (selected) {
......
...@@ -3,7 +3,7 @@ import { proxy } from "valtio"; ...@@ -3,7 +3,7 @@ import { proxy } from "valtio";
import { type NeosStore } from "./shared"; import { type NeosStore } from "./shared";
export interface User { export interface User {
id: string; id: number;
username: string; username: string;
name: string; name: string;
email: string; email: string;
......
...@@ -26,11 +26,17 @@ export const deckStore = proxy({ ...@@ -26,11 +26,17 @@ export const deckStore = proxy({
const index = deckStore.decks.findIndex( const index = deckStore.decks.findIndex(
(deck) => deck.deckName === deckName, (deck) => deck.deckName === deckName,
); );
if (index === -1) return false; if (index === -1) {
deckStore.decks[index] = deck; // if not existed, create one
await del(deckName, deckIdb); // 新的名字可能和旧的名字不一样,所以要删除旧的,再添加 await deckStore.add(deck);
await set(deck.deckName, deck, deckIdb); return true;
return true; } else {
deckStore.decks[index] = deck;
// 新的名字可能和旧的名字不一样,所以要删除旧的,再添加
await del(deckName, deckIdb);
await set(deck.deckName, deck, deckIdb);
return true;
}
}, },
async add(deck: IDeck): Promise<boolean> { async add(deck: IDeck): Promise<boolean> {
......
/** 音频设置 */
export interface AudioConfig {
/** 是否开启音乐 */
enableMusic?: boolean;
/** 是否开启音效 */
enableSoundEffects?: boolean;
/** 音乐音量大小 */
musicVolume?: number;
/** 音效音量大小 */
soundEffectsVolume?: number;
/** 是否根据环境切换音乐 */
enableMusicSwitchByEnv?: boolean;
}
export const defaultAudioConfig: AudioConfig = {
enableMusic: false,
enableSoundEffects: false,
musicVolume: 0.7,
soundEffectsVolume: 0.7,
enableMusicSwitchByEnv: false,
};
import { isSSR } from "@react-spring/shared";
import { pick } from "lodash-es";
import { proxy, subscribe } from "valtio";
import { type NeosStore } from "../shared";
import { AudioConfig, defaultAudioConfig } from "./audio";
/** 将设置保存到本地 */
const NEO_SETTING_CONFIG = "__neo_setting_config__";
/** 设置项 */
type SettingStoreConfig = Pick<SettingStore, "audio">;
/** 默认设置 */
const defaultSettingConfig: SettingStoreConfig = {
audio: defaultAudioConfig,
};
/** 获取默认设置 */
function getDefaultSetting() {
if (!isSSR()) {
/** 获取默认设置 */
const setting = localStorage.getItem(NEO_SETTING_CONFIG);
if (setting) return JSON.parse(setting) as SettingStoreConfig;
}
return defaultSettingConfig;
}
const defaultSetting = getDefaultSetting();
/** 设置模块 */
class SettingStore implements NeosStore {
/** 音频设置 */
audio: AudioConfig = defaultSetting.audio;
/** 保存音频设置 */
saveAudioConfig(config: Partial<AudioConfig>): void {
Object.assign(this.audio, config);
}
reset(): void {
const defaultSetting = getDefaultSetting();
this.audio = defaultSetting.audio;
}
}
/** 设置项 */
export const settingStore = proxy(new SettingStore());
/** 持久化设置项 */
subscribe(settingStore, () => {
if (!isSSR()) {
localStorage.setItem(
NEO_SETTING_CONFIG,
JSON.stringify(pick(settingStore, ["audio"])),
);
}
});
...@@ -15,7 +15,7 @@ import { ...@@ -15,7 +15,7 @@ import {
} from "@/common"; } from "@/common";
import { CardEffectText, IconFont, ScrollableArea, YgoCard } from "@/ui/Shared"; import { CardEffectText, IconFont, ScrollableArea, YgoCard } from "@/ui/Shared";
import styles from "./CardDetail.module.scss"; import styles from "./index.module.scss";
export const CardDetail: React.FC<{ export const CardDetail: React.FC<{
code: number; code: number;
......
.search-cards {
--card-width: 5rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--card-width), 1fr));
padding: 0.75rem;
gap: 0.625rem;
}
.empty {
gap: 1.25rem;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
import { message, Pagination } from "antd";
import React, { memo, useEffect, useState } from "react";
import { CardMeta } from "@/api";
import { isExtraDeckCard } from "@/common";
import { DeckCard, DeckCardMouseUpEvent, IconFont } from "@/ui/Shared";
import { selectedCard } from "../..";
import { editDeckStore } from "../../store";
import styles from "./index.module.scss";
/** 搜索区的搜索结果,使用memo避免重复渲染 */
export const CardResults: React.FC<{
results: CardMeta[];
scrollToTop: () => void;
}> = memo(({ results, scrollToTop }) => {
const itemsPerPage = 196; // 每页显示的数据数量
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
setCurrentPage(1);
}, [results]);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentData = results.slice(startIndex, endIndex);
const showSelectedCard = (card: CardMeta) => {
selectedCard.id = card.id;
selectedCard.open = true;
};
const handleAddCardToMain = (card: CardMeta) => {
const cardType = card.data.type ?? 0;
const isExtraCard = isExtraDeckCard(cardType);
const type = isExtraCard ? "extra" : "main";
const { result, reason } = editDeckStore.canAdd(card, type, "search");
if (result) {
editDeckStore.add(type, card);
} else {
message.error(reason);
}
};
const handleAddCardToSide = (card: CardMeta) => {
const { result, reason } = editDeckStore.canAdd(card, "side", "search");
if (result) {
editDeckStore.add("side", card);
} else {
message.error(reason);
}
};
/** safari 不支持 onAuxClick,所以使用 mousedown 模拟 */
const handleMouseUp = (payload: DeckCardMouseUpEvent) => {
const { event, card } = payload;
switch (event.button) {
// 左键
case 0:
showSelectedCard(card);
break;
// 中键
case 1:
handleAddCardToSide(card);
break;
// 右键
case 2:
handleAddCardToMain(card);
break;
default:
break;
}
};
return (
<>
{results.length ? (
<>
<div className={styles["search-cards"]}>
{currentData.map((card) => (
<DeckCard
value={card}
key={card.id}
source="search"
onMouseUp={handleMouseUp}
onMouseEnter={() => showSelectedCard(card)}
/>
))}
</div>
{results.length > itemsPerPage && (
<div style={{ textAlign: "center", padding: "0.625rem 0 1.25rem" }}>
<Pagination
current={currentPage}
onChange={(page) => {
setCurrentPage(page);
scrollToTop();
}}
total={results.length}
pageSize={itemsPerPage}
showSizeChanger={false}
showLessItems
hideOnSinglePage
/>
</div>
)}
</>
) : (
<div className={styles.empty}>
<IconFont type="icon-empty" size={40} />
<div>找不到相应卡片</div>
</div>
)}
</>
);
});
.container {
width: 100%;
}
.search-decks {
--deck-width: 8rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--deck-width), 1fr));
padding: 0.75rem;
gap: 0.5rem;
}
.mdpro-deck {
position: relative;
display: flex;
width: 8rem;
height: 8rem;
background-color: black;
justify-content: center;
align-items: center;
flex-direction: column;
border: 1px solid grey;
border-radius: 10% 2%;
cursor: pointer;
&:hover {
background-color: grey;
}
img {
width: 4rem;
}
.text {
text-align: center;
font-size: 0.8rem;
}
}
.copy-btn {
background-color: #4B4B4B;
}
.empty {
gap: 1.25rem;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
import { App, Dropdown, message, Pagination } from "antd";
import { MessageInstance } from "antd/es/message/interface";
import Fuse from "fuse.js";
import React, { memo, useEffect } from "react";
import { type INTERNAL_Snapshot as Snapshot, proxy, useSnapshot } from "valtio";
import YGOProDeck from "ygopro-deck-encode";
import { deleteDeck, getPersonalList, mgetDeck, pullDecks } from "@/api";
import { MdproDeckLike } from "@/api/mdproDeck/schema";
import { useConfig } from "@/config";
import { accountStore } from "@/stores";
import { IconFont } from "@/ui/Shared";
import { setSelectedDeck } from "../..";
import { editDeckStore } from "../../store";
import { iDeckToEditingDeck } from "../../utils";
import styles from "./index.module.scss";
const { assetsPath } = useConfig();
interface Props {
query: string;
page: number;
decks: MdproDeckLike[];
total: number;
onlyMine: boolean;
}
// TODO: useConfig
const PAGE_SIZE = 30;
const SORT_LIKE = true;
const store = proxy<Props>({
query: "",
page: 1,
decks: [],
total: 0,
onlyMine: false,
});
export const DeckResults: React.FC = memo(() => {
const snap = useSnapshot(store);
const { message } = App.useApp();
useEffect(() => {
if (snap.onlyMine) {
// show only decks uploaded by myself
updatePersonalList(message);
} else {
const update = async () => {
const resp = await pullDecks({
page: snap.page,
size: PAGE_SIZE,
keyWord: snap.query !== "" ? snap.query : undefined,
sortLike: SORT_LIKE,
});
if (resp?.data) {
const { total, records: newDecks } = resp.data;
store.total = total;
store.decks = newDecks;
} else {
store.decks = [];
}
};
update();
}
}, [snap.query, snap.page, snap.onlyMine]);
const onChangePage = async (page: number) => {
const resp = await pullDecks({
page,
size: PAGE_SIZE,
keyWord: store.query !== "" ? store.query : undefined,
sortLike: SORT_LIKE,
});
if (resp?.data) {
const { current, total, records } = resp.data;
store.page = current;
store.total = total;
store.decks = records;
} else if (resp?.code !== 0) {
message.error(resp?.message);
} else {
message.error("翻页失败,请检查您的网络状况。");
}
};
return (
<>
{snap.decks.length ? (
<div className={styles.container}>
<div className={styles["search-decks"]}>
{snap.decks.map((deck) => (
<MdproDeckBlock
key={deck.deckId}
deck={deck}
onlyMine={snap.onlyMine}
/>
))}
</div>
<div style={{ textAlign: "center", padding: "0.625rem 0 1.25rem" }}>
<Pagination
current={snap.page}
onChange={onChangePage}
total={snap.total}
pageSize={PAGE_SIZE}
showLessItems
hideOnSinglePage
/>
</div>
</div>
) : (
<div className={styles.empty}>
<IconFont type="icon-empty" size={40} />
<div>找不到相应卡组</div>
</div>
)}
</>
);
});
const MdproDeckBlock: React.FC<{
deck: Snapshot<MdproDeckLike>;
onlyMine: boolean;
}> = ({ deck, onlyMine }) => {
const { message } = App.useApp();
const user = accountStore.user;
const onDelete = async () => {
if (user) {
const resp = await deleteDeck(user.id, user.token, deck.deckId);
if (resp?.code === 0 && resp.data === true) {
message.success(
"删除卡组成功,由于缓存的原因请稍等片刻后重新刷新页面。",
);
// fresh when deletion succeed
await updatePersonalList(message);
} else if (resp !== undefined && resp.message !== "") {
message.error(resp.message);
} else {
message.error("删除卡组失败,请检查自己的网络状况。");
}
} else {
message.error("需要先登录萌卡才能删除卡组。");
}
};
const items = [];
if (onlyMine) {
items.push({ key: 0, label: "删除", danger: true, onClick: onDelete });
}
return (
<Dropdown
menu={{
items,
}}
trigger={["contextMenu"]}
>
<div
className={styles["mdpro-deck"]}
onClick={async () => await copyMdproDeckToEditing(deck)}
>
<img
src={`${assetsPath}/deck-cases/DeckCase${deck.deckCase
.toString()
.slice(-4)}_L.png`}
/>
<div className={styles.text}>
<div>{truncateString(deck.deckName, 8)}</div>
<div>{`By ${truncateString(deck.deckContributor, 6)}`}</div>
</div>
</div>
</Dropdown>
);
};
const updatePersonalList = async (message: MessageInstance) => {
const user = accountStore.user;
if (user) {
const resp = await getPersonalList({
userID: user.id,
token: user.token,
});
if (resp) {
if (resp.code !== 0 || resp.data === undefined) {
message.error(resp.message);
} else {
let decks = resp.data;
if (store.query !== "") {
// use `fuse.js` to search
const fuse = new Fuse(decks, {
keys: ["deckName"],
includeScore: true,
threshold: 0.3,
});
const results = fuse.search(store.query);
decks = results.map((result) => result.item);
}
const total = decks.length;
store.total = total;
if (total === 0) {
store.page = 1;
store.decks = [];
} else {
if (total <= (store.page - 1) * PAGE_SIZE)
store.page = Math.floor((total - 1) / PAGE_SIZE) + 1;
store.decks = decks.slice(
(store.page - 1) * PAGE_SIZE,
store.page * PAGE_SIZE,
);
}
}
} else {
message.error("获取个人卡组列表失败,请检查自己的网络状况。");
}
} else {
message.error("需要先登录萌卡账号才能查看自己的在线卡组");
// set to default
store.page = 1;
store.decks = [];
store.total = 0;
}
};
const copyMdproDeckToEditing = async (mdproDeck: MdproDeckLike) => {
// currently the content of the deck, which we named `Ydk`,
// haven't been downloaded, so we need to fetch from server again by `mgetDeck`
// API.
const deckID = mdproDeck.deckId;
const resp = await mgetDeck(deckID);
if (resp?.code !== 0) {
message.error(resp?.message);
} else if (resp.data?.deckYdk !== undefined) {
const deck = YGOProDeck.fromYdkString(resp.data.deckYdk);
if (!(deck.main.length + deck.extra.length + deck.side.length === 0)) {
const deckName = mdproDeck.deckName;
const ideck = { deckName, ...deck };
const editingDeck = await iDeckToEditingDeck(ideck);
setSelectedDeck(ideck);
editDeckStore.set(editingDeck);
} else {
message.error("卡组解析失败,请联系技术人员解决:<ccc@neos.moe>");
}
} else {
message.error("卡组复制失败,请联系技术人员结局:<ccc@neos.moe>");
}
};
function truncateString(str: string, maxLen: number): string {
const length = Array.from(str).length;
if (length <= maxLen) {
return str;
}
const start = Array.from(str).slice(0, 3).join("");
const end = Array.from(str).slice(-3).join("");
return `${start}...${end}`;
}
export const freshMdrpoDecks = (query: string, onlyMine?: boolean) => {
store.query = query;
if (onlyMine !== undefined) store.onlyMine = onlyMine;
};
import {
DeleteOutlined,
FilterOutlined,
SearchOutlined,
SortAscendingOutlined,
SwapOutlined,
} from "@ant-design/icons";
import { App, Button, Dropdown, Input, Space } from "antd";
import { MenuProps } from "antd/lib";
import { isEqual } from "lodash-es";
import { OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useDrop } from "react-dnd";
import { CardMeta, searchCards } from "@/api";
import { isToken } from "@/common";
import { emptySearchConditions, FtsConditions } from "@/middleware/sqlite/fts";
import { ScrollableArea, Select, Type } from "@/ui/Shared";
import { Filter } from "../Filter";
import styles from "../index.module.scss";
import { editDeckStore } from "../store";
import { CardResults } from "./CardResults";
import { DeckResults, freshMdrpoDecks } from "./DeckResults";
/** 卡片库,选择卡片加入正在编辑的卡组 */
export const DeckDatabase: React.FC = () => {
const { modal } = App.useApp();
const [searchWord, setSearchWord] = useState("");
const [searchConditions, setSearchConditions] = useState<FtsConditions>(
emptySearchConditions,
);
const [searchCardResult, setSearchCardResult] = useState<CardMeta[]>([]);
const defaultSort = (a: CardMeta, b: CardMeta) => a.id - b.id;
const sortRef = useRef<(a: CardMeta, b: CardMeta) => number>(defaultSort);
const [sortEdited, setSortEdited] = useState(false);
const [showMdproDecks, setShowMdproDecks] = useState(false);
const setSortRef = (sort: (a: CardMeta, b: CardMeta) => number) => {
sortRef.current = sort;
setSearchCardResult([...searchCardResult.sort(sortRef.current)]);
setSortEdited(true);
};
const genSort = (key: keyof CardMeta["data"], scale: 1 | -1 = 1) => {
return () =>
setSortRef(
(a: CardMeta, b: CardMeta) =>
((a.data?.[key] ?? 0) - (b.data?.[key] ?? 0)) * scale,
);
};
const dropdownOptions: MenuProps["items"] = (
[
["从新到旧", () => setSortRef((a, b) => b.id - a.id)],
["从旧到新", () => setSortRef((a, b) => a.id - b.id)],
["攻击力从高到低", genSort("atk", -1)],
["攻击力从低到高", genSort("atk")],
["守备力从高到低", genSort("def", -1)],
["守备力从低到高", genSort("def")],
["星/阶/刻/Link从高到低", genSort("level", -1)],
["星/阶/刻/Link从低到高", genSort("level")],
["灵摆刻度从高到低", genSort("lscale", -1)],
["灵摆刻度从低到高", genSort("lscale")],
] as const
).map(([label, onClick], key) => ({ key, label, onClick }));
const handleSearch = (conditions: FtsConditions = searchConditions) => {
if (showMdproDecks) {
freshMdrpoDecks(searchWord);
} else {
const result = searchCards({ query: searchWord, conditions })
.filter((card) => !isToken(card.data.type ?? 0))
.sort(sortRef.current); // 衍生物不显示
setSearchCardResult(() => result);
}
};
useEffect(() => {
handleSearch();
}, []);
const [_, dropRef] = useDrop({
accept: ["Card"], // 指明该区域允许接收的拖放物。可以是单个,也可以是数组
// 里面的值就是useDrag所定义的type
// 当拖拽物在这个拖放区域放下时触发,这个item就是拖拽物的item(拖拽物携带的数据)
drop: ({ value, source }: { value: CardMeta; source: Type | "search" }) => {
if (source !== "search") {
editDeckStore.remove(source, value);
}
},
});
const showFilterModal = () => {
const { destroy } = modal.info({
width: 500,
centered: true,
title: null,
icon: null,
content: (
<Filter
conditions={searchConditions}
onConfirm={(newConditions) => {
setSearchConditions(newConditions);
destroy();
setTimeout(() => handleSearch(newConditions), 200); // 先收起再搜索
}}
onCancel={() => destroy()}
/>
),
footer: null,
});
};
/** 滚动条的ref,用来在翻页之后快速回顶 */
const ref = useRef<OverlayScrollbarsComponentRef<"div">>(null);
const scrollToTop = useCallback(() => {
const viewport = ref.current?.osInstance()?.elements().viewport;
if (viewport) viewport.scrollTop = 0;
}, []);
return (
<div className={styles.container} ref={dropRef}>
<Space className={styles.title} direction="horizontal">
<Input
placeholder="关键词(空格分隔)"
bordered={false}
suffix={
<Button
type="text"
icon={<SearchOutlined />}
onClick={() => handleSearch()}
/>
}
value={searchWord}
onChange={(e) => setSearchWord(e.target.value)}
onKeyUp={(e) => e.key === "Enter" && handleSearch()}
allowClear
style={{ width: "250%" }}
/>
<Button
style={{ marginRight: "1rem" }}
icon={<SwapOutlined />}
onClick={() => setShowMdproDecks(!showMdproDecks)}
>
{showMdproDecks ? "卡片数据库" : "Mdpro在线卡组"}
</Button>
</Space>
<div className={styles["select-btns"]}>
{showMdproDecks ? (
<Select
title=""
style={{ width: "18.90rem" }}
defaultValue={false}
options={[
{ value: true, label: "只显示我上传的卡组" },
{ value: false, label: "显示全部在线卡组" },
]}
onChange={
// @ts-ignore
(value) => freshMdrpoDecks(searchWord, value)
}
/>
) : (
<Button
block
type={
isEqual(emptySearchConditions, searchConditions)
? "text"
: "primary"
}
disabled={showMdproDecks}
icon={<FilterOutlined />}
onClick={showFilterModal}
>
筛选
</Button>
)}
<Dropdown
menu={{ items: dropdownOptions }}
disabled={showMdproDecks}
trigger={["click"]}
placement="bottom"
arrow
>
<Button
block
type={sortEdited ? "primary" : "text"}
icon={<SortAscendingOutlined />}
>
<span>
排列
<span className={styles["search-count"]}>
({searchCardResult.length})
</span>
</span>
</Button>
</Dropdown>
<Button
block
type="text"
disabled={showMdproDecks}
icon={<DeleteOutlined />}
onClick={() => {
setSearchConditions(emptySearchConditions);
setSortRef(defaultSort);
setSortEdited(false);
handleSearch(emptySearchConditions);
}}
>
重置
</Button>
</div>
<ScrollableArea className={styles["search-cards-container"]} ref={ref}>
{showMdproDecks ? (
<DeckResults />
) : (
<CardResults results={searchCardResult} scrollToTop={scrollToTop} />
)}
</ScrollableArea>
</div>
);
};
...@@ -4,19 +4,24 @@ import { ...@@ -4,19 +4,24 @@ import {
DownloadOutlined, DownloadOutlined,
FileAddOutlined, FileAddOutlined,
PlusOutlined, PlusOutlined,
UploadOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { App, Button, Dropdown, MenuProps, UploadProps } from "antd"; import { App, Button, Dropdown, MenuProps, UploadProps } from "antd";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import YGOProDeck from "ygopro-deck-encode"; import YGOProDeck from "ygopro-deck-encode";
import { deckStore, IDeck } from "@/stores"; import { uploadDeck } from "@/api";
import { accountStore, deckStore, IDeck } from "@/stores";
import { Uploader } from "../Shared"; import { Uploader } from "../../Shared";
import styles from "./DeckSelect.module.scss"; import { genYdkText } from "../utils";
import styles from "./index.module.scss";
const DEFAULT_DECK_CASE = 1082012;
export const DeckSelect: React.FC<{ export const DeckSelect: React.FC<{
decks: readonly { deckName: string }[]; decks: IDeck[];
selected: string; selected: string;
onSelect: (deckName: string) => any; onSelect: (deckName: string) => any;
onDelete: (deckName: string) => Promise<any>; onDelete: (deckName: string) => Promise<any>;
...@@ -122,25 +127,53 @@ export const DeckSelect: React.FC<{ ...@@ -122,25 +127,53 @@ export const DeckSelect: React.FC<{
}, },
].map((_, key) => ({ ..._, key })); ].map((_, key) => ({ ..._, key }));
const onUploadMdDeck = async (deck: IDeck) => {
const user = accountStore.user;
if (user) {
// TODO: Deck Case
const resp = await uploadDeck({
userId: user.id,
token: user.token,
deckContributor: user.username,
deck: {
deckName: deck.deckName,
deckCase: DEFAULT_DECK_CASE,
deckYdk: genYdkText(deck),
},
});
if (resp) {
if (resp.code) {
message.error(resp.message);
} else {
message.success(`上传在线卡组<${deck.deckName}>成功!`);
}
} else {
message.error("上传在线卡组失败,请检查网络状况。");
}
} else {
message.error("需要先登录萌卡账号才能上传在线卡组!");
}
};
return ( return (
<> <>
<div className={styles["deck-select"]}> <div className={styles["deck-select"]}>
{decks.map(({ deckName }) => ( {decks.map((deck) => (
<div <div
key={deckName} key={deck.deckName}
className={styles.item} className={styles.item}
onClick={() => onSelect(deckName)} onClick={() => onSelect(deck.deckName)}
> >
<div className={styles.hover} /> <div className={styles.hover} />
{selected === deckName && <div className={styles.selected} />} {selected === deck.deckName && <div className={styles.selected} />}
<span>{deckName}</span> <span>{deck.deckName}</span>
<div className={styles.btns}> <div className={styles.btns}>
<Button <Button
icon={<CopyOutlined />} icon={<CopyOutlined />}
type="text" type="text"
size="small" size="small"
onClick={cancelBubble(async () => { onClick={cancelBubble(async () => {
const result = await onCopy(deckName); const result = await onCopy(deck.deckName);
result result
? message.success(`${i18n("CopySuccessful")}`) ? message.success(`${i18n("CopySuccessful")}`)
: message.error(`${i18n("CopyFailed")}`); : message.error(`${i18n("CopyFailed")}`);
...@@ -151,7 +184,7 @@ export const DeckSelect: React.FC<{ ...@@ -151,7 +184,7 @@ export const DeckSelect: React.FC<{
type="text" type="text"
size="small" size="small"
onClick={cancelBubble(async () => { onClick={cancelBubble(async () => {
await onDelete(deckName); await onDelete(deck.deckName);
onSelect(decks[0].deckName); onSelect(decks[0].deckName);
})} })}
/> />
...@@ -160,7 +193,13 @@ export const DeckSelect: React.FC<{ ...@@ -160,7 +193,13 @@ export const DeckSelect: React.FC<{
icon={<DownloadOutlined />} icon={<DownloadOutlined />}
type="text" type="text"
size="small" size="small"
onClick={cancelBubble(() => onDownload(deckName))} onClick={cancelBubble(() => onDownload(deck.deckName))}
/>
<Button
icon={<UploadOutlined />}
type="text"
size="small"
onClick={cancelBubble(async () => onUploadMdDeck(deck))}
/> />
</div> </div>
</div> </div>
......
...@@ -17,7 +17,7 @@ import { ...@@ -17,7 +17,7 @@ import {
} from "@/common"; } from "@/common";
import { FtsConditions } from "@/middleware/sqlite/fts"; import { FtsConditions } from "@/middleware/sqlite/fts";
import styles from "./Filter.module.scss"; import styles from "./index.module.scss";
const levels = Array.from({ length: 12 }, (_, index) => ({ const levels = Array.from({ length: 12 }, (_, index) => ({
value: index + 1, value: index + 1,
......
...@@ -74,29 +74,12 @@ ...@@ -74,29 +74,12 @@
.search-cards-container { .search-cards-container {
height: 100%; height: 100%;
.search-cards {
--card-width: 5rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--card-width), 1fr));
padding: 0.75rem;
gap: 0.625rem;
}
} }
.search-count { .search-count {
font-size: 0.7rem; font-size: 0.7rem;
} }
.empty {
gap: 1.25rem;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.editing-zone-name { .editing-zone-name {
position: absolute; position: absolute;
right: 0; right: 0;
......
...@@ -2,39 +2,23 @@ import { ...@@ -2,39 +2,23 @@ import {
CheckOutlined, CheckOutlined,
DeleteOutlined, DeleteOutlined,
EditOutlined, EditOutlined,
FilterOutlined,
QuestionCircleOutlined, QuestionCircleOutlined,
RetweetOutlined, RetweetOutlined,
SearchOutlined,
SortAscendingOutlined,
SwapOutlined, SwapOutlined,
UndoOutlined, UndoOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { import { App, Button, Dropdown, Input, MenuProps, message, Pagination, Space, Tooltip } from "antd";
App,
Button,
Dropdown,
Input,
type MenuProps,
message,
Pagination,
Space,
Tooltip,
} from "antd";
import { isEqual } from "lodash-es";
import { type OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import { HTML5toTouch } from "rdndmb-html5-to-touch"; import { HTML5toTouch } from "rdndmb-html5-to-touch";
import { memo, useCallback, useEffect, useRef, useState } from "react"; import { memo, useCallback, useEffect, useRef, useState } from "react";
import { useDrop } from "react-dnd";
import { DndProvider } from "react-dnd-multi-backend"; import { DndProvider } from "react-dnd-multi-backend";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { LoaderFunction } from "react-router-dom"; import { LoaderFunction } from "react-router-dom";
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { subscribeKey } from "valtio/utils"; import { subscribeKey } from "valtio/utils";
import { type CardMeta, searchCards } from "@/api"; import { searchCards, type CardMeta } from "@/api";
import { isExtraDeckCard, isToken } from "@/common"; import { isExtraDeckCard, isToken } from "@/common";
import { emptySearchConditions, FtsConditions } from "@/middleware/sqlite/fts"; import { AudioActionType, changeScene } from "@/infra/audio";
import { deckStore, emptyDeck, type IDeck, initStore } from "@/stores"; import { deckStore, emptyDeck, type IDeck, initStore } from "@/stores";
import { import {
Background, Background,
...@@ -48,8 +32,8 @@ import { ...@@ -48,8 +32,8 @@ import {
import { Type } from "@/ui/Shared/DeckZone"; import { Type } from "@/ui/Shared/DeckZone";
import { CardDetail } from "./CardDetail"; import { CardDetail } from "./CardDetail";
import { DeckDatabase } from "./DeckDatabase";
import { DeckSelect } from "./DeckSelect"; import { DeckSelect } from "./DeckSelect";
import { Filter } from "./Filter";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
import { editDeckStore } from "./store"; import { editDeckStore } from "./store";
import { import {
...@@ -58,6 +42,11 @@ import { ...@@ -58,6 +42,11 @@ import {
editingDeckToIDeck, editingDeckToIDeck,
iDeckToEditingDeck, iDeckToEditingDeck,
} from "./utils"; } from "./utils";
import { FtsConditions, emptySearchConditions } from "@/middleware/sqlite/fts";
import { isEqual } from "lodash-es";
import Filter from "lodash-es/filter";
import { OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import { useDrop } from "react-dnd";
export const loader: LoaderFunction = async () => { export const loader: LoaderFunction = async () => {
// 必须先加载卡组,不然页面会崩溃 // 必须先加载卡组,不然页面会崩溃
...@@ -81,32 +70,46 @@ export const loader: LoaderFunction = async () => { ...@@ -81,32 +70,46 @@ export const loader: LoaderFunction = async () => {
}); });
} }
// 更新场景
changeScene(AudioActionType.BGM_DECK);
return null; return null;
}; };
export const selectedCard = proxy({
id: 23995346,
open: false,
});
const selectedDeck = proxy<{ deck: IDeck }>({
deck: deckStore.decks.at(0) ?? emptyDeck,
});
export const setSelectedDeck = (deck: IDeck) => {
selectedDeck.deck = deck;
};
export const Component: React.FC = () => { export const Component: React.FC = () => {
const snapDecks = useSnapshot(deckStore); const snapDecks = useSnapshot(deckStore);
const { progress } = useSnapshot(initStore.sqlite); const { progress } = useSnapshot(initStore.sqlite);
const [selectedDeck, setSelectedDeck] = useState<IDeck>( const { deck: snapSelectedDeck } = useSnapshot(selectedDeck);
deckStore.decks.at(0) ?? emptyDeck,
);
const { message } = App.useApp(); const { message } = App.useApp();
const { t: i18n } = useTranslation("BuildDeck"); const { t: i18n } = useTranslation("BuildDeck");
const handleDeckEditorReset = async () => { const handleDeckEditorReset = async () => {
editDeckStore.set(await iDeckToEditingDeck(selectedDeck)); editDeckStore.set(await iDeckToEditingDeck(selectedDeck.deck as IDeck));
message.info(`${i18n("ResetSuccessful")}`); message.info(`${i18n("ResetSuccessful")}`);
}; };
const handleDeckEditorSave = async () => { const handleDeckEditorSave = async () => {
const tmpIDeck = editingDeckToIDeck(editDeckStore); const tmpIDeck = editingDeckToIDeck(editDeckStore);
const result = await deckStore.update(selectedDeck.deckName, tmpIDeck); const result = await deckStore.update(selectedDeck.deck.deckName, tmpIDeck);
if (result) { if (result) {
setSelectedDeck(tmpIDeck); setSelectedDeck(tmpIDeck);
message.info(`${i18n("SaveSuccessful")}`); message.info(`${i18n("SaveSuccessful")}`);
editDeckStore.edited = false; editDeckStore.edited = false;
} else { } else {
editDeckStore.set(await iDeckToEditingDeck(selectedDeck)); editDeckStore.set(await iDeckToEditingDeck(selectedDeck.deck as IDeck));
message.error("保存失败"); message.error("保存失败");
editDeckStore.edited = false; editDeckStore.edited = false;
} }
...@@ -127,8 +130,8 @@ export const Component: React.FC = () => { ...@@ -127,8 +130,8 @@ export const Component: React.FC = () => {
<div className={styles.sider}> <div className={styles.sider}>
<ScrollableArea className={styles["deck-select-container"]}> <ScrollableArea className={styles["deck-select-container"]}>
<DeckSelect <DeckSelect
decks={snapDecks.decks} decks={snapDecks.decks as IDeck[]}
selected={selectedDeck.deckName} selected={snapSelectedDeck.deckName}
onSelect={(name) => onSelect={(name) =>
setSelectedDeck(deckStore.get(name) ?? emptyDeck) setSelectedDeck(deckStore.get(name) ?? emptyDeck)
} }
...@@ -151,7 +154,7 @@ export const Component: React.FC = () => { ...@@ -151,7 +154,7 @@ export const Component: React.FC = () => {
<> <>
<div className={styles.deck}> <div className={styles.deck}>
<DeckEditor <DeckEditor
deck={selectedDeck} deck={snapSelectedDeck as IDeck}
onClear={editDeckStore.clear} onClear={editDeckStore.clear}
onReset={handleDeckEditorReset} onReset={handleDeckEditorReset}
onSave={handleDeckEditorSave} onSave={handleDeckEditorSave}
...@@ -160,7 +163,7 @@ export const Component: React.FC = () => { ...@@ -160,7 +163,7 @@ export const Component: React.FC = () => {
/> />
</div> </div>
<div className={styles.select}> <div className={styles.select}>
<Search /> <DeckDatabase />
</div> </div>
</> </>
) : ( ) : (
...@@ -353,20 +356,18 @@ const Search: React.FC = () => { ...@@ -353,20 +356,18 @@ const Search: React.FC = () => {
); );
}; };
const { t } = useTranslation("BuildDeck");
const dropdownOptions: MenuProps["items"] = ( const dropdownOptions: MenuProps["items"] = (
[ [
[t("FromNewToOld"), () => setSortRef((a, b) => b.id - a.id)], ["从新到旧", () => setSortRef((a, b) => b.id - a.id)],
[t("FromOldToNew"), () => setSortRef((a, b) => a.id - b.id)], ["从旧到新", () => setSortRef((a, b) => a.id - b.id)],
[t("AttackPowerFromHighToLow"), genSort("atk", -1)], ["攻击力从高到低", genSort("atk", -1)],
[t("AttackPowerFromLowToHigh"), genSort("atk")], ["攻击力从低到高", genSort("atk")],
[t("DefensePowerFromHighToLow"), genSort("def", -1)], ["守备力从高到低", genSort("def", -1)],
[t("DefensePowerFromLowToHigh"), genSort("def")], ["守备力从低到高", genSort("def")],
[t("StarsRanksLevelsLinkFromHighToLow"), genSort("level", -1)], ["星/阶/刻/Link从高到低", genSort("level", -1)],
[t("StarsRanksLevelsLinkFromLowToHigh"), genSort("level")], ["星/阶/刻/Link从低到高", genSort("level")],
[t("PendulumScaleFromHighToLow"), genSort("lscale", -1)], ["灵摆刻度从高到低", genSort("lscale", -1)],
[t("PendulumScaleFromLowToHigh"), genSort("lscale")], ["灵摆刻度从低到高", genSort("lscale")],
] as const ] as const
).map(([label, onClick], key) => ({ key, label, onClick })); ).map(([label, onClick], key) => ({ key, label, onClick }));
...@@ -420,13 +421,11 @@ const Search: React.FC = () => { ...@@ -420,13 +421,11 @@ const Search: React.FC = () => {
if (viewport) viewport.scrollTop = 0; if (viewport) viewport.scrollTop = 0;
}, []); }, []);
const { t: i18n } = useTranslation("BuildDeck");
return ( return (
<div className={styles.container} ref={dropRef}> <div className={styles.container} ref={dropRef}>
<div className={styles.title}> <div className={styles.title}>
<Input <Input
placeholder={i18n("KeywordsPlaceholder")} placeholder="关键词(空格分隔)"
bordered={false} bordered={false}
suffix={ suffix={
<Button <Button
...@@ -452,7 +451,7 @@ const Search: React.FC = () => { ...@@ -452,7 +451,7 @@ const Search: React.FC = () => {
icon={<FilterOutlined />} icon={<FilterOutlined />}
onClick={showFilterModal} onClick={showFilterModal}
> >
{i18n("Filter")} 筛选
</Button> </Button>
<Dropdown <Dropdown
menu={{ items: dropdownOptions }} menu={{ items: dropdownOptions }}
...@@ -466,7 +465,7 @@ const Search: React.FC = () => { ...@@ -466,7 +465,7 @@ const Search: React.FC = () => {
icon={<SortAscendingOutlined />} icon={<SortAscendingOutlined />}
> >
<span> <span>
{i18n("SortBy")} 排列
<span className={styles["search-count"]}> <span className={styles["search-count"]}>
({searchResult.length}) ({searchResult.length})
</span> </span>
...@@ -484,7 +483,7 @@ const Search: React.FC = () => { ...@@ -484,7 +483,7 @@ const Search: React.FC = () => {
handleSearch(emptySearchConditions); handleSearch(emptySearchConditions);
}} }}
> >
{i18n("Reset")} 重置
</Button> </Button>
</div> </div>
<ScrollableArea className={styles["search-cards-container"]} ref={ref}> <ScrollableArea className={styles["search-cards-container"]} ref={ref}>
...@@ -607,8 +606,3 @@ const HigherCardDetail: React.FC = () => { ...@@ -607,8 +606,3 @@ const HigherCardDetail: React.FC = () => {
/> />
); );
}; };
const selectedCard = proxy({
id: 23995346,
open: false,
});
...@@ -38,7 +38,7 @@ export const compareCards = (a: CardMeta, b: CardMeta): number => { ...@@ -38,7 +38,7 @@ export const compareCards = (a: CardMeta, b: CardMeta): number => {
}; };
/** 生成ydk格式的卡组文本 */ /** 生成ydk格式的卡组文本 */
function genYdkText(deck: IDeck): string { export function genYdkText(deck: IDeck): string {
const { main, extra, side } = deck; const { main, extra, side } = deck;
const lines = [ const lines = [
......
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { LoaderFunction, useNavigate, useSearchParams } from "react-router-dom";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { ygopro } from "@/api";
import { useEnv } from "@/hook";
import { AudioActionType, changeScene } from "@/infra/audio";
import { matStore, SideStage, sideStore } from "@/stores"; import { matStore, SideStage, sideStore } from "@/stores";
import { import {
...@@ -23,11 +26,32 @@ import { LifeBar, Mat, Menu, Underlying } from "./PlayMat"; ...@@ -23,11 +26,32 @@ import { LifeBar, Mat, Menu, Underlying } from "./PlayMat";
import { ChatBox } from "./PlayMat/ChatBox"; import { ChatBox } from "./PlayMat/ChatBox";
import { HandChain } from "./PlayMat/HandChain"; import { HandChain } from "./PlayMat/HandChain";
export const loader: LoaderFunction = async () => {
// 更新场景
changeScene(AudioActionType.BGM_DUEL);
return null;
};
export const Component: React.FC = () => { export const Component: React.FC = () => {
const { stage } = useSnapshot(sideStore); const { stage } = useSnapshot(sideStore);
const { duelEnd } = useSnapshot(matStore); const { duelEnd } = useSnapshot(matStore);
const navigate = useNavigate(); const navigate = useNavigate();
// 如果处于开发时的本地文件回放模式,则重新跳回Match且保持record参数,从而开始下一轮播放
const [searchParams] = useSearchParams();
const { DEV } = useEnv();
useEffect(() => {
if (!DEV) return;
const recordName = searchParams.get("record");
if (
searchParams &&
matStore.selfType === ygopro.StocTypeChange.SelfType.UNKNOWN
) {
navigate(`/match?record=${recordName}`);
}
}, []);
useEffect(() => { useEffect(() => {
if (stage === SideStage.SIDE_CHANGING) { if (stage === SideStage.SIDE_CHANGING) {
// 跳转更换Side // 跳转更换Side
......
...@@ -3,10 +3,14 @@ import { Avatar, Button, Checkbox, Input, List } from "antd"; ...@@ -3,10 +3,14 @@ import { Avatar, Button, Checkbox, Input, List } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { CardMeta, searchCards, sendSelectOptionResponse } from "@/api"; import {
CardMeta,
getCardImgUrl,
searchCards,
sendSelectOptionResponse,
} from "@/api";
import { isDeclarable, isToken } from "@/common"; import { isDeclarable, isToken } from "@/common";
import { emptySearchConditions } from "@/middleware/sqlite/fts"; import { emptySearchConditions } from "@/middleware/sqlite/fts";
import { getCardImgUrl } from "@/ui/Shared";
import { NeosModal } from "../NeosModal"; import { NeosModal } from "../NeosModal";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
......
...@@ -56,6 +56,7 @@ export const CardListModal = () => { ...@@ -56,6 +56,7 @@ export const CardListModal = () => {
<YgoCard <YgoCard
code={card.code} code={card.code}
key={card.uuid} key={card.uuid}
targeted={card.targeted}
width={CARD_WIDTH} width={CARD_WIDTH}
onClick={() => showCardModal(card)} onClick={() => showCardModal(card)}
/> />
......
...@@ -185,6 +185,7 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({ ...@@ -185,6 +185,7 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({
cover={ cover={
<YgoCard <YgoCard
code={card.meta.id} code={card.meta.id}
targeted={card.targeted}
className={styles.card} className={styles.card}
/> />
} }
...@@ -222,7 +223,11 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({ ...@@ -222,7 +223,11 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({
<div> <div>
<Card <Card
cover={ cover={
<YgoCard code={card.meta.id} className={styles.card} /> <YgoCard
code={card.meta.id}
targeted={card.targeted}
className={styles.card}
/>
} }
className={styles["check-card"]} className={styles["check-card"]}
onClick={() => { onClick={() => {
...@@ -269,6 +274,9 @@ export interface Option { ...@@ -269,6 +274,9 @@ export interface Option {
level1?: number; level1?: number;
level2?: number; level2?: number;
response?: number; response?: number;
targeted?: boolean;
// 便于直接返回这个信息 // 便于直接返回这个信息
//
// 尽量不要用这个字段
card?: CardType; card?: CardType;
} }
...@@ -21,8 +21,7 @@ import React, { useEffect, useState } from "react"; ...@@ -21,8 +21,7 @@ import React, { useEffect, useState } from "react";
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { sendSortCardResponse } from "@/api"; import { sendSortCardResponse } from "@/api";
import { CardMeta } from "@/api/cards"; import { CardMeta, getCardImgUrl } from "@/api/cards";
import { getCardImgUrl } from "@/ui/Shared";
import { NeosModal } from "../NeosModal"; import { NeosModal } from "../NeosModal";
......
import { Avatar, Dropdown } from "antd"; import { App, Avatar, Dropdown } from "antd";
import classNames from "classnames"; import classNames from "classnames";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
...@@ -20,6 +20,7 @@ import { useConfig } from "@/config"; ...@@ -20,6 +20,7 @@ import { useConfig } from "@/config";
import { accountStore } from "@/stores"; import { accountStore } from "@/stores";
import { I18NSelector } from "../I18N"; import { I18NSelector } from "../I18N";
import { Setting } from "../Setting";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
import { import {
getLoginStatus, getLoginStatus,
...@@ -73,6 +74,7 @@ export const Component = () => { ...@@ -73,6 +74,7 @@ export const Component = () => {
const { pathname } = routerLocation; const { pathname } = routerLocation;
const pathnamesHideHeader = ["/waitroom", "/duel", "/side"]; const pathnamesHideHeader = ["/waitroom", "/duel", "/side"];
const { modal } = App.useApp();
const callbackUrl = `${location.origin}/match/`; const callbackUrl = `${location.origin}/match/`;
const onLogin = () => location.replace(getSSOSignInUrl(callbackUrl)); const onLogin = () => location.replace(getSSOSignInUrl(callbackUrl));
...@@ -137,6 +139,18 @@ export const Component = () => { ...@@ -137,6 +139,18 @@ export const Component = () => {
</a> </a>
), ),
}, },
{
label: "系统设置",
onClick: () => {
modal.info({
content: <Setting />,
centered: true,
maskClosable: true,
icon: null,
footer: null,
});
},
},
{ {
label: logined ? i18n("LogOut") : i18n("Login"), label: logined ? i18n("LogOut") : i18n("Login"),
onClick: logined ? onLogout : onLogin, onClick: logined ? onLogout : onLogin,
......
...@@ -82,6 +82,8 @@ export const handleSSOLogin = async (search: string) => { ...@@ -82,6 +82,8 @@ export const handleSSOLogin = async (search: string) => {
const sso = new URLSearchParams(search).get("sso"); const sso = new URLSearchParams(search).get("sso");
const user = sso ? getSSOUser(new URLSearchParams(atob(sso))) : undefined; const user = sso ? getSSOUser(new URLSearchParams(atob(sso))) : undefined;
if (user) { if (user) {
// Convert userID to [`Number`] here
user.id = Number(user.id);
accountStore.login(user); accountStore.login(user);
setCookie(CookieKeys.USER, JSON.stringify(user)); setCookie(CookieKeys.USER, JSON.stringify(user));
// TODO: toast显示登录成功 // TODO: toast显示登录成功
......
import { Button, message, Modal, UploadProps } from "antd"; import { Button, message, Modal, type UploadProps } from "antd";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { useEnv } from "@/hook";
import { replayStore } from "@/stores"; import { replayStore } from "@/stores";
import { Uploader } from "../../Shared"; import { Uploader } from "../../Shared";
...@@ -40,26 +41,34 @@ export const ReplayModal: React.FC = () => { ...@@ -40,26 +41,34 @@ export const ReplayModal: React.FC = () => {
} else { } else {
setLoading(true); setLoading(true);
// 标记为回放模式 await launchReplay(replay);
replayStore.isReplay = true;
await connectSrvpro({
ip: "",
player: "",
passWd: "",
replay: true,
replayData: replay,
});
} }
}; };
// 开发时的回放模式:路径跳转到duel
const [searchParams] = useSearchParams();
const { DEV } = useEnv();
const recordName = searchParams.get("record");
// 如处于回放模式且有回放文件,则导入播放
useEffect(() => {
if (!DEV) return;
if (recordName) {
import(
/* @vite-ignore */ `../../../../neos-assets/records/${recordName}.yrp3d?arraybuffer`
)
.then((res) => launchReplay(res.default))
.catch(() => console.error(`Local record '${recordName}' not found`));
}
}, []);
useEffect(() => { useEffect(() => {
if (hasStart) { if (hasStart) {
setLoading(false); setLoading(false);
localStore.open = false; localStore.open = false;
localStore.hasStart = false; localStore.hasStart = false;
// 跳转 // 跳转
navigate(`/duel`); navigate(recordName ? `/duel?record=${recordName}` : "/duel");
} }
}, [hasStart]); }, [hasStart]);
...@@ -93,3 +102,17 @@ export const replayOpen = () => { ...@@ -93,3 +102,17 @@ export const replayOpen = () => {
export const replayStart = () => { export const replayStart = () => {
localStore.hasStart = true; localStore.hasStart = true;
}; };
/** 单独抽离出来,以便可以在 Match.tsx 中调用,跳过Modal直接加载回放,便于开发 */
export const launchReplay = async (replayData: ArrayBuffer) => {
// 标记为回放模式
replayStore.isReplay = true;
await connectSrvpro({
ip: "",
player: "",
passWd: "",
replay: true,
replayData,
});
};
...@@ -18,6 +18,7 @@ import { ...@@ -18,6 +18,7 @@ import {
match, match,
} from "@/api"; } from "@/api";
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { AudioActionType, changeScene } from "@/infra/audio";
import { accountStore, deckStore, resetUniverse, roomStore } from "@/stores"; import { accountStore, deckStore, resetUniverse, roomStore } from "@/stores";
import { Background, IconFont, ScrollableArea, Select } from "@/ui/Shared"; import { Background, IconFont, ScrollableArea, Select } from "@/ui/Shared";
...@@ -37,6 +38,8 @@ const { servers: serverList } = useConfig(); ...@@ -37,6 +38,8 @@ const { servers: serverList } = useConfig();
export const loader: LoaderFunction = () => { export const loader: LoaderFunction = () => {
// 在加载这个页面之前先重置一些store,清掉上局游戏遗留的数据 // 在加载这个页面之前先重置一些store,清掉上局游戏遗留的数据
resetUniverse(); resetUniverse();
// 更新当前场景
changeScene(AudioActionType.BGM_MENU);
return null; return null;
}; };
......
import { Checkbox, Form, Slider, Space, Switch } from "antd";
import React from "react";
import { useSnapshot } from "valtio";
import { settingStore } from "@/stores/settingStore";
export const AudioSetting: React.FC = () => {
const { audio } = useSnapshot(settingStore);
return (
<Form
initialValues={audio}
onValuesChange={(config) => {
settingStore.saveAudioConfig(config);
}}
labelAlign="left"
>
<Form.Item label="开启音乐">
<Space size={16}>
<Form.Item name="enableMusic" noStyle valuePropName="checked">
<Checkbox />
</Form.Item>
<Form.Item name="musicVolume" noStyle>
<Slider
style={{ width: 200 }}
min={0}
max={1}
step={0.01}
tooltip={{
formatter: (value) => ((value || 0) * 100).toFixed(0),
}}
/>
</Form.Item>
</Space>
</Form.Item>
<Form.Item label="开启音效">
<Space size={16}>
<Form.Item name="enableSoundEffects" noStyle valuePropName="checked">
<Checkbox />
</Form.Item>
<Form.Item name="soundEffectsVolume" noStyle>
<Slider
style={{ width: 200 }}
min={0}
max={1}
step={0.01}
tooltip={{
formatter: (value) => ((value || 0) * 100).toFixed(0),
}}
/>
</Form.Item>
</Space>
</Form.Item>
<Form.Item
name="enableMusicSwitchByEnv"
label="根据环境切换音乐"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Form>
);
};
import { ConfigProvider, Modal, Tabs, TabsProps } from "antd";
import zhCN from "antd/locale/zh_CN";
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { theme } from "../theme";
import { AudioSetting } from "./Audio";
/** 设置面板属性 */
export interface SettingProps {
/** 默认设置页 */
defaultKey?: "audio" | "other";
}
export const Setting = (props: SettingProps) => {
const { defaultKey = "audio" } = props;
const items: TabsProps["items"] = [
{
key: "audio",
label: "音频设置",
children: <AudioSetting />,
},
];
return <Tabs defaultActiveKey={defaultKey} items={items} />;
};
/**
* 打开设置面板,允许在非组件内通过此 API 打开设置面板
*/
export function openSettingPanel(props: SettingProps) {
const div = document.createElement("div");
document.body.appendChild(div);
const destroy = () => {
const result = unmountComponentAtNode(div);
if (result && div.parentNode) {
div.parentNode.removeChild(div);
}
};
render(
<ConfigProvider theme={theme} locale={zhCN}>
<Modal open centered footer={null} onCancel={destroy} closeIcon={null}>
<Setting {...props} />
</Modal>
</ConfigProvider>,
div,
);
}
...@@ -7,13 +7,22 @@ export const SpecialButton: React.FC< ...@@ -7,13 +7,22 @@ export const SpecialButton: React.FC<
React.PropsWithChildren<React.ComponentProps<"span">> & { React.PropsWithChildren<React.ComponentProps<"span">> & {
disabled?: boolean; disabled?: boolean;
} }
> = ({ children, className, disabled, ...rest }) => ( > = ({ children, className, disabled, ...rest }) => {
<span // 这里的音效有滞后,暂时先注释掉,后面再来修复这个问题
className={classNames(styles["special-btn"], className, { /*
[styles.disabled]: disabled, const [effectRef] = usePlayEffect<HTMLSpanElement>(
})} AudioActionType.SOUND_BUTTON,
{...rest} );
> */
{children} return (
</span> <span
); // ref={effectRef}
className={classNames(styles["special-btn"], className, {
[styles.disabled]: disabled,
})}
{...rest}
>
{children}
</span>
);
};
...@@ -13,3 +13,12 @@ ...@@ -13,3 +13,12 @@
background-size: contain; background-size: contain;
text-align: center; text-align: center;
} }
.targeted {
position: relative;
width: 80%;
height: 80%;
justify-content: center;
z-index: 2;
pointer-events: none;
}
import classNames from "classnames"; import classNames from "classnames";
import { CSSProperties, useMemo } from "react"; import { CSSProperties, useMemo } from "react";
import { isSuperReleaseCard } from "@/api"; import { getCardImgUrl } from "@/api";
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
const { assetsPath } = useConfig();
interface Props { interface Props {
className?: string; className?: string;
isBack?: boolean; isBack?: boolean;
code?: number; code?: number;
targeted?: boolean;
// cardName?: string; // cardName?: string;
style?: CSSProperties; style?: CSSProperties;
width?: number | string; width?: number | string;
...@@ -23,6 +26,7 @@ export const YgoCard: React.FC<Props> = (props) => { ...@@ -23,6 +26,7 @@ export const YgoCard: React.FC<Props> = (props) => {
code = 0, code = 0,
// cardName, // cardName,
isBack = false, isBack = false,
targeted = false,
width, width,
style, style,
onClick, onClick,
...@@ -45,40 +49,15 @@ export const YgoCard: React.FC<Props> = (props) => { ...@@ -45,40 +49,15 @@ export const YgoCard: React.FC<Props> = (props) => {
> >
{/* 暂时不能这么写...但如果用onload的话来判断可能又很消耗性能,再看看吧 */} {/* 暂时不能这么写...但如果用onload的话来判断可能又很消耗性能,再看看吧 */}
{/* {cardName} */} {/* {cardName} */}
{targeted ? (
<div className={styles.targeted}>
<img src={`${assetsPath}/targeted.svg`} />
</div>
) : (
<></>
)}
</div> </div>
), ),
[code], [code],
); );
}; };
const NeosConfig = useConfig();
// TODO: 这个函数应该从这个文件抽离出来作为公共的函数使用
export function getCardImgUrl(code: number, back = false) {
const ASSETS_BASE =
import.meta.env.BASE_URL === "/"
? NeosConfig.assetsPath
: `${import.meta.env.BASE_URL}${NeosConfig.assetsPath}`;
if (back || code === 0) {
return `${ASSETS_BASE}/card_back.jpg`;
}
if (isSuperReleaseCard(code)) {
return `${NeosConfig.preReleaseImgUrl}/${code}.jpg`;
} else {
const language = localStorage.getItem("language");
if (
language === "en" ||
language === "br" ||
language === "pt" ||
language === "fr" ||
language === "es"
) {
NeosConfig.releaseImgUrl = NeosConfig.releaseImgUrl.replace(
"zh-CN",
"en-US",
);
}
return `${NeosConfig.releaseImgUrl}/${code}.jpg`;
}
}
...@@ -3,11 +3,12 @@ import { App, Button, Space } from "antd"; ...@@ -3,11 +3,12 @@ import { App, Button, Space } from "antd";
import { HTML5toTouch } from "rdndmb-html5-to-touch"; import { HTML5toTouch } from "rdndmb-html5-to-touch";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { DndProvider } from "react-dnd-multi-backend"; import { DndProvider } from "react-dnd-multi-backend";
import { useNavigate } from "react-router-dom"; import { LoaderFunction, useNavigate } from "react-router-dom";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { CardMeta, fetchCard, sendUpdateDeck } from "@/api"; import { CardMeta, fetchCard, sendUpdateDeck } from "@/api";
import { isExtraDeckCard } from "@/common"; import { isExtraDeckCard } from "@/common";
import { AudioActionType, changeScene } from "@/infra/audio";
import { IDeck, roomStore, SideStage, sideStore } from "@/stores"; import { IDeck, roomStore, SideStage, sideStore } from "@/stores";
import { CardDetail } from "../BuildDeck/CardDetail"; import { CardDetail } from "../BuildDeck/CardDetail";
...@@ -16,6 +17,12 @@ import { Chat } from "../WaitRoom/Chat"; ...@@ -16,6 +17,12 @@ import { Chat } from "../WaitRoom/Chat";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
import { TpModal } from "./TpModal"; import { TpModal } from "./TpModal";
export const loader: LoaderFunction = async () => {
// 更新场景
changeScene(AudioActionType.BGM_DECK);
return null;
};
export const Component: React.FC = () => { export const Component: React.FC = () => {
const { message } = App.useApp(); const { message } = App.useApp();
const initialDeck = sideStore.getSideDeck(); const initialDeck = sideStore.getSideDeck();
......
import { RightOutlined } from "@ant-design/icons"; import { RightOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { LoaderFunction, useNavigate } from "react-router-dom";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { getSSOSignInUrl } from "@/api"; import { getSSOSignInUrl } from "@/api";
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { AudioActionType, changeScene } from "@/infra/audio";
import { accountStore } from "@/stores"; import { accountStore } from "@/stores";
import { Background, SpecialButton } from "@/ui/Shared"; import { Background, SpecialButton } from "@/ui/Shared";
...@@ -12,6 +13,12 @@ import styles from "./index.module.scss"; ...@@ -12,6 +13,12 @@ import styles from "./index.module.scss";
const NeosConfig = useConfig(); const NeosConfig = useConfig();
export const loader: LoaderFunction = async () => {
// 更新场景
changeScene(AudioActionType.BGM_MENU);
return null;
};
export const Component: React.FC = () => { export const Component: React.FC = () => {
const { t } = useTranslation("Start"); const { t } = useTranslation("Start");
const { user } = useSnapshot(accountStore); const { user } = useSnapshot(accountStore);
......
...@@ -18,10 +18,11 @@ import { App, Avatar, Button, Skeleton, Space } from "antd"; ...@@ -18,10 +18,11 @@ import { App, Avatar, Button, Skeleton, Space } from "antd";
import classNames from "classnames"; import classNames from "classnames";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { LoaderFunction, useNavigate } from "react-router-dom";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { AudioActionType, changeScene } from "@/infra/audio";
import { import {
accountStore, accountStore,
deckStore, deckStore,
...@@ -40,6 +41,12 @@ import { Mora, MoraPopover, Tp, TpPopover } from "./Popover"; ...@@ -40,6 +41,12 @@ import { Mora, MoraPopover, Tp, TpPopover } from "./Popover";
const NeosConfig = useConfig(); const NeosConfig = useConfig();
export const loader: LoaderFunction = async () => {
// 更新场景
changeScene(AudioActionType.BGM_MENU);
return null;
};
export const Component: React.FC = () => { export const Component: React.FC = () => {
const { t: i18n } = useTranslation("WaitRoom"); const { t: i18n } = useTranslation("WaitRoom");
const { message } = App.useApp(); const { message } = App.useApp();
......
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
}, },
"include": [ "include": [
"src", "src",
"rust-src/pkg" "rust-src/pkg",
"neos-assets"
] ]
} }
...@@ -6,6 +6,7 @@ import tsconfigPaths from "vite-tsconfig-paths"; ...@@ -6,6 +6,7 @@ import tsconfigPaths from "vite-tsconfig-paths";
import wasmPack from "vite-plugin-wasm-pack"; import wasmPack from "vite-plugin-wasm-pack";
import sassDts from "vite-plugin-sass-dts"; import sassDts from "vite-plugin-sass-dts";
import path from "path"; import path from "path";
import arraybuffer from "vite-plugin-arraybuffer";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
...@@ -13,6 +14,7 @@ export default defineConfig({ ...@@ -13,6 +14,7 @@ export default defineConfig({
react(), react(),
svgr(), svgr(),
ydkLoader(), ydkLoader(),
arraybuffer(),
tsconfigPaths(), tsconfigPaths(),
wasmPack("./rust-src"), wasmPack("./rust-src"),
sassDts({ sassDts({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment