add: skin viewer, refatoring to api
This commit is contained in:
195
package-lock.json
generated
195
package-lock.json
generated
@ -15,6 +15,8 @@
|
|||||||
"@mui/material": "^7.2.0",
|
"@mui/material": "^7.2.0",
|
||||||
"@xmcl/core": "^2.14.1",
|
"@xmcl/core": "^2.14.1",
|
||||||
"@xmcl/installer": "^6.1.0",
|
"@xmcl/installer": "^6.1.0",
|
||||||
|
"@xmcl/model": "^2.0.4",
|
||||||
|
"@xmcl/resourcepack": "^1.2.4",
|
||||||
"@xmcl/user": "^4.2.0",
|
"@xmcl/user": "^4.2.0",
|
||||||
"electron-debug": "^4.1.0",
|
"electron-debug": "^4.1.0",
|
||||||
"electron-log": "^5.3.2",
|
"electron-log": "^5.3.2",
|
||||||
@ -25,7 +27,9 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.3.0",
|
"react-router-dom": "^7.3.0",
|
||||||
|
"skinview3d": "^3.4.1",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
|
"three": "^0.178.0",
|
||||||
"undici": "^7.11.0",
|
"undici": "^7.11.0",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
@ -43,6 +47,7 @@
|
|||||||
"@types/react": "^19.0.11",
|
"@types/react": "^19.0.11",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@types/react-test-renderer": "^19.0.0",
|
"@types/react-test-renderer": "^19.0.0",
|
||||||
|
"@types/three": "^0.178.1",
|
||||||
"@types/webpack-bundle-analyzer": "^4.7.0",
|
"@types/webpack-bundle-analyzer": "^4.7.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||||
"@typescript-eslint/parser": "^8.26.1",
|
"@typescript-eslint/parser": "^8.26.1",
|
||||||
@ -2228,6 +2233,13 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dimforge/rapier3d-compat": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@discoveryjs/json-ext": {
|
"node_modules/@discoveryjs/json-ext": {
|
||||||
"version": "0.5.7",
|
"version": "0.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
|
||||||
@ -4897,6 +4909,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tweenjs/tween.js": {
|
||||||
|
"version": "23.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
|
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/aria-query": {
|
"node_modules/@types/aria-query": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
@ -5418,6 +5437,28 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/stats.js": {
|
||||||
|
"version": "0.17.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||||
|
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/three": {
|
||||||
|
"version": "0.178.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.178.1.tgz",
|
||||||
|
"integrity": "sha512-WSabew1mgWgRx2RfLfKY+9h4wyg6U94JfLbZEGU245j/WY2kXqU0MUfghS+3AYMV5ET1VlILAgpy77cB6a3Itw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||||
|
"@tweenjs/tween.js": "~23.1.3",
|
||||||
|
"@types/stats.js": "*",
|
||||||
|
"@types/webxr": "*",
|
||||||
|
"@webgpu/types": "*",
|
||||||
|
"fflate": "~0.8.2",
|
||||||
|
"meshoptimizer": "~0.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/tough-cookie": {
|
"node_modules/@types/tough-cookie": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
|
||||||
@ -5445,6 +5486,12 @@
|
|||||||
"webpack": "^5"
|
"webpack": "^5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/webxr": {
|
||||||
|
"version": "0.5.22",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz",
|
||||||
|
"integrity": "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.5.12",
|
"version": "8.5.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
|
||||||
@ -5856,6 +5903,13 @@
|
|||||||
"@xtuc/long": "4.2.2"
|
"@xtuc/long": "4.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@webgpu/types": {
|
||||||
|
"version": "0.1.64",
|
||||||
|
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.64.tgz",
|
||||||
|
"integrity": "sha512-84kRIAGV46LJTlJZWxShiOrNL30A+9KokD7RB3dRCIqODFjodS5tCD5yyiZ8kIReGVZSDfA3XkkwyyOIF6K62A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/@webpack-cli/configtest": {
|
"node_modules/@webpack-cli/configtest": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz",
|
||||||
@ -5988,6 +6042,70 @@
|
|||||||
"node": ">=20.18.1"
|
"node": ">=20.18.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xmcl/model": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmcl/model/-/model-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-m671ny9FAaM0nF731Coq+F7N7ikQFCeJA+NhrXNcYkyclMXaqL6Q/kCrYpjhnbozMYVOblqu44QPSuEFqR/JHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/three": "^0.150.0",
|
||||||
|
"@xmcl/resourcepack": "1.2.4",
|
||||||
|
"three": "0.156.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xmcl/model/node_modules/@types/three": {
|
||||||
|
"version": "0.150.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.150.2.tgz",
|
||||||
|
"integrity": "sha512-cvcz/81Mmj4oiAA+uxzwaRK3t8lYw8WxejXKqIBfu6PqvwSAEEiCi3VfCiVY18UflBqL0LDX/za85+sfqjMoIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/stats.js": "*",
|
||||||
|
"@types/webxr": "*",
|
||||||
|
"fflate": "~0.6.9",
|
||||||
|
"lil-gui": "~0.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xmcl/model/node_modules/fflate": {
|
||||||
|
"version": "0.6.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
|
||||||
|
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@xmcl/model/node_modules/three": {
|
||||||
|
"version": "0.156.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.156.1.tgz",
|
||||||
|
"integrity": "sha512-kP7H0FK9d/k6t/XvQ9FO6i+QrePoDcNhwl0I02+wmUJRNSLCUIDMcfObnzQvxb37/0Uc9TDT0T1HgsRRrO6SYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@xmcl/resourcepack": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmcl/resourcepack/-/resourcepack-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-OlDOBAX33EKHC0PYC68a6RW/mBtfmskl0OKbsP8gSPM7RH7zqR2SNR+u5AAavEhpmCvjThztv6KwHifGk6/t/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@xmcl/system": "2.2.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xmcl/system": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmcl/system/-/system-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-G5argPsvKqvYDfUE1z+pVCIuNbhuaC+YXWlQHHXgMSpKSDnJRQoYvlQAcTorlFCqFqtL5a51hV+Rmsug0geJuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@xmcl/unzip": "2.1.2",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"yauzl": "^2.10.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@xmcl/task": {
|
"node_modules/@xmcl/task": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@xmcl/task/-/task-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@xmcl/task/-/task-4.1.1.tgz",
|
||||||
@ -8435,7 +8553,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cosmiconfig": {
|
"node_modules/cosmiconfig": {
|
||||||
@ -11943,6 +12060,13 @@
|
|||||||
"pend": "~1.2.0"
|
"pend": "~1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||||
@ -13302,7 +13426,6 @@
|
|||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/immutable": {
|
"node_modules/immutable": {
|
||||||
@ -15358,7 +15481,6 @@
|
|||||||
"version": "3.10.1",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||||
"dev": true,
|
|
||||||
"license": "(MIT OR GPL-3.0-or-later)",
|
"license": "(MIT OR GPL-3.0-or-later)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lie": "~3.3.0",
|
"lie": "~3.3.0",
|
||||||
@ -15371,14 +15493,12 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/jszip/node_modules/readable-stream": {
|
"node_modules/jszip/node_modules/readable-stream": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
@ -15394,14 +15514,12 @@
|
|||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/jszip/node_modules/string_decoder": {
|
"node_modules/jszip/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
@ -15572,12 +15690,17 @@
|
|||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"immediate": "~3.0.5"
|
"immediate": "~3.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lil-gui": {
|
||||||
|
"version": "0.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.17.0.tgz",
|
||||||
|
"integrity": "sha512-MVBHmgY+uEbmJNApAaPbtvNh1RCAeMnKym82SBjtp5rODTYKWtM+MXHCifLe2H2Ti1HuBGBtK/5SyG4ShQ3pUQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lilconfig": {
|
"node_modules/lilconfig": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
@ -15959,6 +16082,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/meshoptimizer": {
|
||||||
|
"version": "0.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
|
||||||
|
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/methods": {
|
"node_modules/methods": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
@ -16939,7 +17068,6 @@
|
|||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
"dev": true,
|
|
||||||
"license": "(MIT AND Zlib)"
|
"license": "(MIT AND Zlib)"
|
||||||
},
|
},
|
||||||
"node_modules/param-case": {
|
"node_modules/param-case": {
|
||||||
@ -17921,7 +18049,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/progress": {
|
"node_modules/progress": {
|
||||||
@ -19482,7 +19609,6 @@
|
|||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/setprototypeof": {
|
"node_modules/setprototypeof": {
|
||||||
@ -19656,6 +19782,47 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/skinview-utils": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/skinview-utils/-/skinview-utils-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-4eLrMqR526ehlZbsd8SuZ/CHpS9GiH0xUMoV+PYlJVi95ZFz5HJu7Spt5XYa72DRS7wgt5qquvHZf0XZJgmu9Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/skinview3d": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/skinview3d/-/skinview3d-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-WVN1selfDSAoQB7msLs3ueJjW/pge3nsmbqxJeXPnN/qIJ1GJKpMZO8mavSvMojaMrmpSgOJWfYUkK9B34ts2g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/three": "^0.156.0",
|
||||||
|
"skinview-utils": "^0.7.1",
|
||||||
|
"three": "^0.156.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/skinview3d/node_modules/@types/three": {
|
||||||
|
"version": "0.156.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.156.0.tgz",
|
||||||
|
"integrity": "sha512-733bXDSRdlrxqOmQuOmfC1UBRuJ2pREPk8sWnx9MtIJEVDQMx8U0NQO5MVVaOrjzDPyLI+cFPim2X/ss9v0+LQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/stats.js": "*",
|
||||||
|
"@types/webxr": "*",
|
||||||
|
"fflate": "~0.6.10",
|
||||||
|
"meshoptimizer": "~0.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/skinview3d/node_modules/fflate": {
|
||||||
|
"version": "0.6.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
|
||||||
|
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/skinview3d/node_modules/three": {
|
||||||
|
"version": "0.156.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.156.1.tgz",
|
||||||
|
"integrity": "sha512-kP7H0FK9d/k6t/XvQ9FO6i+QrePoDcNhwl0I02+wmUJRNSLCUIDMcfObnzQvxb37/0Uc9TDT0T1HgsRRrO6SYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/slash": {
|
"node_modules/slash": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||||
@ -20603,6 +20770,12 @@
|
|||||||
"tslib": "^2"
|
"tslib": "^2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/three": {
|
||||||
|
"version": "0.178.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.178.0.tgz",
|
||||||
|
"integrity": "sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/thunky": {
|
"node_modules/thunky": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
||||||
|
@ -109,6 +109,8 @@
|
|||||||
"@mui/material": "^7.2.0",
|
"@mui/material": "^7.2.0",
|
||||||
"@xmcl/core": "^2.14.1",
|
"@xmcl/core": "^2.14.1",
|
||||||
"@xmcl/installer": "^6.1.0",
|
"@xmcl/installer": "^6.1.0",
|
||||||
|
"@xmcl/model": "^2.0.4",
|
||||||
|
"@xmcl/resourcepack": "^1.2.4",
|
||||||
"@xmcl/user": "^4.2.0",
|
"@xmcl/user": "^4.2.0",
|
||||||
"electron-debug": "^4.1.0",
|
"electron-debug": "^4.1.0",
|
||||||
"electron-log": "^5.3.2",
|
"electron-log": "^5.3.2",
|
||||||
@ -119,7 +121,9 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.3.0",
|
"react-router-dom": "^7.3.0",
|
||||||
|
"skinview3d": "^3.4.1",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
|
"three": "^0.178.0",
|
||||||
"undici": "^7.11.0",
|
"undici": "^7.11.0",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
@ -137,6 +141,7 @@
|
|||||||
"@types/react": "^19.0.11",
|
"@types/react": "^19.0.11",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@types/react-test-renderer": "^19.0.0",
|
"@types/react-test-renderer": "^19.0.0",
|
||||||
|
"@types/three": "^0.178.1",
|
||||||
"@types/webpack-bundle-analyzer": "^4.7.0",
|
"@types/webpack-bundle-analyzer": "^4.7.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||||
"@typescript-eslint/parser": "^8.26.1",
|
"@typescript-eslint/parser": "^8.26.1",
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
} from '@xmcl/installer';
|
} from '@xmcl/installer';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { AuthService } from './auth-service';
|
import { AuthService } from './auth-service';
|
||||||
|
import { API_BASE_URL } from '../renderer/api';
|
||||||
|
|
||||||
// Константы
|
// Константы
|
||||||
const AUTHLIB_INJECTOR_FILENAME = 'authlib-injector-1.2.5.jar';
|
const AUTHLIB_INJECTOR_FILENAME = 'authlib-injector-1.2.5.jar';
|
||||||
@ -740,7 +741,7 @@ export function initMinecraftHandlers() {
|
|||||||
server: serverConfig, // Используем созданный объект конфигурации
|
server: serverConfig, // Используем созданный объект конфигурации
|
||||||
extraJVMArgs: [
|
extraJVMArgs: [
|
||||||
'-Dlog4j2.formatMsgNoLookups=true',
|
'-Dlog4j2.formatMsgNoLookups=true',
|
||||||
`-javaagent:${authlibPath}=http://147.78.65.214:8000`,
|
`-javaagent:${authlibPath}=${API_BASE_URL}`,
|
||||||
`-Xmx${memory}M`,
|
`-Xmx${memory}M`,
|
||||||
'-Dauthlibinjector.skinWhitelist=127.0.0.1,falrfg-213-87-196-173.ru.tuna.am',
|
'-Dauthlibinjector.skinWhitelist=127.0.0.1,falrfg-213-87-196-173.ru.tuna.am',
|
||||||
'-Dauthlibinjector.debug=verbose,authlib',
|
'-Dauthlibinjector.debug=verbose,authlib',
|
||||||
|
@ -53,3 +53,7 @@ h5 {
|
|||||||
h6 {
|
h6 {
|
||||||
font-family: 'Benzin-Bold' !important;
|
font-family: 'Benzin-Bold' !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-family: 'Benzin-Bold' !important;
|
||||||
|
}
|
||||||
|
@ -14,6 +14,8 @@ import { Box } from '@mui/material';
|
|||||||
import MinecraftBackground from './components/MinecraftBackground';
|
import MinecraftBackground from './components/MinecraftBackground';
|
||||||
import { Notifier } from './components/Notifier';
|
import { Notifier } from './components/Notifier';
|
||||||
import { VersionsExplorer } from './pages/VersionsExplorer';
|
import { VersionsExplorer } from './pages/VersionsExplorer';
|
||||||
|
import Profile from './pages/Profile';
|
||||||
|
import Shop from './pages/Shop';
|
||||||
|
|
||||||
const AuthCheck = ({ children }: { children: ReactNode }) => {
|
const AuthCheck = ({ children }: { children: ReactNode }) => {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||||
@ -120,7 +122,7 @@ const App = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MinecraftBackground />
|
<MinecraftBackground />
|
||||||
<TopBar onRegister={handleRegister} username={username} />
|
<TopBar onRegister={handleRegister} username={username || ''} />
|
||||||
<Notifier />
|
<Notifier />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
@ -140,6 +142,22 @@ const App = () => {
|
|||||||
</AuthCheck>
|
</AuthCheck>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/profile"
|
||||||
|
element={
|
||||||
|
<AuthCheck>
|
||||||
|
<Profile />
|
||||||
|
</AuthCheck>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/shop"
|
||||||
|
element={
|
||||||
|
<AuthCheck>
|
||||||
|
<Shop />
|
||||||
|
</AuthCheck>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Box>
|
</Box>
|
||||||
</Router>
|
</Router>
|
||||||
|
127
src/renderer/api.ts
Normal file
127
src/renderer/api.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
export const API_BASE_URL = 'http://147.78.65.214:8000';
|
||||||
|
|
||||||
|
export interface Player {
|
||||||
|
uuid: string;
|
||||||
|
username: string;
|
||||||
|
skin_url: string;
|
||||||
|
cloak_url: string;
|
||||||
|
coins: number;
|
||||||
|
is_active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CoinsResponse {
|
||||||
|
username: string;
|
||||||
|
coins: number;
|
||||||
|
total_time_played: {
|
||||||
|
seconds: number;
|
||||||
|
formatted: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiError {
|
||||||
|
message: string;
|
||||||
|
details?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение информации о игроке
|
||||||
|
export async function fetchPlayer(uuid: string): Promise<Player> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/users/${uuid}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.message || 'Ошибка получения данных игрока');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API ошибка:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCoins(username: string): Promise<CoinsResponse> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/users/${username}/coins`);
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.message || 'Ошибка получения данных игрока');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API ошибка:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загрузка скина
|
||||||
|
export async function uploadSkin(
|
||||||
|
username: string,
|
||||||
|
skinFile: File,
|
||||||
|
skinModel: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const savedConfig = localStorage.getItem('launcher_config');
|
||||||
|
let accessToken = '';
|
||||||
|
let clientToken = '';
|
||||||
|
|
||||||
|
if (savedConfig) {
|
||||||
|
const config = JSON.parse(savedConfig);
|
||||||
|
accessToken = config.accessToken || '';
|
||||||
|
clientToken = config.clientToken || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('skin_file', skinFile);
|
||||||
|
formData.append('skin_model', skinModel);
|
||||||
|
formData.append('accessToken', accessToken);
|
||||||
|
formData.append('clientToken', clientToken);
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/${username}/skin`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.message || 'Не удалось загрузить скин');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение токенов из локального хранилища
|
||||||
|
export function getAuthTokens(): { accessToken: string; clientToken: string } {
|
||||||
|
const savedConfig = localStorage.getItem('launcher_config');
|
||||||
|
let accessToken = '';
|
||||||
|
let clientToken = '';
|
||||||
|
|
||||||
|
if (savedConfig) {
|
||||||
|
const config = JSON.parse(savedConfig);
|
||||||
|
accessToken = config.accessToken || '';
|
||||||
|
clientToken = config.clientToken || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { accessToken, clientToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загрузка плаща
|
||||||
|
export async function uploadCape(
|
||||||
|
username: string,
|
||||||
|
capeFile: File,
|
||||||
|
): Promise<void> {
|
||||||
|
const { accessToken, clientToken } = getAuthTokens();
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('cape_file', capeFile);
|
||||||
|
formData.append('accessToken', accessToken);
|
||||||
|
formData.append('clientToken', clientToken);
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/${username}/cape`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.message || 'Не удалось загрузить плащ');
|
||||||
|
}
|
||||||
|
}
|
72
src/renderer/components/SkinViewer.tsx
Normal file
72
src/renderer/components/SkinViewer.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
interface SkinViewerProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
skinUrl?: string;
|
||||||
|
capeUrl?: string;
|
||||||
|
walkingSpeed?: number;
|
||||||
|
autoRotate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SkinViewer({
|
||||||
|
width = 300,
|
||||||
|
height = 400,
|
||||||
|
skinUrl,
|
||||||
|
capeUrl,
|
||||||
|
walkingSpeed = 0.5,
|
||||||
|
autoRotate = true,
|
||||||
|
}: SkinViewerProps) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const viewerRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canvasRef.current) return;
|
||||||
|
|
||||||
|
// Используем динамический импорт для обхода проблемы ESM/CommonJS
|
||||||
|
const initSkinViewer = async () => {
|
||||||
|
try {
|
||||||
|
const skinview3d = await import('skinview3d');
|
||||||
|
|
||||||
|
// Создаем просмотрщик скина по документации
|
||||||
|
const viewer = new skinview3d.SkinViewer({
|
||||||
|
canvas: canvasRef.current,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
skin: skinUrl || undefined,
|
||||||
|
cape: capeUrl || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Настраиваем вращение
|
||||||
|
viewer.autoRotate = autoRotate;
|
||||||
|
|
||||||
|
// Настраиваем анимацию ходьбы
|
||||||
|
viewer.animation = new skinview3d.WalkingAnimation();
|
||||||
|
viewer.animation.speed = walkingSpeed;
|
||||||
|
|
||||||
|
// Сохраняем экземпляр для очистки
|
||||||
|
viewerRef.current = viewer;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при инициализации skinview3d:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initSkinViewer();
|
||||||
|
|
||||||
|
// Очистка при размонтировании
|
||||||
|
return () => {
|
||||||
|
if (viewerRef.current) {
|
||||||
|
viewerRef.current.dispose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [width, height, skinUrl, capeUrl, walkingSpeed, autoRotate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
style={{ display: 'block' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
import { Box, Button, Typography } from '@mui/material';
|
import { Box, Button, Tab, Tabs, Typography } from '@mui/material';
|
||||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Tooltip } from '@mui/material';
|
import { Tooltip } from '@mui/material';
|
||||||
|
import { fetchCoins } from '../api';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -23,15 +24,6 @@ interface TopBarProps {
|
|||||||
username?: string;
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CoinsResponse {
|
|
||||||
username: string;
|
|
||||||
coins: number;
|
|
||||||
total_time_played: {
|
|
||||||
seconds: number;
|
|
||||||
formatted: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TopBar({ onRegister, username }: TopBarProps) {
|
export default function TopBar({ onRegister, username }: TopBarProps) {
|
||||||
// Получаем текущий путь
|
// Получаем текущий путь
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -40,6 +32,18 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
|||||||
const isVersionsExplorerPage = location.pathname.startsWith('/');
|
const isVersionsExplorerPage = location.pathname.startsWith('/');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [coins, setCoins] = useState<number>(0);
|
const [coins, setCoins] = useState<number>(0);
|
||||||
|
const [value, setValue] = useState(0);
|
||||||
|
|
||||||
|
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||||
|
setValue(newValue);
|
||||||
|
if (newValue === 0) {
|
||||||
|
navigate('/');
|
||||||
|
} else if (newValue === 1) {
|
||||||
|
navigate('/profile');
|
||||||
|
} else if (newValue === 2) {
|
||||||
|
navigate('/shop');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleLaunchPage = () => {
|
const handleLaunchPage = () => {
|
||||||
navigate('/');
|
navigate('/');
|
||||||
@ -59,17 +63,12 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Функция для получения количества монет
|
// Функция для получения количества монет
|
||||||
const fetchCoins = async () => {
|
const fetchCoinsData = async () => {
|
||||||
if (!username) return;
|
if (!username) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const coinsData = await fetchCoins(username);
|
||||||
`http://147.78.65.214:8000/users/${username}/coins`,
|
setCoins(coinsData.coins);
|
||||||
);
|
|
||||||
if (response.ok) {
|
|
||||||
const data: CoinsResponse = await response.json();
|
|
||||||
setCoins(data.coins);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при получении количества монет:', error);
|
console.error('Ошибка при получении количества монет:', error);
|
||||||
}
|
}
|
||||||
@ -77,8 +76,8 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (username) {
|
if (username) {
|
||||||
fetchCoins();
|
fetchCoinsData();
|
||||||
const intervalId = setInterval(fetchCoins, 60000);
|
const intervalId = setInterval(fetchCoinsData, 60000);
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}
|
}
|
||||||
}, [username]);
|
}, [username]);
|
||||||
@ -130,6 +129,28 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
|||||||
<ArrowBackRoundedIcon />
|
<ArrowBackRoundedIcon />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{!isLaunchPage && (
|
||||||
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||||
|
<Tabs
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
aria-label="basic tabs example"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
label="Версии"
|
||||||
|
sx={{ color: 'white', fontFamily: 'Benzin-Bold' }}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
label="Профиль"
|
||||||
|
sx={{ color: 'white', fontFamily: 'Benzin-Bold' }}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
label="Магазин"
|
||||||
|
sx={{ color: 'white', fontFamily: 'Benzin-Bold' }}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{/* Центр */}
|
{/* Центр */}
|
||||||
<Box
|
<Box
|
||||||
|
247
src/renderer/pages/Profile.tsx
Normal file
247
src/renderer/pages/Profile.tsx
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import SkinViewer from '../components/SkinViewer';
|
||||||
|
import { fetchPlayer, uploadSkin } from '../api';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Alert,
|
||||||
|
CircularProgress,
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
export default function Profile() {
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [walkingSpeed, setWalkingSpeed] = useState<number>(0.5);
|
||||||
|
const [skin, setSkin] = useState<string>('');
|
||||||
|
const [cape, setCape] = useState<string>('');
|
||||||
|
const [username, setUsername] = useState<string>('');
|
||||||
|
const [skinFile, setSkinFile] = useState<File | null>(null);
|
||||||
|
const [skinModel, setSkinModel] = useState<string>(''); // slim или classic
|
||||||
|
const [uploadStatus, setUploadStatus] = useState<
|
||||||
|
'idle' | 'loading' | 'success' | 'error'
|
||||||
|
>('idle');
|
||||||
|
const [statusMessage, setStatusMessage] = useState<string>('');
|
||||||
|
const [isDragOver, setIsDragOver] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedConfig = localStorage.getItem('launcher_config');
|
||||||
|
if (savedConfig) {
|
||||||
|
const config = JSON.parse(savedConfig);
|
||||||
|
if (config.uuid) {
|
||||||
|
loadPlayerData(config.uuid);
|
||||||
|
setUsername(config.username || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadPlayerData = async (uuid: string) => {
|
||||||
|
try {
|
||||||
|
const player = await fetchPlayer(uuid);
|
||||||
|
setSkin(player.skin_url);
|
||||||
|
setCape(player.cloak_url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении данных игрока:', error);
|
||||||
|
setSkin('');
|
||||||
|
setCape('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработка перетаскивания файла
|
||||||
|
const handleFileDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragOver(false);
|
||||||
|
|
||||||
|
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||||
|
const file = e.dataTransfer.files[0];
|
||||||
|
if (file.type === 'image/png') {
|
||||||
|
setSkinFile(file);
|
||||||
|
setStatusMessage(`Файл "${file.name}" готов к загрузке`);
|
||||||
|
} else {
|
||||||
|
setStatusMessage('Пожалуйста, выберите файл в формате PNG');
|
||||||
|
setUploadStatus('error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработка выбора файла
|
||||||
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file.type === 'image/png') {
|
||||||
|
setSkinFile(file);
|
||||||
|
setStatusMessage(`Файл "${file.name}" готов к загрузке`);
|
||||||
|
} else {
|
||||||
|
setStatusMessage('Пожалуйста, выберите файл в формате PNG');
|
||||||
|
setUploadStatus('error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Отправка запроса на установку скина
|
||||||
|
const handleUploadSkin = async () => {
|
||||||
|
if (!skinFile || !username) {
|
||||||
|
setStatusMessage('Необходимо выбрать файл и указать имя пользователя');
|
||||||
|
setUploadStatus('error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadStatus('loading');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await uploadSkin(username, skinFile, skinModel);
|
||||||
|
|
||||||
|
setStatusMessage('Скин успешно загружен!');
|
||||||
|
setUploadStatus('success');
|
||||||
|
|
||||||
|
// Обновляем информацию о игроке, чтобы увидеть новый скин
|
||||||
|
const config = JSON.parse(
|
||||||
|
localStorage.getItem('launcher_config') || '{}',
|
||||||
|
);
|
||||||
|
if (config.uuid) {
|
||||||
|
loadPlayerData(config.uuid);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setStatusMessage(
|
||||||
|
`Ошибка: ${error instanceof Error ? error.message : 'Не удалось загрузить скин'}`,
|
||||||
|
);
|
||||||
|
setUploadStatus('error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
my: 4,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
borderRadius: 2,
|
||||||
|
overflow: 'hidden',
|
||||||
|
mb: 4,
|
||||||
|
bgcolor: 'transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Используем переработанный компонент SkinViewer */}
|
||||||
|
<SkinViewer
|
||||||
|
width={300}
|
||||||
|
height={400}
|
||||||
|
skinUrl={skin}
|
||||||
|
capeUrl={cape}
|
||||||
|
walkingSpeed={walkingSpeed}
|
||||||
|
autoRotate={true}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Box sx={{ width: '100%', maxWidth: '500px', mt: 2 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Установить скин
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
border: '2px dashed',
|
||||||
|
borderColor: isDragOver ? 'primary.main' : 'grey.400',
|
||||||
|
borderRadius: 2,
|
||||||
|
p: 3,
|
||||||
|
mb: 2,
|
||||||
|
textAlign: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
bgcolor: isDragOver ? 'rgba(25, 118, 210, 0.08)' : 'transparent',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
}}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragOver(true);
|
||||||
|
}}
|
||||||
|
onDragLeave={() => setIsDragOver(false)}
|
||||||
|
onDrop={handleFileDrop}
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
accept=".png"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
/>
|
||||||
|
<Typography sx={{ color: 'white' }}>
|
||||||
|
{skinFile
|
||||||
|
? `Выбран файл: ${skinFile.name}`
|
||||||
|
: 'Перетащите PNG файл скина или кликните для выбора'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<FormControl color="primary" fullWidth sx={{ mb: 2, color: 'white' }}>
|
||||||
|
<InputLabel sx={{ color: 'white' }}>Модель скина</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={skinModel}
|
||||||
|
label="Модель скина"
|
||||||
|
onChange={(e) => setSkinModel(e.target.value)}
|
||||||
|
sx={{
|
||||||
|
color: 'white',
|
||||||
|
borderColor: 'white',
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
border: '1px solid white',
|
||||||
|
transition: 'unset',
|
||||||
|
},
|
||||||
|
'&:focus': {
|
||||||
|
borderRadius: 4,
|
||||||
|
borderColor: '#80bdff',
|
||||||
|
boxShadow: '0 0 0 0.2rem rgba(0,123,255,.25)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value="">По умолчанию</MenuItem>
|
||||||
|
<MenuItem value="slim">Тонкая (Alex)</MenuItem>
|
||||||
|
<MenuItem value="classic">Классическая (Steve)</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{uploadStatus === 'error' && (
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
{statusMessage}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{uploadStatus === 'success' && (
|
||||||
|
<Alert severity="success" sx={{ mb: 2 }}>
|
||||||
|
{statusMessage}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
sx={{ color: 'white' }}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
fullWidth
|
||||||
|
onClick={handleUploadSkin}
|
||||||
|
disabled={uploadStatus === 'loading' || !skinFile}
|
||||||
|
startIcon={
|
||||||
|
uploadStatus === 'loading' ? (
|
||||||
|
<CircularProgress size={20} color="inherit" />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{uploadStatus === 'loading' ? (
|
||||||
|
<Typography sx={{ color: 'white' }}>Загрузка...</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography sx={{ color: 'white' }}>Установить скин</Typography>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
3
src/renderer/pages/Shop.tsx
Normal file
3
src/renderer/pages/Shop.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function Shop() {
|
||||||
|
return <div>Shop</div>;
|
||||||
|
}
|
Reference in New Issue
Block a user