Compare commits
23 Commits
dev
...
07caa7c53c
Author | SHA1 | Date | |
---|---|---|---|
07caa7c53c | |||
a2f42346ae | |||
932d867505 | |||
212b58c072 | |||
3d78d3e279 | |||
e018aec8db | |||
fa115e0a6c | |||
51b155e70a | |||
6ee1b67315 | |||
c39a8bc43c | |||
56da7c7543 | |||
26f601635b | |||
ec54219192 | |||
f3aa32a60a | |||
7938555c91 | |||
591e354dcb | |||
ce141a014c | |||
3d4c9c89ef | |||
90d4bb80e6 | |||
fdf5c7a90d | |||
387d1548ba | |||
942066ea76 | |||
815ce286f7 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
|
public/
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
BIN
assets/icons/popa-popa.png
Normal file
BIN
assets/icons/popa-popa.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
18
assets/icons/popa-popa.svg
Normal file
18
assets/icons/popa-popa.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 80 KiB |
@ -1,11 +1,23 @@
|
|||||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect x="4" width="8" height="4" fill="#FF2D0F"/>
|
<rect x="0" y="12" width="28" height="4" fill="#BD2211"/>
|
||||||
<rect x="8" y="4" width="4" height="16" fill="#FF2D0F"/>
|
<rect x="4" y="16" width="20" height="4" fill="#BD2211"/>
|
||||||
<rect x="4" y="8" width="4" height="8" fill="#FF2D0F"/>
|
<rect x="8" y="20" width="12" height="4" fill="#BD2211"/>
|
||||||
|
<rect x="12" y="24" width="4" height="4" fill="#BD2211"/>
|
||||||
|
<rect x="4" width="8" height="4.5" fill="#FF2D0F"/>
|
||||||
|
<rect x="16" width="8" height="4.5" fill="#FF2D0F"/>
|
||||||
|
<rect x="0" y="4" width="28" height="8" fill="#FF2D0F"/>
|
||||||
|
<rect x="4" y="8" width="20" height="8" fill="#FF2D0F"/>
|
||||||
|
<rect x="8" y="12" width="12" height="8" fill="#FF2D0F"/>
|
||||||
|
<rect x="12" y="16" width="4" height="8" fill="#FF2D0F"/>
|
||||||
|
<rect x="4" y="4" width="4" height="4" fill="#FFCAC8"/>
|
||||||
|
<!-- <rect x="7" y="4" width="6" height="16" fill="#FF2D0F"/>
|
||||||
|
<rect x="6" y="3" width="6" height="16" fill="#FF2D0F"/>
|
||||||
|
<rect x="3" y="8" width="4" height="8" fill="#FF2D0F"/>
|
||||||
<rect y="4" width="4" height="8" fill="#FF2D0F"/>
|
<rect y="4" width="4" height="8" fill="#FF2D0F"/>
|
||||||
<rect x="24" y="4" width="4" height="8" fill="#FF2D0F"/>
|
<rect x="24" y="4" width="4" height="8" fill="#FF2D0F"/>
|
||||||
<rect x="16" width="8" height="16" fill="#FF2D0F"/>
|
<rect x="16" width="8" height="16" fill="#FF2D0F"/>
|
||||||
<rect x="16" y="16" width="4" height="4" fill="#FF2D0F"/>
|
<rect x="20" y="4" width="8" height="12" fill="#FF2D0F"/>
|
||||||
|
<rect x="15" y="4" width="5" height="16" fill="#FF2D0F"/>
|
||||||
<rect x="24" y="12" width="4" height="4" fill="#BD2211"/>
|
<rect x="24" y="12" width="4" height="4" fill="#BD2211"/>
|
||||||
<rect x="20" y="16" width="4" height="4" fill="#BD2211"/>
|
<rect x="20" y="16" width="4" height="4" fill="#BD2211"/>
|
||||||
<rect x="16" y="20" width="4" height="4" fill="#BD2211"/>
|
<rect x="16" y="20" width="4" height="4" fill="#BD2211"/>
|
||||||
@ -14,5 +26,5 @@
|
|||||||
<rect x="4" y="16" width="4" height="4" fill="#BD2211"/>
|
<rect x="4" y="16" width="4" height="4" fill="#BD2211"/>
|
||||||
<rect x="4" y="4" width="4" height="4" fill="#FFCAC8"/>
|
<rect x="4" y="4" width="4" height="4" fill="#FFCAC8"/>
|
||||||
<rect y="12" width="4" height="4" fill="#BD2211"/>
|
<rect y="12" width="4" height="4" fill="#BD2211"/>
|
||||||
<rect x="12" y="4" width="4" height="20" fill="#FF2D0F"/>
|
<rect x="12" y="4" width="4" height="20" fill="#FF2D0F"/> -->
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 994 B After Width: | Height: | Size: 1.7 KiB |
169
package-lock.json
generated
169
package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"@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/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",
|
||||||
@ -22,10 +23,13 @@
|
|||||||
"find-java-home": "^2.0.0",
|
"find-java-home": "^2.0.0",
|
||||||
"https-browserify": "^1.0.0",
|
"https-browserify": "^1.0.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
|
"qr-code-styling": "^1.9.2",
|
||||||
"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,32 @@
|
|||||||
"node": ">=20.18.1"
|
"node": ">=20.18.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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 +8515,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 +12022,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 +13388,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 +15443,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 +15455,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 +15476,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,7 +15652,6 @@
|
|||||||
"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"
|
||||||
@ -15959,6 +16038,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 +17024,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 +18005,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": {
|
||||||
@ -18054,6 +18137,24 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/qr-code-styling": {
|
||||||
|
"version": "1.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz",
|
||||||
|
"integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"qrcode-generator": "^1.4.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode-generator": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.13.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
@ -19482,7 +19583,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 +19756,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 +20744,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,7 @@
|
|||||||
"@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/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",
|
||||||
@ -116,10 +117,13 @@
|
|||||||
"find-java-home": "^2.0.0",
|
"find-java-home": "^2.0.0",
|
||||||
"https-browserify": "^1.0.0",
|
"https-browserify": "^1.0.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
|
"qr-code-styling": "^1.9.2",
|
||||||
"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",
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { YggdrasilClient, YggrasilAuthentication } from '@xmcl/user';
|
import { YggdrasilClient, YggrasilAuthentication } from '@xmcl/user';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { API_BASE_URL } from '../renderer/api';
|
||||||
|
|
||||||
// Ely.by сервер
|
// Ely.by сервер
|
||||||
const ELY_BY_AUTH_SERVER = 'https://authserver.ely.by';
|
const ELY_BY_AUTH_SERVER = API_BASE_URL;
|
||||||
|
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private client: YggdrasilClient;
|
private client: YggdrasilClient;
|
||||||
@ -49,6 +50,7 @@ export class AuthService {
|
|||||||
|
|
||||||
async validate(accessToken: string, clientToken: string): Promise<boolean> {
|
async validate(accessToken: string, clientToken: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
|
console.log(accessToken, clientToken);
|
||||||
const response = await fetch(`${ELY_BY_AUTH_SERVER}/auth/validate`, {
|
const response = await fetch(`${ELY_BY_AUTH_SERVER}/auth/validate`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -119,7 +119,7 @@ const createWindow = async () => {
|
|||||||
width: 1024,
|
width: 1024,
|
||||||
height: 850,
|
height: 850,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
resizable: false,
|
resizable: true,
|
||||||
frame: false,
|
frame: false,
|
||||||
icon: getAssetPath('icon.png'),
|
icon: getAssetPath('icon.png'),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
|
@ -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';
|
||||||
@ -280,7 +281,7 @@ export async function findJava(): Promise<string> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Предпочитаем Java 21 или 17 для совместимости с authlib-injector
|
// Предпочитаем Java 21 или 17 для совместимости с authlib-injector
|
||||||
const preferredVersions = [21, 17, 11];
|
const preferredVersions = [24, 21, 17, 11];
|
||||||
|
|
||||||
for (const preferredVersion of preferredVersions) {
|
for (const preferredVersion of preferredVersions) {
|
||||||
const preferred = javaVersions.find(
|
const preferred = javaVersions.find(
|
||||||
@ -479,7 +480,7 @@ export function initMinecraftHandlers() {
|
|||||||
username,
|
username,
|
||||||
memory = 4096,
|
memory = 4096,
|
||||||
baseVersion = '1.21.4',
|
baseVersion = '1.21.4',
|
||||||
fabricVersion = 'fabric0.16.14',
|
fabricVersion = '0.16.14',
|
||||||
packName = 'Comfort', // Название основной сборки
|
packName = 'Comfort', // Название основной сборки
|
||||||
versionToLaunchOverride = '', // Возможность переопределить версию для запуска
|
versionToLaunchOverride = '', // Возможность переопределить версию для запуска
|
||||||
serverIp = 'popa-popa.ru',
|
serverIp = 'popa-popa.ru',
|
||||||
@ -499,6 +500,8 @@ export function initMinecraftHandlers() {
|
|||||||
// Найти версию пакета, Fabric или базовую версию
|
// Найти версию пакета, Fabric или базовую версию
|
||||||
let versionToLaunch = versionToLaunchOverride;
|
let versionToLaunch = versionToLaunchOverride;
|
||||||
|
|
||||||
|
console.log('fabric:', `${baseVersion}-fabric${fabricVersion}`);
|
||||||
|
|
||||||
if (!versionToLaunch) {
|
if (!versionToLaunch) {
|
||||||
if (
|
if (
|
||||||
versionsContents.includes(`${baseVersion}-fabric${fabricVersion}`)
|
versionsContents.includes(`${baseVersion}-fabric${fabricVersion}`)
|
||||||
@ -519,6 +522,8 @@ export function initMinecraftHandlers() {
|
|||||||
message: 'Поиск Java...',
|
message: 'Поиск Java...',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Поиск Java...');
|
||||||
|
|
||||||
let javaPath;
|
let javaPath;
|
||||||
try {
|
try {
|
||||||
javaPath = await findJava();
|
javaPath = await findJava();
|
||||||
@ -540,11 +545,15 @@ export function initMinecraftHandlers() {
|
|||||||
message: 'Получение списка версий Minecraft...',
|
message: 'Получение списка версий Minecraft...',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Получение списка версий Minecraft...');
|
||||||
|
|
||||||
const versionList = await getVersionList();
|
const versionList = await getVersionList();
|
||||||
const minecraftVersion = versionList.versions.find(
|
const minecraftVersion = versionList.versions.find(
|
||||||
(v) => v.id === baseVersion,
|
(v) => v.id === baseVersion,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log('minecraftVersion:', minecraftVersion);
|
||||||
|
|
||||||
if (minecraftVersion) {
|
if (minecraftVersion) {
|
||||||
// Устанавливаем базовую версию Minecraft
|
// Устанавливаем базовую версию Minecraft
|
||||||
event.sender.send('installation-status', {
|
event.sender.send('installation-status', {
|
||||||
@ -552,11 +561,15 @@ export function initMinecraftHandlers() {
|
|||||||
message: `Установка Minecraft ${baseVersion}...`,
|
message: `Установка Minecraft ${baseVersion}...`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Установка Minecraft...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const installMcTask = installTask(minecraftVersion, minecraftDir, {
|
const installMcTask = installTask(minecraftVersion, minecraftDir, {
|
||||||
skipRevalidate: true,
|
skipRevalidate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('installMcTask:', installMcTask);
|
||||||
|
|
||||||
await installMcTask.startAndWait({
|
await installMcTask.startAndWait({
|
||||||
onStart(task) {
|
onStart(task) {
|
||||||
event.sender.send('installation-status', {
|
event.sender.send('installation-status', {
|
||||||
@ -577,6 +590,7 @@ export function initMinecraftHandlers() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onFailed(task, error) {
|
onFailed(task, error) {
|
||||||
|
console.log('onFailed:', task, error);
|
||||||
console.warn(
|
console.warn(
|
||||||
`Ошибка при установке ${task.path}, продолжаем:`,
|
`Ошибка при установке ${task.path}, продолжаем:`,
|
||||||
error,
|
error,
|
||||||
@ -594,10 +608,15 @@ export function initMinecraftHandlers() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Ошибка при установке Minecraft, продолжаем:', error);
|
console.log('Ошибка при установке Minecraft, продолжаем:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Устанавливаем Fabric
|
// 2. Устанавливаем Fabric
|
||||||
|
console.log('Попытка установки Fabric:', {
|
||||||
|
minecraftVersion: baseVersion,
|
||||||
|
fabricVersion: fabricVersion,
|
||||||
|
minecraftDir: minecraftDir,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
event.sender.send('installation-status', {
|
event.sender.send('installation-status', {
|
||||||
step: 'fabric-list',
|
step: 'fabric-list',
|
||||||
@ -610,6 +629,12 @@ export function initMinecraftHandlers() {
|
|||||||
message: `Установка Fabric ${fabricVersion}...`,
|
message: `Установка Fabric ${fabricVersion}...`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('installFabric:', {
|
||||||
|
minecraftVersion: baseVersion,
|
||||||
|
fabricVersion: fabricVersion,
|
||||||
|
minecraftDir: minecraftDir,
|
||||||
|
});
|
||||||
|
|
||||||
await installFabric({
|
await installFabric({
|
||||||
minecraftVersion: baseVersion,
|
minecraftVersion: baseVersion,
|
||||||
version: fabricVersion, // Используйте напрямую, без .version
|
version: fabricVersion, // Используйте напрямую, без .version
|
||||||
@ -617,7 +642,7 @@ export function initMinecraftHandlers() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Ошибка при установке Fabric, продолжаем:', error);
|
console.log('Ошибка при установке Fabric, продолжаем:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Подготовка версии и установка зависимостей
|
// 3. Подготовка версии и установка зависимостей
|
||||||
@ -625,11 +650,18 @@ export function initMinecraftHandlers() {
|
|||||||
// Используем идентификатор Fabric-версии
|
// Используем идентификатор Fabric-версии
|
||||||
const fabricVersionId = `${baseVersion}-fabric${fabricVersion}`;
|
const fabricVersionId = `${baseVersion}-fabric${fabricVersion}`;
|
||||||
|
|
||||||
|
console.log('version-parse:', fabricVersionId);
|
||||||
|
|
||||||
event.sender.send('installation-status', {
|
event.sender.send('installation-status', {
|
||||||
step: 'version-parse',
|
step: 'version-parse',
|
||||||
message: 'Подготовка версии...',
|
message: 'Подготовка версии...',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('version-parse:', {
|
||||||
|
minecraftDir: minecraftDir,
|
||||||
|
fabricVersionId: fabricVersionId,
|
||||||
|
});
|
||||||
|
|
||||||
resolvedVersion = await Version.parse(
|
resolvedVersion = await Version.parse(
|
||||||
minecraftDir,
|
minecraftDir,
|
||||||
fabricVersionId,
|
fabricVersionId,
|
||||||
@ -670,6 +702,7 @@ export function initMinecraftHandlers() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onFailed(task, error) {
|
onFailed(task, error) {
|
||||||
|
console.log('onFailed:', task, error);
|
||||||
console.warn(
|
console.warn(
|
||||||
`Ошибка при установке ${task.path}, продолжаем:`,
|
`Ошибка при установке ${task.path}, продолжаем:`,
|
||||||
error,
|
error,
|
||||||
@ -687,32 +720,38 @@ export function initMinecraftHandlers() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.log(
|
||||||
'Ошибка при загрузке ресурсов, продолжаем запуск:',
|
'Ошибка при загрузке ресурсов, продолжаем запуск:',
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Ошибка при подготовке версии, продолжаем:', error);
|
console.log('Ошибка при подготовке версии, продолжаем:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Произошла ошибка при подготовке Minecraft:', error);
|
console.log('Произошла ошибка при подготовке Minecraft:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загрузка и проверка authlib-injector
|
// Загрузка и проверка authlib-injector
|
||||||
const authlibPath = await ensureAuthlibInjectorExists(appPath);
|
const authlibPath = await ensureAuthlibInjectorExists(appPath);
|
||||||
|
console.log('authlibPath:', authlibPath);
|
||||||
|
|
||||||
event.sender.send('installation-status', {
|
event.sender.send('installation-status', {
|
||||||
step: 'authlib-injector',
|
step: 'authlib-injector',
|
||||||
message: 'authlib-injector готов',
|
message: 'authlib-injector готов',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Запускаем Minecraft с authlib-injector для Ely.by
|
// Запускаем Minecraft с authlib-injector для Ely.by
|
||||||
|
console.log('Запуск игры...');
|
||||||
|
|
||||||
event.sender.send('installation-status', {
|
event.sender.send('installation-status', {
|
||||||
step: 'launch',
|
step: 'launch',
|
||||||
message: 'Запуск игры...',
|
message: 'Запуск игры...',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Запуск игры...');
|
||||||
|
|
||||||
// При запуске используем переданные параметры
|
// При запуске используем переданные параметры
|
||||||
const packDir = path.join(versionsDir, packName);
|
const packDir = path.join(versionsDir, packName);
|
||||||
|
|
||||||
@ -724,17 +763,27 @@ export function initMinecraftHandlers() {
|
|||||||
serverConfig.port = serverPort;
|
serverConfig.port = serverPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('packDir:', packDir);
|
||||||
|
|
||||||
const proc = await launch({
|
const proc = await launch({
|
||||||
gamePath: packDir,
|
gamePath: packDir,
|
||||||
resourcePath: minecraftDir,
|
resourcePath: minecraftDir,
|
||||||
javaPath,
|
javaPath,
|
||||||
version: versionToLaunch,
|
version: versionToLaunch,
|
||||||
launcherName: 'popa-popa',
|
launcherName: 'popa-popa',
|
||||||
server: serverConfig, // Используем созданный объект конфигурации
|
|
||||||
extraJVMArgs: [
|
extraJVMArgs: [
|
||||||
'-Dlog4j2.formatMsgNoLookups=true',
|
'-Dlog4j2.formatMsgNoLookups=true',
|
||||||
`-javaagent:${authlibPath}=ely.by`,
|
`-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.debug=verbose,authlib',
|
||||||
|
'-Dauthlibinjector.legacySkinPolyfill=enabled',
|
||||||
|
'-Dauthlibinjector.mojangAntiFeatures=disabled',
|
||||||
|
'-Dcom.mojang.authlib.disableSecureProfileEndpoints=true',
|
||||||
|
],
|
||||||
|
extraMCArgs: [
|
||||||
|
'--quickPlayMultiplayer',
|
||||||
|
`${serverIp}:${serverPort || 25565}`,
|
||||||
],
|
],
|
||||||
// Используем данные аутентификации Yggdrasil
|
// Используем данные аутентификации Yggdrasil
|
||||||
accessToken,
|
accessToken,
|
||||||
@ -752,6 +801,8 @@ export function initMinecraftHandlers() {
|
|||||||
console.error(`Minecraft stderr: ${data}`);
|
console.error(`Minecraft stderr: ${data}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Запуск игры...');
|
||||||
|
|
||||||
return { success: true, pid: proc.pid };
|
return { success: true, pid: proc.pid };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при запуске Minecraft:', error);
|
console.error('Ошибка при запуске Minecraft:', error);
|
||||||
@ -804,6 +855,106 @@ export function initMinecraftHandlers() {
|
|||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ПРОБЛЕМА: У вас два обработчика для одного и того же канала 'get-installed-versions'
|
||||||
|
// РЕШЕНИЕ: Объединим логику в один обработчик, а из второго обработчика вызовем функцию getInstalledVersions
|
||||||
|
|
||||||
|
// Сначала создаем общую функцию для получения установленных версий
|
||||||
|
function getInstalledVersions() {
|
||||||
|
try {
|
||||||
|
const appPath = path.dirname(app.getPath('exe'));
|
||||||
|
const minecraftDir = path.join(appPath, '.minecraft');
|
||||||
|
const versionsDir = path.join(minecraftDir, 'versions');
|
||||||
|
|
||||||
|
if (!fs.existsSync(versionsDir)) {
|
||||||
|
return { success: true, versions: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = fs.readdirSync(versionsDir);
|
||||||
|
const versions = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const versionPath = path.join(versionsDir, item);
|
||||||
|
if (fs.statSync(versionPath).isDirectory()) {
|
||||||
|
// Проверяем, есть ли конфигурация для пакета
|
||||||
|
const versionJsonPath = path.join(versionPath, `${item}.json`);
|
||||||
|
let versionInfo = {
|
||||||
|
id: item,
|
||||||
|
name: item,
|
||||||
|
version: item,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fs.existsSync(versionJsonPath)) {
|
||||||
|
try {
|
||||||
|
const versionData = JSON.parse(
|
||||||
|
fs.readFileSync(versionJsonPath, 'utf8'),
|
||||||
|
);
|
||||||
|
versionInfo.version = versionData.id || item;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Ошибка при чтении файла версии ${item}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
versions.push(versionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, versions };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении установленных версий:', error);
|
||||||
|
return { success: false, error: error.message, versions: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Регистрируем обработчик для get-installed-versions
|
||||||
|
ipcMain.handle('get-installed-versions', async () => {
|
||||||
|
return getInstalledVersions();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик get-available-versions использует функцию getInstalledVersions
|
||||||
|
ipcMain.handle('get-available-versions', async (event, { gistUrl }) => {
|
||||||
|
try {
|
||||||
|
// Используем URL из параметров или значение по умолчанию
|
||||||
|
const url =
|
||||||
|
gistUrl ||
|
||||||
|
'https://gist.githubusercontent.com/DIKER0K/06cd12fb3a4d08b1f0f8c763a7d05e06/raw/versions.json';
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch versions from Gist: ${response.status} ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = await response.json();
|
||||||
|
|
||||||
|
// Получаем уже установленные версии
|
||||||
|
const installedResult = getInstalledVersions();
|
||||||
|
const installedVersions = installedResult.success
|
||||||
|
? installedResult.versions
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Добавляем флаг installed к каждой версии
|
||||||
|
const versionsWithInstallStatus = versions.map((version: any) => {
|
||||||
|
const isInstalled = installedVersions.some(
|
||||||
|
(installed: any) => installed.id === version.id,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...version,
|
||||||
|
installed: isInstalled,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
versions: versionsWithInstallStatus,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении доступных версий:', error);
|
||||||
|
return { success: false, error: error.message, versions: [] };
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем обработчики IPC для аутентификации
|
// Добавляем обработчики IPC для аутентификации
|
||||||
@ -824,22 +975,18 @@ export function initAuthHandlers() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Валидация токена
|
// Валидация токена
|
||||||
ipcMain.handle('validate-token', async (event, accessToken) => {
|
ipcMain.handle(
|
||||||
try {
|
'validate-token',
|
||||||
const clientToken = JSON.parse(
|
async (event, { accessToken, clientToken }) => {
|
||||||
fs.readFileSync(
|
try {
|
||||||
path.join(app.getPath('userData'), 'config.json'),
|
const valid = await authService.validate(accessToken, clientToken);
|
||||||
'utf8',
|
return { valid };
|
||||||
),
|
} catch (error) {
|
||||||
).clientToken;
|
console.error('Ошибка валидации токена:', error);
|
||||||
|
return { valid: false };
|
||||||
const valid = await authService.validate(accessToken, clientToken);
|
}
|
||||||
return { valid };
|
},
|
||||||
} catch (error) {
|
);
|
||||||
console.error('Ошибка валидации токена:', error);
|
|
||||||
return { valid: false };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обновление токена
|
// Обновление токена
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
@ -979,3 +1126,57 @@ export function initPackConfigHandlers() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Добавляем после обработчика get-available-versions
|
||||||
|
ipcMain.handle('get-version-config', async (event, { versionId }) => {
|
||||||
|
try {
|
||||||
|
const appPath = path.dirname(app.getPath('exe'));
|
||||||
|
const minecraftDir = path.join(appPath, '.minecraft');
|
||||||
|
const versionsDir = path.join(minecraftDir, 'versions');
|
||||||
|
const versionPath = path.join(versionsDir, versionId);
|
||||||
|
|
||||||
|
// Проверяем существование директории версии
|
||||||
|
if (!fs.existsSync(versionPath)) {
|
||||||
|
return { success: false, error: `Версия ${versionId} не найдена` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем конфигурационный файл версии
|
||||||
|
const configPath = path.join(versionPath, 'popa-launcher-config.json');
|
||||||
|
|
||||||
|
// Определяем базовые настройки по умолчанию
|
||||||
|
let config = {
|
||||||
|
downloadUrl: '',
|
||||||
|
apiReleaseUrl: '',
|
||||||
|
versionFileName: `${versionId}_version.txt`,
|
||||||
|
packName: versionId,
|
||||||
|
memory: 4096,
|
||||||
|
baseVersion: '1.21.4',
|
||||||
|
serverIp: 'popa-popa.ru',
|
||||||
|
fabricVersion: '0.16.14',
|
||||||
|
preserveFiles: ['popa-launcher-config.json'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Если это Comfort, используем настройки по умолчанию
|
||||||
|
if (versionId === 'Comfort') {
|
||||||
|
config.downloadUrl =
|
||||||
|
'https://github.com/DIKER0K/Comfort/releases/latest/download/Comfort.zip';
|
||||||
|
config.apiReleaseUrl =
|
||||||
|
'https://api.github.com/repos/DIKER0K/Comfort/releases/latest';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если есть конфигурационный файл, загружаем из него
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
try {
|
||||||
|
const savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
|
config = { ...config, ...savedConfig };
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Ошибка чтения конфигурации ${versionId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, config };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения настроек версии:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -11,7 +11,9 @@ export type Channels =
|
|||||||
| 'save-pack-config'
|
| 'save-pack-config'
|
||||||
| 'load-pack-config'
|
| 'load-pack-config'
|
||||||
| 'update-available'
|
| 'update-available'
|
||||||
| 'install-update';
|
| 'install-update'
|
||||||
|
| 'get-installed-versions'
|
||||||
|
| 'get-available-versions';
|
||||||
|
|
||||||
const electronHandler = {
|
const electronHandler = {
|
||||||
ipcRenderer: {
|
ipcRenderer: {
|
||||||
|
@ -22,6 +22,8 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@ -47,3 +49,11 @@ h4 {
|
|||||||
h5 {
|
h5 {
|
||||||
font-family: 'Benzin-Bold' !important;
|
font-family: 'Benzin-Bold' !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-family: 'Benzin-Bold' !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-family: 'Benzin-Bold' !important;
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
Routes,
|
Routes,
|
||||||
Route,
|
Route,
|
||||||
Navigate,
|
Navigate,
|
||||||
|
useNavigate,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import LaunchPage from './pages/LaunchPage';
|
import LaunchPage from './pages/LaunchPage';
|
||||||
@ -10,21 +11,13 @@ import { ReactNode, useEffect, useState } from 'react';
|
|||||||
import './App.css';
|
import './App.css';
|
||||||
import TopBar from './components/TopBar';
|
import TopBar from './components/TopBar';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import MinecraftBackround from './components/MinecraftBackround';
|
import MinecraftBackground from './components/MinecraftBackground';
|
||||||
import { Notifier } from './components/Notifier';
|
import { Notifier } from './components/Notifier';
|
||||||
|
import { VersionsExplorer } from './pages/VersionsExplorer';
|
||||||
// Переместите launchOptions сюда, вне компонентов
|
import Profile from './pages/Profile';
|
||||||
const launchOptions = {
|
import Shop from './pages/Shop';
|
||||||
downloadUrl:
|
import Marketplace from './pages/Marketplace';
|
||||||
'https://github.com/DIKER0K/Comfort/releases/latest/download/Comfort.zip',
|
import { Registration } from './pages/Registration';
|
||||||
apiReleaseUrl: 'https://api.github.com/repos/DIKER0K/Comfort/releases/latest',
|
|
||||||
versionFileName: 'comfort_version.txt',
|
|
||||||
packName: 'Comfort',
|
|
||||||
memory: 4096,
|
|
||||||
baseVersion: '1.21.4',
|
|
||||||
serverIp: 'popa-popa.ru',
|
|
||||||
fabricVersion: '0.16.14', // Уберите префикс "fabric"
|
|
||||||
};
|
|
||||||
|
|
||||||
const AuthCheck = ({ children }: { children: ReactNode }) => {
|
const AuthCheck = ({ children }: { children: ReactNode }) => {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||||
@ -37,7 +30,10 @@ const AuthCheck = ({ children }: { children: ReactNode }) => {
|
|||||||
const config = JSON.parse(savedConfig);
|
const config = JSON.parse(savedConfig);
|
||||||
if (config.accessToken) {
|
if (config.accessToken) {
|
||||||
// Можно добавить дополнительную проверку токена
|
// Можно добавить дополнительную проверку токена
|
||||||
const isValid = await validateToken(config.accessToken);
|
const isValid = await validateToken(
|
||||||
|
config.accessToken,
|
||||||
|
config.clientToken,
|
||||||
|
);
|
||||||
setIsAuthenticated(isValid);
|
setIsAuthenticated(isValid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -52,17 +48,39 @@ const AuthCheck = ({ children }: { children: ReactNode }) => {
|
|||||||
checkAuth();
|
checkAuth();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const validateToken = async (token: string) => {
|
const validateToken = async (accessToken: string, clientToken: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('https://authserver.ely.by/auth/validate', {
|
// Используем IPC для валидации токена через main процесс
|
||||||
method: 'POST',
|
const result = await window.electron.ipcRenderer.invoke(
|
||||||
headers: {
|
'validate-token',
|
||||||
'Content-Type': 'application/json',
|
{ accessToken, clientToken },
|
||||||
},
|
);
|
||||||
body: JSON.stringify({ accessToken: token }),
|
|
||||||
});
|
// Если токен недействителен, очищаем сохраненные данные в localStorage
|
||||||
return response.ok;
|
if (!result.valid) {
|
||||||
|
console.log(
|
||||||
|
'Токен недействителен, очищаем данные авторизации из localStorage',
|
||||||
|
);
|
||||||
|
const savedConfig = localStorage.getItem('launcher_config');
|
||||||
|
if (savedConfig) {
|
||||||
|
const config = JSON.parse(savedConfig);
|
||||||
|
// Сохраняем только логин и другие настройки, но удаляем токены
|
||||||
|
const cleanedConfig = {
|
||||||
|
username: config.username,
|
||||||
|
memory: config.memory || 4096,
|
||||||
|
comfortVersion: config.comfortVersion || '',
|
||||||
|
password: '', // Очищаем пароль для безопасности
|
||||||
|
};
|
||||||
|
localStorage.setItem(
|
||||||
|
'launcher_config',
|
||||||
|
JSON.stringify(cleanedConfig),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.valid;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки токена:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -79,6 +97,17 @@ const App = () => {
|
|||||||
const handleRegister = () => {
|
const handleRegister = () => {
|
||||||
window.open('https://account.ely.by/register', '_blank');
|
window.open('https://account.ely.by/register', '_blank');
|
||||||
};
|
};
|
||||||
|
const [username, setUsername] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedConfig = localStorage.getItem('launcher_config');
|
||||||
|
if (savedConfig) {
|
||||||
|
const config = JSON.parse(savedConfig);
|
||||||
|
if (config.username) {
|
||||||
|
setUsername(config.username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
@ -91,18 +120,52 @@ const App = () => {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
overflowX: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MinecraftBackround />
|
<MinecraftBackground />
|
||||||
<TopBar onRegister={handleRegister} />
|
<TopBar onRegister={handleRegister} username={username || ''} />
|
||||||
<Notifier />
|
<Notifier />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/registration" element={<Registration />} />
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
<AuthCheck>
|
<AuthCheck>
|
||||||
<LaunchPage launchOptions={launchOptions} />
|
<VersionsExplorer />
|
||||||
|
</AuthCheck>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/launch/:versionId"
|
||||||
|
element={
|
||||||
|
<AuthCheck>
|
||||||
|
<LaunchPage />
|
||||||
|
</AuthCheck>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/profile"
|
||||||
|
element={
|
||||||
|
<AuthCheck>
|
||||||
|
<Profile />
|
||||||
|
</AuthCheck>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/shop"
|
||||||
|
element={
|
||||||
|
<AuthCheck>
|
||||||
|
<Shop />
|
||||||
|
</AuthCheck>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/marketplace"
|
||||||
|
element={
|
||||||
|
<AuthCheck>
|
||||||
|
<Marketplace />
|
||||||
</AuthCheck>
|
</AuthCheck>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
628
src/renderer/api.ts
Normal file
628
src/renderer/api.ts
Normal file
@ -0,0 +1,628 @@
|
|||||||
|
export const API_BASE_URL = 'https://minecraft.api.popa-popa.ru';
|
||||||
|
|
||||||
|
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 Cape {
|
||||||
|
cape_id: string;
|
||||||
|
cape_name: string;
|
||||||
|
cape_description: string;
|
||||||
|
image_url: string;
|
||||||
|
purchased_at: string;
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CapesResponse = Cape[];
|
||||||
|
|
||||||
|
export interface StoreCape {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
image_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StoreCapesResponse = StoreCape[];
|
||||||
|
|
||||||
|
export interface ApiError {
|
||||||
|
message: string;
|
||||||
|
details?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Server {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
description: string;
|
||||||
|
online_players: number;
|
||||||
|
max_players: number;
|
||||||
|
last_activity: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActiveServersResponse {
|
||||||
|
servers: Server[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnlinePlayersResponse {
|
||||||
|
server: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
online_players: {
|
||||||
|
// Это массив объектов, а не один объект
|
||||||
|
username: string;
|
||||||
|
uuid: string;
|
||||||
|
online_since: string;
|
||||||
|
}[]; // Добавьте [] здесь чтобы указать, что это массив
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketplaceResponse {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
_id: string;
|
||||||
|
id: string;
|
||||||
|
material: string;
|
||||||
|
amount: number;
|
||||||
|
price: number;
|
||||||
|
seller_name: string;
|
||||||
|
server_ip: string;
|
||||||
|
display_name: string | null;
|
||||||
|
lore: string | null;
|
||||||
|
enchants: string | null;
|
||||||
|
item_data: {
|
||||||
|
slot: number;
|
||||||
|
material: string;
|
||||||
|
amount: number;
|
||||||
|
};
|
||||||
|
created_at: string;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketplaceItemResponse {
|
||||||
|
_id: string;
|
||||||
|
id: string;
|
||||||
|
material: string;
|
||||||
|
amount: number;
|
||||||
|
price: number;
|
||||||
|
seller_name: string;
|
||||||
|
server_ip: string;
|
||||||
|
display_name: string | null;
|
||||||
|
lore: string | null;
|
||||||
|
enchants: string | null;
|
||||||
|
item_data: {
|
||||||
|
slot: number;
|
||||||
|
material: string;
|
||||||
|
amount: number;
|
||||||
|
};
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SellItemResponse {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuyItemResponse {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerInventoryResponse {
|
||||||
|
status: string;
|
||||||
|
request_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerInventory {
|
||||||
|
status: string;
|
||||||
|
result: {
|
||||||
|
player_name: string;
|
||||||
|
server_ip: string;
|
||||||
|
inventory_data: PlayerInventoryItem[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerInventoryItem {
|
||||||
|
slot: number;
|
||||||
|
material: string;
|
||||||
|
amount: number;
|
||||||
|
enchants: {
|
||||||
|
[key: string]: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketplaceOperation {
|
||||||
|
id: string;
|
||||||
|
type: 'sell' | 'buy';
|
||||||
|
player_name: string;
|
||||||
|
slot_index?: number;
|
||||||
|
amount?: number;
|
||||||
|
price: number;
|
||||||
|
server_ip: string;
|
||||||
|
status: 'pending' | 'completed' | 'failed';
|
||||||
|
item_id?: string;
|
||||||
|
error?: string;
|
||||||
|
created_at: string;
|
||||||
|
item_data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OperationsResponse {
|
||||||
|
operations: MarketplaceOperation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterUserResponse {
|
||||||
|
status: string;
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GenerateVerificationCodeResponse {
|
||||||
|
status: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerificationStatusResponse {
|
||||||
|
is_verified: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVerificationStatus(
|
||||||
|
username: string,
|
||||||
|
): Promise<VerificationStatusResponse> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/auth/verification_status/${username}`,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось получить статус верификации');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateVerificationCode(
|
||||||
|
username: string,
|
||||||
|
): Promise<GenerateVerificationCodeResponse> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/auth/generate_code?username=${username}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось сгенерировать код верификации');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
export async function registerUser(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<RegisterUserResponse> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/auth/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось зарегистрировать пользователя');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPlayerInventory(
|
||||||
|
request_id: string,
|
||||||
|
): Promise<PlayerInventory> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/api/server/inventory/${request_id}`,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось получить инвентарь игрока');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function RequestPlayerInventory(
|
||||||
|
server_ip: string,
|
||||||
|
player_name: string,
|
||||||
|
): Promise<PlayerInventoryResponse> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/server/inventory`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
server_ip: server_ip,
|
||||||
|
player_name: player_name,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось получить инвентарь игрока');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buyItem(
|
||||||
|
buyer_username: string,
|
||||||
|
item_id: string,
|
||||||
|
): Promise<{ status: string; operation_id: string; message: string }> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/api/marketplace/items/buy/${item_id}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: buyer_username,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(
|
||||||
|
errorData.message || errorData.detail || 'Не удалось купить предмет',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirmMarketplaceOperation(
|
||||||
|
operation_id: string,
|
||||||
|
status: string = 'success',
|
||||||
|
error?: string,
|
||||||
|
): Promise<{ status: string }> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/api/marketplace/operations/confirm`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
operation_id,
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось подтвердить операцию');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitItemDetails(
|
||||||
|
operation_id: string,
|
||||||
|
item_data: any,
|
||||||
|
): Promise<{ status: string }> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/api/marketplace/items/details`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
operation_id,
|
||||||
|
item_data,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось отправить данные предмета');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sellItem(
|
||||||
|
username: string,
|
||||||
|
slot_index: number,
|
||||||
|
amount: number,
|
||||||
|
price: number,
|
||||||
|
server_ip: string,
|
||||||
|
): Promise<{ status: string; operation_id: string }> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/marketplace/items/sell`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
slot_index,
|
||||||
|
amount,
|
||||||
|
price,
|
||||||
|
server_ip,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(
|
||||||
|
errorData.message ||
|
||||||
|
errorData.detail ||
|
||||||
|
'Не удалось выставить предмет на продажу',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMarketplaceItem(
|
||||||
|
item_id: string,
|
||||||
|
): Promise<MarketplaceItemResponse> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/api/marketplace/items/${item_id}`,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось получить рынок');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMarketplace(
|
||||||
|
server_ip: string,
|
||||||
|
page: number,
|
||||||
|
limit: number,
|
||||||
|
): Promise<MarketplaceResponse> {
|
||||||
|
// Создаем URL с параметрами запроса
|
||||||
|
const url = new URL(`${API_BASE_URL}/api/marketplace/items`);
|
||||||
|
url.searchParams.append('server_ip', server_ip);
|
||||||
|
url.searchParams.append('page', page.toString());
|
||||||
|
url.searchParams.append('limit', limit.toString());
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось получить предметы рынка');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исправьте тип возвращаемого значения
|
||||||
|
export async function fetchActiveServers(): Promise<Server[]> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/pranks/servers`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось получить активные сервера');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchOnlinePlayers(
|
||||||
|
server_id: string,
|
||||||
|
): Promise<OnlinePlayersResponse> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/api/pranks/servers/${server_id}/players`,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось получить онлайн игроков');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение информации о игроке
|
||||||
|
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 fetchCapes(username: string): Promise<CapesResponse> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/store/user/${username}/capes`,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
return []; // Если плащи не найдены, возвращаем пустой массив
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API ошибка:', error);
|
||||||
|
return []; // В случае ошибки возвращаем пустой массив
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function purchaseCape(
|
||||||
|
username: string,
|
||||||
|
cape_id: string,
|
||||||
|
): Promise<void> {
|
||||||
|
// Создаем URL с query-параметрами
|
||||||
|
const url = new URL(`${API_BASE_URL}/store/purchase/cape`);
|
||||||
|
url.searchParams.append('username', username);
|
||||||
|
url.searchParams.append('cape_id', cape_id);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
// Не нужно отправлять тело запроса
|
||||||
|
// body: JSON.stringify({
|
||||||
|
// username: username,
|
||||||
|
// cape_id: cape_id,
|
||||||
|
// }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(
|
||||||
|
errorData.message ||
|
||||||
|
errorData.detail?.toString() ||
|
||||||
|
'Не удалось купить плащ',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCapesStore(): Promise<StoreCape[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/store/capes`);
|
||||||
|
if (!response.ok) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API ошибка:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function activateCape(
|
||||||
|
username: string,
|
||||||
|
cape_id: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/store/user/${username}/capes/activate/${cape_id}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
cape_id: cape_id,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось активировать плащ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deactivateCape(
|
||||||
|
username: string,
|
||||||
|
cape_id: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/store/user/${username}/capes/deactivate/${cape_id}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
cape_id: cape_id,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new 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 || 'Не удалось загрузить плащ');
|
||||||
|
}
|
||||||
|
}
|
127
src/renderer/components/CapeCard.tsx
Normal file
127
src/renderer/components/CapeCard.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
// src/renderer/components/CapeCard.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardMedia,
|
||||||
|
CardContent,
|
||||||
|
Typography,
|
||||||
|
CardActions,
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
Box,
|
||||||
|
Chip,
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
// Тип для плаща с необязательными полями для обоих вариантов использования
|
||||||
|
export interface CapeCardProps {
|
||||||
|
cape: {
|
||||||
|
cape_id?: string;
|
||||||
|
id?: string;
|
||||||
|
cape_name?: string;
|
||||||
|
name?: string;
|
||||||
|
cape_description?: string;
|
||||||
|
description?: string;
|
||||||
|
image_url: string;
|
||||||
|
is_active?: boolean;
|
||||||
|
price?: number;
|
||||||
|
purchased_at?: string;
|
||||||
|
};
|
||||||
|
mode: 'profile' | 'shop';
|
||||||
|
onAction: (capeId: string) => void;
|
||||||
|
actionDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CapeCard({
|
||||||
|
cape,
|
||||||
|
mode,
|
||||||
|
onAction,
|
||||||
|
actionDisabled = false,
|
||||||
|
}: CapeCardProps) {
|
||||||
|
// Определяем текст и цвет кнопки в зависимости от режима
|
||||||
|
const getActionButton = () => {
|
||||||
|
if (mode === 'shop') {
|
||||||
|
return {
|
||||||
|
text: 'Купить',
|
||||||
|
color: 'primary',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Профиль
|
||||||
|
return cape.is_active
|
||||||
|
? { text: 'Снять', color: 'error' }
|
||||||
|
: { text: 'Надеть', color: 'success' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionButton = getActionButton();
|
||||||
|
|
||||||
|
// В функции компонента добавьте нормализацию данных
|
||||||
|
const capeId = cape.cape_id || cape.id || '';
|
||||||
|
const capeName = cape.cape_name || cape.name || '';
|
||||||
|
const capeDescription = cape.cape_description || cape.description || '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip arrow title={capeDescription}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
width: 200,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative', // для позиционирования ценника
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Ценник для магазина */}
|
||||||
|
{mode === 'shop' && cape.price !== undefined && (
|
||||||
|
<Chip
|
||||||
|
label={`${cape.price} коинов`}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
zIndex: 2,
|
||||||
|
bgcolor: 'rgba(0, 0, 0, 0.7)',
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
image={cape.image_url}
|
||||||
|
alt={capeName}
|
||||||
|
sx={{
|
||||||
|
display: 'block',
|
||||||
|
width: '100%',
|
||||||
|
transform: 'scale(2.9) translateX(66px) translateY(32px)',
|
||||||
|
imageRendering: 'pixelated',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CardContent sx={{ bgcolor: 'rgba(255, 255, 255, 0.05)', pt: '9vh' }}>
|
||||||
|
<Typography sx={{ color: 'white' }}>{capeName}</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<CardActions sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color={actionButton.color as 'primary' | 'success' | 'error'}
|
||||||
|
onClick={() => onAction(capeId)}
|
||||||
|
disabled={actionDisabled}
|
||||||
|
sx={{
|
||||||
|
borderRadius: '20px',
|
||||||
|
p: '5px 25px',
|
||||||
|
color: 'white',
|
||||||
|
backgroundColor: 'rgb(0, 134, 0)',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(0, 134, 0, 0.5)',
|
||||||
|
},
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{actionButton.text}
|
||||||
|
</Button>
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
@ -11,23 +11,78 @@ interface AuthFormProps {
|
|||||||
|
|
||||||
const AuthForm = ({ config, handleInputChange, onLogin }: AuthFormProps) => {
|
const AuthForm = ({ config, handleInputChange, onLogin }: AuthFormProps) => {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1.5vw' }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '1.5vw',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6">Логин</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
name="username"
|
name="username"
|
||||||
label="Введите ник"
|
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={config.username}
|
value={config.username}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
// '& .MuiFormLabel-root': {
|
||||||
|
// color: 'white',
|
||||||
|
// },
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
'& .MuiInput-underline:after': {
|
||||||
|
borderBottomColor: '#B2BAC2',
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
'& fieldset': {
|
||||||
|
borderColor: '#E0E3E7',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
'&:hover fieldset': {
|
||||||
|
borderColor: '#B2BAC2',
|
||||||
|
},
|
||||||
|
'&.Mui-focused fieldset': {
|
||||||
|
borderColor: '#6F7E8C',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Typography variant="h6">Пароль</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
label="Введите пароль"
|
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={config.password}
|
value={config.password}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
// '& .MuiFormLabel-root': {
|
||||||
|
// color: 'white',
|
||||||
|
// },
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
'& .MuiInput-underline:after': {
|
||||||
|
borderBottomColor: '#B2BAC2',
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
'& fieldset': {
|
||||||
|
borderColor: '#E0E3E7',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
'&:hover fieldset': {
|
||||||
|
borderColor: '#B2BAC2',
|
||||||
|
},
|
||||||
|
'&.Mui-focused fieldset': {
|
||||||
|
borderColor: '#6F7E8C',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button onClick={onLogin} variant="contained">
|
<Button onClick={onLogin} variant="contained">
|
||||||
Войти
|
Войти
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import heart from '../../../assets/images/heart.svg';
|
import heart from '../../../assets/images/heart.svg';
|
||||||
|
|
||||||
export default function MinecraftBackround() {
|
export default function MinecraftBackground() {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
355
src/renderer/components/PlayerInventory.tsx
Normal file
355
src/renderer/components/PlayerInventory.tsx
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
// src/renderer/components/PlayerInventory.tsx
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardMedia,
|
||||||
|
CardContent,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
TextField,
|
||||||
|
Alert,
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
RequestPlayerInventory,
|
||||||
|
getPlayerInventory,
|
||||||
|
sellItem,
|
||||||
|
PlayerInventoryItem,
|
||||||
|
} from '../api';
|
||||||
|
|
||||||
|
interface PlayerInventoryProps {
|
||||||
|
username: string;
|
||||||
|
serverIp: string;
|
||||||
|
onSellSuccess?: () => void; // Callback для обновления маркетплейса после продажи
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PlayerInventory({
|
||||||
|
username,
|
||||||
|
serverIp,
|
||||||
|
onSellSuccess,
|
||||||
|
}: PlayerInventoryProps) {
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [inventoryItems, setInventoryItems] = useState<PlayerInventoryItem[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [sellDialogOpen, setSellDialogOpen] = useState<boolean>(false);
|
||||||
|
const [selectedItem, setSelectedItem] = useState<PlayerInventoryItem | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [price, setPrice] = useState<number>(0);
|
||||||
|
const [amount, setAmount] = useState<number>(1);
|
||||||
|
const [sellLoading, setSellLoading] = useState<boolean>(false);
|
||||||
|
const [sellError, setSellError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Функция для запроса инвентаря игрока
|
||||||
|
const fetchPlayerInventory = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Сначала делаем запрос на получение идентификатора запроса инвентаря
|
||||||
|
const inventoryRequest = await RequestPlayerInventory(serverIp, username);
|
||||||
|
const requestId = inventoryRequest.request_id;
|
||||||
|
|
||||||
|
// Затем начинаем опрашивать API для получения результата
|
||||||
|
let inventoryData = null;
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 10; // Максимальное количество попыток
|
||||||
|
|
||||||
|
while (!inventoryData && attempts < maxAttempts) {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Пауза перед следующим запросом
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// Запрашиваем состояние инвентаря
|
||||||
|
const response = await getPlayerInventory(requestId);
|
||||||
|
|
||||||
|
// Если инвентарь загружен, сохраняем его
|
||||||
|
if (response.status === 'completed') {
|
||||||
|
inventoryData = response.result.inventory_data;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Ожидание завершения запроса инвентаря...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inventoryData) {
|
||||||
|
setInventoryItems(inventoryData);
|
||||||
|
} else {
|
||||||
|
setError('Не удалось получить инвентарь. Попробуйте еще раз.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка при получении инвентаря:', e);
|
||||||
|
setError('Произошла ошибка при загрузке инвентаря.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Открываем диалог для продажи предмета
|
||||||
|
const handleOpenSellDialog = (item: PlayerInventoryItem) => {
|
||||||
|
setSelectedItem(item);
|
||||||
|
setAmount(1);
|
||||||
|
setPrice(0);
|
||||||
|
setSellError(null);
|
||||||
|
setSellDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Закрываем диалог
|
||||||
|
const handleCloseSellDialog = () => {
|
||||||
|
setSellDialogOpen(false);
|
||||||
|
setSelectedItem(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Выставляем предмет на продажу
|
||||||
|
const handleSellItem = async () => {
|
||||||
|
if (!selectedItem) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSellLoading(true);
|
||||||
|
setSellError(null);
|
||||||
|
|
||||||
|
// Проверяем валидность введенных данных
|
||||||
|
if (price <= 0) {
|
||||||
|
setSellError('Цена должна быть больше 0');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount <= 0 || amount > selectedItem.amount) {
|
||||||
|
setSellError(`Количество должно быть от 1 до ${selectedItem.amount}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем запрос на продажу
|
||||||
|
const result = await sellItem(
|
||||||
|
username,
|
||||||
|
selectedItem.slot,
|
||||||
|
amount,
|
||||||
|
price,
|
||||||
|
serverIp,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Проверяем статус операции
|
||||||
|
if (result.status === 'pending') {
|
||||||
|
// Закрываем диалог и обновляем инвентарь
|
||||||
|
handleCloseSellDialog();
|
||||||
|
|
||||||
|
// Показываем уведомление о том, что операция обрабатывается
|
||||||
|
// setNotification({ // Assuming setNotification is available in the context
|
||||||
|
// open: true,
|
||||||
|
// message: 'Предмет выставляется на продажу. Это может занять некоторое время.',
|
||||||
|
// type: 'info'
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Через 5 секунд обновляем инвентарь
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchPlayerInventory();
|
||||||
|
|
||||||
|
// Вызываем callback для обновления маркетплейса
|
||||||
|
if (onSellSuccess) {
|
||||||
|
onSellSuccess();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка при продаже предмета:', e);
|
||||||
|
setSellError('Произошла ошибка при продаже предмета.');
|
||||||
|
} finally {
|
||||||
|
setSellLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Загружаем инвентарь при монтировании компонента
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPlayerInventory();
|
||||||
|
}, [username, serverIp]);
|
||||||
|
|
||||||
|
// Получаем отображаемое имя предмета
|
||||||
|
const getItemDisplayName = (material: string) => {
|
||||||
|
return material
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\b\w/g, (l) => l.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 4 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h5" color="white">
|
||||||
|
Ваш инвентарь
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={fetchPlayerInventory}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Обновить
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', my: 4 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{inventoryItems.length === 0 ? (
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
color="white"
|
||||||
|
sx={{ textAlign: 'center', my: 4 }}
|
||||||
|
>
|
||||||
|
Ваш инвентарь пуст или не удалось загрузить предметы.
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{inventoryItems.map((item) =>
|
||||||
|
item.material !== 'AIR' && item.amount > 0 ? (
|
||||||
|
<Grid item xs={6} sm={4} md={3} lg={2} key={item.slot}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
'&:hover': { transform: 'scale(1.03)' },
|
||||||
|
}}
|
||||||
|
onClick={() => handleOpenSellDialog(item)}
|
||||||
|
>
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
sx={{
|
||||||
|
height: 100,
|
||||||
|
objectFit: 'contain',
|
||||||
|
bgcolor: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
p: 1,
|
||||||
|
imageRendering: 'pixelated',
|
||||||
|
}}
|
||||||
|
image={`/minecraft/${item.material.toLowerCase()}.png`}
|
||||||
|
alt={item.material}
|
||||||
|
/>
|
||||||
|
<CardContent sx={{ p: 1 }}>
|
||||||
|
<Typography variant="body2" color="white" noWrap>
|
||||||
|
{getItemDisplayName(item.material)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="white">
|
||||||
|
x{item.amount}
|
||||||
|
</Typography>
|
||||||
|
{Object.keys(item.enchants || {}).length > 0 && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="secondary"
|
||||||
|
sx={{ display: 'block' }}
|
||||||
|
>
|
||||||
|
Зачарования: {Object.keys(item.enchants).length}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
) : null,
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Диалог для продажи предмета */}
|
||||||
|
<Dialog open={sellDialogOpen} onClose={handleCloseSellDialog}>
|
||||||
|
<DialogTitle>Продать предмет</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{selectedItem && (
|
||||||
|
<Box sx={{ mt: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
sx={{
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
objectFit: 'contain',
|
||||||
|
mr: 2,
|
||||||
|
}}
|
||||||
|
image={`/items/${selectedItem.material.toLowerCase()}.png`}
|
||||||
|
alt={selectedItem.material}
|
||||||
|
/>
|
||||||
|
<Typography variant="h6">
|
||||||
|
{getItemDisplayName(selectedItem.material)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
Всего доступно: {selectedItem.amount}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Количество"
|
||||||
|
type="number"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
value={amount}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAmount(
|
||||||
|
Math.min(
|
||||||
|
parseInt(e.target.value) || 0,
|
||||||
|
selectedItem.amount,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
inputProps={{ min: 1, max: selectedItem.amount }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Цена (за всё)"
|
||||||
|
type="number"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
value={price}
|
||||||
|
onChange={(e) => setPrice(parseInt(e.target.value) || 0)}
|
||||||
|
inputProps={{ min: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{sellError && (
|
||||||
|
<Alert severity="error" sx={{ mt: 2 }}>
|
||||||
|
{sellError}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleCloseSellDialog}>Отмена</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSellItem}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={sellLoading}
|
||||||
|
>
|
||||||
|
{sellLoading ? <CircularProgress size={24} /> : 'Продать'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -104,10 +104,7 @@ const ServerStatus = ({
|
|||||||
?
|
?
|
||||||
</Avatar>
|
</Avatar>
|
||||||
)}
|
)}
|
||||||
|
{serverStatus.error ? (
|
||||||
{serverStatus.loading ? (
|
|
||||||
<CircularProgress size={20} />
|
|
||||||
) : serverStatus.error ? (
|
|
||||||
<Typography color="error">Ошибка загрузки</Typography>
|
<Typography color="error">Ошибка загрузки</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Typography sx={{ fontWeight: 'bold' }}>
|
<Typography sx={{ fontWeight: 'bold' }}>
|
||||||
|
92
src/renderer/components/Settings/SettingsModal.tsx
Normal file
92
src/renderer/components/Settings/SettingsModal.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { Box, Typography, Button, Modal } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
import MemorySlider from '../Login/MemorySlider';
|
||||||
|
import FilesSelector from '../FilesSelector';
|
||||||
|
|
||||||
|
interface SettingsModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
config: {
|
||||||
|
memory: number;
|
||||||
|
preserveFiles: string[];
|
||||||
|
};
|
||||||
|
onConfigChange: (newConfig: {
|
||||||
|
memory: number;
|
||||||
|
preserveFiles: string[];
|
||||||
|
}) => void;
|
||||||
|
packName: string;
|
||||||
|
onSave: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsModal = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
config,
|
||||||
|
onConfigChange,
|
||||||
|
packName,
|
||||||
|
onSave,
|
||||||
|
}: SettingsModalProps) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
aria-labelledby="modal-modal-title"
|
||||||
|
aria-describedby="modal-modal-description"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 400,
|
||||||
|
background:
|
||||||
|
'linear-gradient(-242.94deg, #000000 39.07%, #3b4187 184.73%)',
|
||||||
|
border: '2px solid #000',
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4,
|
||||||
|
borderRadius: '3vw',
|
||||||
|
gap: '1vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography id="modal-modal-title" variant="body1" component="h2">
|
||||||
|
Файлы и папки, которые будут сохранены после переустановки сборки
|
||||||
|
</Typography>
|
||||||
|
<FilesSelector
|
||||||
|
packName={packName}
|
||||||
|
initialSelected={config.preserveFiles}
|
||||||
|
onSelectionChange={(selected) => {
|
||||||
|
onConfigChange({ ...config, preserveFiles: selected });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="body1" sx={{ color: 'white' }}>
|
||||||
|
Оперативная память выделенная для Minecraft
|
||||||
|
</Typography>
|
||||||
|
<MemorySlider
|
||||||
|
memory={config.memory}
|
||||||
|
onChange={(e, value) => {
|
||||||
|
onConfigChange({ ...config, memory: value as number });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
onClick={() => {
|
||||||
|
onSave();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
borderRadius: '3vw',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsModal;
|
73
src/renderer/components/SkinViewer.tsx
Normal file
73
src/renderer/components/SkinViewer.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
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,
|
||||||
|
model: 'auto-detect',
|
||||||
|
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,7 +1,10 @@
|
|||||||
import { Box, Button, Typography } from '@mui/material';
|
import { Box, Button, Tab, Tabs, Typography } from '@mui/material';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||||
import MinimizeIcon from '@mui/icons-material/Minimize';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useLocation } from 'react-router-dom';
|
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Tooltip } from '@mui/material';
|
||||||
|
import { fetchCoins } from '../api';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -18,12 +21,86 @@ declare global {
|
|||||||
// Определяем пропсы
|
// Определяем пропсы
|
||||||
interface TopBarProps {
|
interface TopBarProps {
|
||||||
onRegister?: () => void; // Опционально, если нужен обработчик регистрации
|
onRegister?: () => void; // Опционально, если нужен обработчик регистрации
|
||||||
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TopBar({ onRegister }: TopBarProps) {
|
export default function TopBar({ onRegister, username }: TopBarProps) {
|
||||||
// Получаем текущий путь
|
// Получаем текущий путь
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isLoginPage = location.pathname === '/login';
|
const isLoginPage = location.pathname === '/login';
|
||||||
|
const isLaunchPage = location.pathname.startsWith('/launch');
|
||||||
|
const isVersionsExplorerPage = location.pathname.startsWith('/');
|
||||||
|
const isRegistrationPage = location.pathname === '/registration';
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [coins, setCoins] = useState<number>(0);
|
||||||
|
const [value, setValue] = useState(0);
|
||||||
|
const [activePage, setActivePage] = useState('versions');
|
||||||
|
|
||||||
|
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');
|
||||||
|
} else if (newValue === 3) {
|
||||||
|
navigate('/marketplace');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLaunchPage = () => {
|
||||||
|
navigate('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPageTitle = () => {
|
||||||
|
if (isLoginPage) {
|
||||||
|
return 'Вход';
|
||||||
|
}
|
||||||
|
if (isLaunchPage) {
|
||||||
|
return 'Запуск';
|
||||||
|
}
|
||||||
|
if (isVersionsExplorerPage) {
|
||||||
|
if (activePage === 'versions') {
|
||||||
|
return 'Версии';
|
||||||
|
}
|
||||||
|
if (activePage === 'profile') {
|
||||||
|
return 'Профиль';
|
||||||
|
}
|
||||||
|
if (activePage === 'shop') {
|
||||||
|
return 'Магазин';
|
||||||
|
}
|
||||||
|
if (activePage === 'marketplace') {
|
||||||
|
return 'Рынок';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'Неизвестная страница';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для получения количества монет
|
||||||
|
const fetchCoinsData = async () => {
|
||||||
|
if (!username) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const coinsData = await fetchCoins(username);
|
||||||
|
setCoins(coinsData.coins);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении количества монет:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (username) {
|
||||||
|
fetchCoinsData();
|
||||||
|
|
||||||
|
// Создаем интервалы для периодического обновления данных
|
||||||
|
const coinsInterval = setInterval(fetchCoinsData, 60000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(coinsInterval);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [username]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -33,30 +110,216 @@ export default function TopBar({ onRegister }: TopBarProps) {
|
|||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: '50px',
|
height: '7vh',
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
WebkitAppRegion: 'drag',
|
WebkitAppRegion: 'drag',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
justifyContent: 'flex-end', // Всё содержимое справа
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
// marginLeft: '1em',
|
||||||
|
// marginRight: '1em',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Правая часть со всеми кнопками */}
|
{/* Левая часть */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
WebkitAppRegion: 'no-drag',
|
WebkitAppRegion: 'no-drag',
|
||||||
gap: '2vw',
|
gap: '2vw',
|
||||||
padding: '1em',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
marginLeft: '1vw',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(isLaunchPage || isRegistrationPage) && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => handleLaunchPage()}
|
||||||
|
sx={{
|
||||||
|
width: '3em',
|
||||||
|
height: '3em',
|
||||||
|
borderRadius: '50%',
|
||||||
|
border: 'unset',
|
||||||
|
color: 'white',
|
||||||
|
minWidth: 'unset',
|
||||||
|
minHeight: 'unset',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowBackRoundedIcon />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!isLaunchPage && !isRegistrationPage && !isLoginPage && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
'& .MuiTabs-indicator': {
|
||||||
|
backgroundColor: 'rgba(255, 77, 77, 1)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
aria-label="basic tabs example"
|
||||||
|
disableRipple={true}
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
label="Версии"
|
||||||
|
disableRipple={true}
|
||||||
|
onClick={() => {
|
||||||
|
setActivePage('versions');
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
color: 'white',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
fontSize: '0.7em',
|
||||||
|
'&.Mui-selected': {
|
||||||
|
color: 'rgba(255, 77, 77, 1)',
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
color: 'rgb(199, 199, 199)',
|
||||||
|
},
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
label="Профиль"
|
||||||
|
disableRipple={true}
|
||||||
|
onClick={() => {
|
||||||
|
setActivePage('profile');
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
color: 'white',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
fontSize: '0.7em',
|
||||||
|
'&.Mui-selected': {
|
||||||
|
color: 'rgba(255, 77, 77, 1)',
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
color: 'rgb(199, 199, 199)',
|
||||||
|
},
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
label="Магазин"
|
||||||
|
disableRipple={true}
|
||||||
|
onClick={() => {
|
||||||
|
setActivePage('shop');
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
color: 'white',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
fontSize: '0.7em',
|
||||||
|
'&.Mui-selected': {
|
||||||
|
color: 'rgba(255, 77, 77, 1)',
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
color: 'rgb(199, 199, 199)',
|
||||||
|
},
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
label="Рынок"
|
||||||
|
disableRipple={true}
|
||||||
|
onClick={() => {
|
||||||
|
setActivePage('marketplace');
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
color: 'white',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
fontSize: '0.7em',
|
||||||
|
'&.Mui-selected': {
|
||||||
|
color: 'rgba(255, 77, 77, 1)',
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
color: 'rgb(199, 199, 199)',
|
||||||
|
},
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{/* Центр */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexGrow: 1,
|
||||||
|
WebkitAppRegion: 'drag',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{ color: 'white', fontFamily: 'Benzin-Bold' }}
|
||||||
|
>
|
||||||
|
{getPageTitle()}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{/* Правая часть со всеми кнопками */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
WebkitAppRegion: 'no-drag',
|
||||||
|
gap: '1vw',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: '1vw',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Кнопка регистрации, если на странице логина */}
|
{/* Кнопка регистрации, если на странице логина */}
|
||||||
|
{username && (
|
||||||
|
<Tooltip
|
||||||
|
title="Попы — внутриигровая валюта, начисляемая за время игры на серверах."
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '6px 12px',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: '#2bff00ff' }}>P</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '16px',
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{coins}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{isLoginPage && (
|
{isLoginPage && (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => onRegister && onRegister()}
|
onClick={() => navigate('/registration')}
|
||||||
sx={{
|
sx={{
|
||||||
width: '10em',
|
width: '10em',
|
||||||
height: '3em',
|
height: '3em',
|
||||||
@ -87,7 +350,17 @@ export default function TopBar({ onRegister }: TopBarProps) {
|
|||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MinimizeIcon sx={{ color: 'white' }} />
|
<svg
|
||||||
|
className="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M 7 19 h 10 c 0.55 0 1 0.45 1 1 s -0.45 1 -1 1 H 7 c -0.55 0 -1 -0.45 -1 -1 s 0.45 -1 1 -1"
|
||||||
|
fill="white"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -101,7 +374,7 @@ export default function TopBar({ onRegister }: TopBarProps) {
|
|||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CloseIcon sx={{ color: 'white' }} />
|
<CloseRoundedIcon sx={{ color: 'white' }} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -5,16 +5,14 @@ import {
|
|||||||
Snackbar,
|
Snackbar,
|
||||||
Alert,
|
Alert,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
Modal,
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import ServerStatus from '../components/ServerStatus/ServerStatus';
|
import ServerStatus from '../components/ServerStatus/ServerStatus';
|
||||||
import PopaPopa from '../components/popa-popa';
|
import PopaPopa from '../components/popa-popa';
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MemorySlider from '../components/Login/MemorySlider';
|
import SettingsModal from '../components/Settings/SettingsModal';
|
||||||
import FilesSelector from '../components/FilesSelector';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -30,7 +28,9 @@ declare global {
|
|||||||
|
|
||||||
// Определяем тип для props
|
// Определяем тип для props
|
||||||
interface LaunchPageProps {
|
interface LaunchPageProps {
|
||||||
launchOptions: {
|
onLaunchPage?: () => void;
|
||||||
|
launchOptions?: {
|
||||||
|
// Делаем опциональным
|
||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
apiReleaseUrl: string;
|
apiReleaseUrl: string;
|
||||||
versionFileName: string;
|
versionFileName: string;
|
||||||
@ -42,8 +42,14 @@ interface LaunchPageProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
|
const LaunchPage = ({
|
||||||
|
onLaunchPage,
|
||||||
|
launchOptions = {} as any,
|
||||||
|
}: LaunchPageProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { versionId } = useParams();
|
||||||
|
const [versionConfig, setVersionConfig] = useState<any>(null);
|
||||||
|
|
||||||
// Начальное состояние должно быть пустым или с минимальными значениями
|
// Начальное состояние должно быть пустым или с минимальными значениями
|
||||||
const [config, setConfig] = useState<{
|
const [config, setConfig] = useState<{
|
||||||
memory: number;
|
memory: number;
|
||||||
@ -101,27 +107,82 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
|
|||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Загрузка конфигурации сборки при монтировании
|
const fetchVersionConfig = async () => {
|
||||||
const loadPackConfig = async () => {
|
if (!versionId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Сначала проверяем, есть ли конфигурация в localStorage
|
||||||
|
const savedConfig = localStorage.getItem('selected_version_config');
|
||||||
|
if (savedConfig) {
|
||||||
|
const parsedConfig = JSON.parse(savedConfig);
|
||||||
|
setVersionConfig(parsedConfig);
|
||||||
|
|
||||||
|
// Устанавливаем значения памяти и preserveFiles из конфигурации
|
||||||
|
setConfig({
|
||||||
|
memory: parsedConfig.memory || 4096,
|
||||||
|
preserveFiles: parsedConfig.preserveFiles || [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Очищаем localStorage
|
||||||
|
localStorage.removeItem('selected_version_config');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если нет в localStorage, запрашиваем с сервера
|
||||||
const result = await window.electron.ipcRenderer.invoke(
|
const result = await window.electron.ipcRenderer.invoke(
|
||||||
'load-pack-config',
|
'get-version-config',
|
||||||
{
|
{ versionId },
|
||||||
packName: launchOptions.packName,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.success && result.config) {
|
if (result.success) {
|
||||||
// Полностью заменяем config значениями из файла
|
setVersionConfig(result.config);
|
||||||
setConfig(result.config);
|
setConfig({
|
||||||
|
memory: result.config.memory || 4096,
|
||||||
|
preserveFiles: result.config.preserveFiles || [],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Если не удалось получить конфигурацию, используем значения по умолчанию
|
||||||
|
const defaultConfig = {
|
||||||
|
downloadUrl: '',
|
||||||
|
apiReleaseUrl: '',
|
||||||
|
versionFileName: `${versionId}_version.txt`,
|
||||||
|
packName: versionId || 'Comfort',
|
||||||
|
memory: 4096,
|
||||||
|
baseVersion: '1.21.4',
|
||||||
|
serverIp: 'popa-popa.ru',
|
||||||
|
fabricVersion: '0.16.14',
|
||||||
|
preserveFiles: ['popa-launcher-config.json'],
|
||||||
|
};
|
||||||
|
setVersionConfig(defaultConfig);
|
||||||
|
setConfig({
|
||||||
|
memory: defaultConfig.memory,
|
||||||
|
preserveFiles: defaultConfig.preserveFiles || [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при загрузке настроек:', error);
|
console.error('Ошибка при получении настроек версии:', error);
|
||||||
|
// Используем значения по умолчанию
|
||||||
|
const defaultConfig = {
|
||||||
|
downloadUrl: '',
|
||||||
|
apiReleaseUrl: '',
|
||||||
|
versionFileName: `${versionId}_version.txt`,
|
||||||
|
packName: versionId || 'Comfort',
|
||||||
|
memory: 4096,
|
||||||
|
baseVersion: '1.21.4',
|
||||||
|
serverIp: 'popa-popa.ru',
|
||||||
|
fabricVersion: '0.16.14',
|
||||||
|
preserveFiles: ['popa-launcher-config.json'],
|
||||||
|
};
|
||||||
|
setVersionConfig(defaultConfig);
|
||||||
|
setConfig({
|
||||||
|
memory: defaultConfig.memory,
|
||||||
|
preserveFiles: defaultConfig.preserveFiles || [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadPackConfig();
|
fetchVersionConfig();
|
||||||
}, [launchOptions.packName]);
|
}, [versionId]);
|
||||||
|
|
||||||
const showNotification = (
|
const showNotification = (
|
||||||
message: string,
|
message: string,
|
||||||
@ -134,94 +195,86 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
|
|||||||
setNotification({ ...notification, open: false });
|
setNotification({ ...notification, open: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Функция для запуска игры с настройками выбранной версии
|
||||||
const handleLaunchMinecraft = async () => {
|
const handleLaunchMinecraft = async () => {
|
||||||
try {
|
try {
|
||||||
setIsDownloading(true);
|
setIsDownloading(true);
|
||||||
setDownloadProgress(0);
|
setDownloadProgress(0);
|
||||||
setBuffer(10);
|
setBuffer(10);
|
||||||
|
|
||||||
// Загружаем настройки сборки
|
// Используем настройки выбранной версии или дефолтные
|
||||||
const result = await window.electron.ipcRenderer.invoke(
|
const currentConfig = versionConfig || {
|
||||||
'load-pack-config',
|
packName: versionId || 'Comfort',
|
||||||
{
|
memory: 4096,
|
||||||
packName: launchOptions.packName,
|
baseVersion: '1.21.4',
|
||||||
},
|
serverIp: 'popa-popa.ru',
|
||||||
);
|
fabricVersion: '0.16.14',
|
||||||
|
preserveFiles: [],
|
||||||
|
};
|
||||||
|
|
||||||
// Используйте уже существующий state вместо локальной переменной
|
// Проверяем, является ли это ванильной версией
|
||||||
if (result.success && result.config) {
|
const isVanillaVersion =
|
||||||
setConfig(result.config); // Обновляем state
|
!currentConfig.downloadUrl || currentConfig.downloadUrl === '';
|
||||||
|
|
||||||
|
if (!isVanillaVersion) {
|
||||||
|
// Если это не ванильная версия, выполняем загрузку и распаковку
|
||||||
|
const packOptions = {
|
||||||
|
downloadUrl: currentConfig.downloadUrl,
|
||||||
|
apiReleaseUrl: currentConfig.apiReleaseUrl,
|
||||||
|
versionFileName: currentConfig.versionFileName,
|
||||||
|
packName: versionId || currentConfig.packName,
|
||||||
|
preserveFiles: config.preserveFiles,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Передаем опции для скачивания
|
||||||
|
const downloadResult = await window.electron.ipcRenderer.invoke(
|
||||||
|
'download-and-extract',
|
||||||
|
packOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (downloadResult?.success) {
|
||||||
|
if (downloadResult.updated) {
|
||||||
|
showNotification(
|
||||||
|
`Сборка ${downloadResult.packName} успешно обновлена до версии ${downloadResult.version}`,
|
||||||
|
'success',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showNotification(
|
||||||
|
`Установлена актуальная версия сборки ${downloadResult.packName} (${downloadResult.version})`,
|
||||||
|
'info',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showNotification('Запускаем ванильный Minecraft...', 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Опции для запуска Minecraft
|
||||||
const savedConfig = JSON.parse(
|
const savedConfig = JSON.parse(
|
||||||
localStorage.getItem('launcher_config') || '{}',
|
localStorage.getItem('launcher_config') || '{}',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Опции для скачивания сборки
|
const options = {
|
||||||
const packOptions = {
|
accessToken: savedConfig.accessToken,
|
||||||
downloadUrl: launchOptions.downloadUrl,
|
uuid: savedConfig.uuid,
|
||||||
apiReleaseUrl: launchOptions.apiReleaseUrl,
|
username: savedConfig.username,
|
||||||
versionFileName: launchOptions.versionFileName,
|
memory: config.memory,
|
||||||
packName: launchOptions.packName,
|
baseVersion: currentConfig.baseVersion,
|
||||||
preserveFiles: config.preserveFiles,
|
packName: versionId || currentConfig.packName,
|
||||||
|
serverIp: currentConfig.serverIp,
|
||||||
|
fabricVersion: currentConfig.fabricVersion,
|
||||||
|
// Для ванильной версии устанавливаем флаг
|
||||||
|
isVanillaVersion: isVanillaVersion,
|
||||||
|
versionToLaunchOverride: isVanillaVersion ? versionId : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Передаем опции для скачивания
|
const launchResult = await window.electron.ipcRenderer.invoke(
|
||||||
const downloadResult = await window.electron.ipcRenderer.invoke(
|
'launch-minecraft',
|
||||||
'download-and-extract',
|
options,
|
||||||
packOptions,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (downloadResult?.success) {
|
if (launchResult?.success) {
|
||||||
let needsSecondAttempt = false;
|
showNotification('Minecraft успешно запущен!', 'success');
|
||||||
|
|
||||||
if (downloadResult.updated) {
|
|
||||||
showNotification(
|
|
||||||
`Сборка ${downloadResult.packName} успешно обновлена до версии ${downloadResult.version}`,
|
|
||||||
'success',
|
|
||||||
);
|
|
||||||
needsSecondAttempt = true;
|
|
||||||
} else {
|
|
||||||
showNotification(
|
|
||||||
`Установлена актуальная версия сборки ${downloadResult.packName} (${downloadResult.version})`,
|
|
||||||
'info',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Опции для запуска
|
|
||||||
const options = {
|
|
||||||
accessToken: savedConfig.accessToken,
|
|
||||||
uuid: savedConfig.uuid,
|
|
||||||
username: savedConfig.username,
|
|
||||||
memory: config.memory, // Используем state
|
|
||||||
baseVersion: launchOptions.baseVersion,
|
|
||||||
packName: launchOptions.packName,
|
|
||||||
serverIp: launchOptions.serverIp,
|
|
||||||
fabricVersion: launchOptions.fabricVersion,
|
|
||||||
};
|
|
||||||
|
|
||||||
const launchResult = await window.electron.ipcRenderer.invoke(
|
|
||||||
'launch-minecraft',
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (needsSecondAttempt) {
|
|
||||||
showNotification(
|
|
||||||
'Завершаем настройку компонентов, повторный запуск...',
|
|
||||||
'info',
|
|
||||||
);
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
const secondAttempt = await window.electron.ipcRenderer.invoke(
|
|
||||||
'launch-minecraft',
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
showNotification('Minecraft успешно запущен!', 'success');
|
|
||||||
} else if (launchResult?.success) {
|
|
||||||
showNotification('Minecraft успешно запущен!', 'success');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
@ -240,7 +293,7 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await window.electron.ipcRenderer.invoke('save-pack-config', {
|
await window.electron.ipcRenderer.invoke('save-pack-config', {
|
||||||
packName: launchOptions.packName,
|
packName: versionId || versionConfig?.packName || 'Comfort',
|
||||||
config: configToSave,
|
config: configToSave,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -267,7 +320,7 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
|
|||||||
СЕРВЕР ГДЕ ВСЕМ НА ВАС ПОХУЙ
|
СЕРВЕР ГДЕ ВСЕМ НА ВАС ПОХУЙ
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||||||
СЕРВЕР ГДЕ РАЗРЕШИНЫ ОДНОПОЛЫЕ БРАКИ
|
СЕРВЕР ГДЕ РАЗРЕШЕНЫ ОДНОПОЛЫЕ БРАКИ
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||||||
СЕРВЕР ГДЕ ВСЕ ДОЛБАЕБЫ
|
СЕРВЕР ГДЕ ВСЕ ДОЛБАЕБЫ
|
||||||
@ -285,7 +338,7 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
|
|||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<ServerStatus
|
<ServerStatus
|
||||||
serverIp={launchOptions.serverIp}
|
serverIp={versionConfig?.serverIp || 'popa-popa.ru'}
|
||||||
refreshInterval={30000}
|
refreshInterval={30000}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@ -365,65 +418,14 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
|
|
||||||
<Modal
|
<SettingsModal
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
aria-labelledby="modal-modal-title"
|
config={config}
|
||||||
aria-describedby="modal-modal-description"
|
onConfigChange={setConfig}
|
||||||
>
|
packName={versionId || versionConfig?.packName || 'Comfort'}
|
||||||
<Box
|
onSave={savePackConfig}
|
||||||
sx={{
|
/>
|
||||||
position: 'absolute',
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
width: 400,
|
|
||||||
background:
|
|
||||||
'linear-gradient(-242.94deg, #000000 39.07%, #3b4187 184.73%)',
|
|
||||||
border: '2px solid #000',
|
|
||||||
boxShadow: 24,
|
|
||||||
p: 4,
|
|
||||||
borderRadius: '3vw',
|
|
||||||
gap: '1vh',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography id="modal-modal-title" variant="body1" component="h2">
|
|
||||||
Файлы и папки, которые будут сохранены после переустановки сборки
|
|
||||||
</Typography>
|
|
||||||
<FilesSelector
|
|
||||||
packName={launchOptions.packName}
|
|
||||||
initialSelected={config.preserveFiles} // Передаем текущие выбранные файлы
|
|
||||||
onSelectionChange={(selected) => {
|
|
||||||
setConfig((prev) => ({ ...prev, preserveFiles: selected }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography variant="body1" sx={{ color: 'white' }}>
|
|
||||||
Оперативная память выделенная для Minecraft
|
|
||||||
</Typography>
|
|
||||||
<MemorySlider
|
|
||||||
memory={config.memory}
|
|
||||||
onChange={(e, value) => {
|
|
||||||
setConfig((prev) => ({ ...prev, memory: value as number }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="success"
|
|
||||||
onClick={() => {
|
|
||||||
savePackConfig();
|
|
||||||
handleClose();
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
borderRadius: '3vw',
|
|
||||||
fontFamily: 'Benzin-Bold',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Сохранить
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Modal>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -38,13 +38,25 @@ const Login = () => {
|
|||||||
console.log(
|
console.log(
|
||||||
'Не удалось обновить токен, требуется новая авторизация',
|
'Не удалось обновить токен, требуется новая авторизация',
|
||||||
);
|
);
|
||||||
const newSession = await authenticateWithElyBy(
|
// Очищаем недействительные токены
|
||||||
config.username,
|
saveConfig({
|
||||||
config.password,
|
accessToken: '',
|
||||||
saveConfig,
|
clientToken: '',
|
||||||
);
|
});
|
||||||
if (!newSession) {
|
|
||||||
console.log('Авторизация не удалась');
|
// Пытаемся выполнить новую авторизацию
|
||||||
|
if (config.password) {
|
||||||
|
const newSession = await authenticateWithElyBy(
|
||||||
|
config.username,
|
||||||
|
config.password,
|
||||||
|
saveConfig,
|
||||||
|
);
|
||||||
|
if (!newSession) {
|
||||||
|
console.log('Авторизация не удалась');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Требуется ввод пароля для новой авторизации');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,6 +65,13 @@ const Login = () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Токен отсутствует, выполняем авторизацию...');
|
console.log('Токен отсутствует, выполняем авторизацию...');
|
||||||
|
// Проверяем наличие пароля
|
||||||
|
if (!config.password) {
|
||||||
|
console.log('Ошибка: не указан пароль');
|
||||||
|
alert('Введите пароль!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const session = await authenticateWithElyBy(
|
const session = await authenticateWithElyBy(
|
||||||
config.username,
|
config.username,
|
||||||
config.password,
|
config.password,
|
||||||
@ -68,6 +87,11 @@ const Login = () => {
|
|||||||
navigate('/');
|
navigate('/');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`ОШИБКА при авторизации: ${error.message}`);
|
console.log(`ОШИБКА при авторизации: ${error.message}`);
|
||||||
|
// Очищаем недействительные токены при ошибке
|
||||||
|
saveConfig({
|
||||||
|
accessToken: '',
|
||||||
|
clientToken: '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -79,12 +103,6 @@ const Login = () => {
|
|||||||
handleInputChange={handleInputChange}
|
handleInputChange={handleInputChange}
|
||||||
onLogin={authorization}
|
onLogin={authorization}
|
||||||
/>
|
/>
|
||||||
<MemorySlider
|
|
||||||
memory={config.memory}
|
|
||||||
onChange={(e, value) => {
|
|
||||||
setConfig((prev) => ({ ...prev, memory: value as number }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
398
src/renderer/pages/Marketplace.tsx
Normal file
398
src/renderer/pages/Marketplace.tsx
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
// src/renderer/pages/Marketplace.tsx
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
CircularProgress,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardMedia,
|
||||||
|
Pagination,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
Alert,
|
||||||
|
Snackbar,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { isPlayerOnline, getPlayerServer } from '../utils/playerOnlineCheck';
|
||||||
|
import { buyItem, fetchMarketplace, MarketplaceResponse, Server } from '../api';
|
||||||
|
import PlayerInventory from '../components/PlayerInventory';
|
||||||
|
|
||||||
|
interface TabPanelProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
index: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabPanel(props: TabPanelProps) {
|
||||||
|
const { children, value, index, ...other } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
hidden={value !== index}
|
||||||
|
id={`marketplace-tabpanel-${index}`}
|
||||||
|
aria-labelledby={`marketplace-tab-${index}`}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
{value === index && <Box sx={{ pt: 3 }}>{children}</Box>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Marketplace() {
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const [marketLoading, setMarketLoading] = useState<boolean>(false);
|
||||||
|
const [isOnline, setIsOnline] = useState<boolean>(false);
|
||||||
|
const [username, setUsername] = useState<string>('');
|
||||||
|
const [playerServer, setPlayerServer] = useState<Server | null>(null);
|
||||||
|
const [marketItems, setMarketItems] = useState<MarketplaceResponse | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [page, setPage] = useState<number>(1);
|
||||||
|
const [totalPages, setTotalPages] = useState<number>(1);
|
||||||
|
const [tabValue, setTabValue] = useState<number>(0);
|
||||||
|
const [notification, setNotification] = useState<{
|
||||||
|
open: boolean;
|
||||||
|
message: string;
|
||||||
|
type: 'success' | 'error';
|
||||||
|
}>({
|
||||||
|
open: false,
|
||||||
|
message: '',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция для проверки онлайн-статуса игрока и определения сервера
|
||||||
|
const checkPlayerStatus = async () => {
|
||||||
|
if (!username) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
// Проверяем, онлайн ли игрок и получаем сервер, где он находится
|
||||||
|
const { online, server } = await getPlayerServer(username);
|
||||||
|
setIsOnline(online);
|
||||||
|
setPlayerServer(server);
|
||||||
|
|
||||||
|
// Если игрок онлайн и на каком-то сервере, загружаем предметы рынка
|
||||||
|
if (online && server) {
|
||||||
|
await loadMarketItems(server.ip, 1);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при проверке онлайн-статуса:', error);
|
||||||
|
setIsOnline(false);
|
||||||
|
setPlayerServer(null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для загрузки предметов маркетплейса
|
||||||
|
const loadMarketItems = async (serverIp: string, pageNumber: number) => {
|
||||||
|
try {
|
||||||
|
setMarketLoading(true);
|
||||||
|
const marketData = await fetchMarketplace(serverIp, pageNumber, 10); // 10 предметов на страницу
|
||||||
|
setMarketItems(marketData);
|
||||||
|
setPage(marketData.page);
|
||||||
|
setTotalPages(marketData.pages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке предметов рынка:', error);
|
||||||
|
setMarketItems(null);
|
||||||
|
} finally {
|
||||||
|
setMarketLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик смены страницы
|
||||||
|
const handlePageChange = (
|
||||||
|
_event: React.ChangeEvent<unknown>,
|
||||||
|
newPage: number,
|
||||||
|
) => {
|
||||||
|
if (playerServer) {
|
||||||
|
loadMarketItems(playerServer.ip, newPage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик смены вкладок
|
||||||
|
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||||
|
setTabValue(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обновляем функцию handleBuyItem в Marketplace.tsx
|
||||||
|
const handleBuyItem = async (itemId: string) => {
|
||||||
|
try {
|
||||||
|
if (username) {
|
||||||
|
const result = await buyItem(username, itemId);
|
||||||
|
|
||||||
|
setNotification({
|
||||||
|
open: true,
|
||||||
|
message:
|
||||||
|
result.message ||
|
||||||
|
'Предмет успешно куплен! Он будет добавлен в ваш инвентарь.',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем список предметов
|
||||||
|
if (playerServer) {
|
||||||
|
loadMarketItems(playerServer.ip, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при покупке предмета:', error);
|
||||||
|
setNotification({
|
||||||
|
open: true,
|
||||||
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Ошибка при покупке предмета',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Закрытие уведомления
|
||||||
|
const handleCloseNotification = () => {
|
||||||
|
setNotification({ ...notification, open: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Получаем имя пользователя из localStorage при монтировании компонента
|
||||||
|
useEffect(() => {
|
||||||
|
const savedConfig = localStorage.getItem('launcher_config');
|
||||||
|
if (savedConfig) {
|
||||||
|
const config = JSON.parse(savedConfig);
|
||||||
|
if (config.username) {
|
||||||
|
setUsername(config.username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Проверяем статус при изменении username
|
||||||
|
useEffect(() => {
|
||||||
|
if (username) {
|
||||||
|
checkPlayerStatus();
|
||||||
|
}
|
||||||
|
}, [username]);
|
||||||
|
|
||||||
|
// Показываем loader во время проверки
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100%',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={60} />
|
||||||
|
<Typography variant="h6" color="white">
|
||||||
|
Проверяем, находитесь ли вы на сервере...
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если игрок не онлайн
|
||||||
|
if (!isOnline) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100%',
|
||||||
|
gap: 3,
|
||||||
|
padding: 4,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h4" color="error">
|
||||||
|
Доступ к рынку ограничен
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" color="white">
|
||||||
|
Для доступа к рынку вам необходимо находиться на одном из серверов
|
||||||
|
игры.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="white" sx={{ opacity: 0.8 }}>
|
||||||
|
Зайдите на любой сервер и обновите страницу.
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={checkPlayerStatus}
|
||||||
|
sx={{
|
||||||
|
mt: '1%',
|
||||||
|
borderRadius: '20px',
|
||||||
|
p: '10px 25px',
|
||||||
|
color: 'white',
|
||||||
|
backgroundColor: 'rgb(255, 77, 77)',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(255, 77, 77, 0.5)',
|
||||||
|
},
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Проверить снова
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ padding: 3 }}>
|
||||||
|
<Typography variant="h4" color="white" gutterBottom>
|
||||||
|
Рынок сервера {playerServer?.name || ''}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Вкладки */}
|
||||||
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||||
|
<Tabs
|
||||||
|
value={tabValue}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
aria-label="marketplace tabs"
|
||||||
|
>
|
||||||
|
<Tab label="Товары" />
|
||||||
|
<Tab label="Мой инвентарь" />
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Содержимое вкладки "Товары" */}
|
||||||
|
<TabPanel value={tabValue} index={0}>
|
||||||
|
{marketLoading ? (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
) : !marketItems || marketItems.items.length === 0 ? (
|
||||||
|
<Box sx={{ mt: 4, textAlign: 'center' }}>
|
||||||
|
<Typography variant="h6" color="white">
|
||||||
|
На данный момент на рынке нет предметов.
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() =>
|
||||||
|
playerServer && loadMarketItems(playerServer.ip, 1)
|
||||||
|
}
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
>
|
||||||
|
Обновить
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||||
|
{marketItems.items.map((item) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} lg={3} key={item.id}>
|
||||||
|
<Card sx={{ bgcolor: 'rgba(255, 255, 255, 0.05)' }}>
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
sx={{
|
||||||
|
height: 140,
|
||||||
|
objectFit: 'contain',
|
||||||
|
bgcolor: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
p: 1,
|
||||||
|
imageRendering: 'pixelated',
|
||||||
|
}}
|
||||||
|
image={`/minecraft/${item.material.toLowerCase()}.png`}
|
||||||
|
alt={item.material}
|
||||||
|
/>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" color="white">
|
||||||
|
{item.display_name ||
|
||||||
|
item.material
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\b\w/g, (l) => l.toUpperCase())}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="white">
|
||||||
|
Количество: {item.amount}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="white">
|
||||||
|
Цена: {item.price} монет
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="white"
|
||||||
|
sx={{ opacity: 0.7 }}
|
||||||
|
>
|
||||||
|
Продавец: {item.seller_name}
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
fullWidth
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
onClick={() => handleBuyItem(item.id)}
|
||||||
|
>
|
||||||
|
Купить
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
||||||
|
<Pagination
|
||||||
|
count={totalPages}
|
||||||
|
page={page}
|
||||||
|
onChange={handlePageChange}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* Содержимое вкладки "Мой инвентарь" */}
|
||||||
|
<TabPanel value={tabValue} index={1}>
|
||||||
|
{playerServer && username ? (
|
||||||
|
<PlayerInventory
|
||||||
|
username={username}
|
||||||
|
serverIp={playerServer.ip}
|
||||||
|
onSellSuccess={() => {
|
||||||
|
// После успешной продажи, обновляем список товаров
|
||||||
|
if (playerServer) {
|
||||||
|
loadMarketItems(playerServer.ip, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем уведомление
|
||||||
|
setNotification({
|
||||||
|
open: true,
|
||||||
|
message: 'Предмет успешно выставлен на продажу!',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
color="white"
|
||||||
|
sx={{ textAlign: 'center', my: 4 }}
|
||||||
|
>
|
||||||
|
Не удалось загрузить инвентарь.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* Уведомления */}
|
||||||
|
<Snackbar
|
||||||
|
open={notification.open}
|
||||||
|
autoHideDuration={6000}
|
||||||
|
onClose={handleCloseNotification}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
onClose={handleCloseNotification}
|
||||||
|
severity={notification.type}
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{notification.message}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
373
src/renderer/pages/Profile.tsx
Normal file
373
src/renderer/pages/Profile.tsx
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import SkinViewer from '../components/SkinViewer';
|
||||||
|
import {
|
||||||
|
fetchPlayer,
|
||||||
|
uploadSkin,
|
||||||
|
fetchCapes,
|
||||||
|
Cape,
|
||||||
|
activateCape,
|
||||||
|
deactivateCape,
|
||||||
|
} from '../api';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Alert,
|
||||||
|
CircularProgress,
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import CapeCard from '../components/CapeCard';
|
||||||
|
|
||||||
|
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);
|
||||||
|
const [capes, setCapes] = useState<Cape[]>([]);
|
||||||
|
const [uuid, setUuid] = useState<string>('');
|
||||||
|
const [loading, setLoading] = 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 || '');
|
||||||
|
loadCapesData(config.username || '');
|
||||||
|
setUuid(config.uuid || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadPlayerData = async (uuid: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const player = await fetchPlayer(uuid);
|
||||||
|
setSkin(player.skin_url);
|
||||||
|
setCape(player.cloak_url);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении данных игрока:', error);
|
||||||
|
setSkin('');
|
||||||
|
setCape('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadCapesData = async (username: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const capesData = await fetchCapes(username);
|
||||||
|
setCapes(capesData);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении плащей:', error);
|
||||||
|
setCapes([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработка перетаскивания файла
|
||||||
|
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 handleActivateCape = async (cape_id: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
await activateCape(username, cape_id);
|
||||||
|
await loadCapesData(username);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeactivateCape = async (cape_id: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
await deactivateCape(username, cape_id);
|
||||||
|
await loadCapesData(username);
|
||||||
|
await loadPlayerData(uuid);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Отправка запроса на установку скина
|
||||||
|
const handleUploadSkin = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
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');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
my: 4,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '100px',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<CircularProgress />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
borderRadius: 2,
|
||||||
|
overflow: 'hidden',
|
||||||
|
mb: 4,
|
||||||
|
bgcolor: 'transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Используем переработанный компонент SkinViewer */}
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
alignSelf: 'center',
|
||||||
|
justifySelf: 'center',
|
||||||
|
textAlign: 'center',
|
||||||
|
width: '100%',
|
||||||
|
mb: '5vw',
|
||||||
|
fontSize: '3vw',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{username}
|
||||||
|
</Typography>
|
||||||
|
<SkinViewer
|
||||||
|
width={300}
|
||||||
|
height={400}
|
||||||
|
skinUrl={skin}
|
||||||
|
capeUrl={cape}
|
||||||
|
walkingSpeed={walkingSpeed}
|
||||||
|
autoRotate={true}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '500px',
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
padding: '3vw',
|
||||||
|
borderRadius: '1vw',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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',
|
||||||
|
borderRadius: '20px',
|
||||||
|
p: '10px 25px',
|
||||||
|
backgroundColor: 'rgb(0, 134, 0)',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(0, 134, 0, 0.5)',
|
||||||
|
},
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
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
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>Ваши плащи</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 2,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{capes.map((cape) => (
|
||||||
|
<CapeCard
|
||||||
|
key={cape.cape_id}
|
||||||
|
cape={cape}
|
||||||
|
mode="profile"
|
||||||
|
onAction={
|
||||||
|
cape.is_active ? handleDeactivateCape : handleActivateCape
|
||||||
|
}
|
||||||
|
actionDisabled={loading}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
421
src/renderer/pages/Registration.tsx
Normal file
421
src/renderer/pages/Registration.tsx
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
import Stepper from '@mui/material/Stepper';
|
||||||
|
import Step from '@mui/material/Step';
|
||||||
|
import StepLabel from '@mui/material/StepLabel';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
StepConnector,
|
||||||
|
stepConnectorClasses,
|
||||||
|
StepIconProps,
|
||||||
|
styled,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Snackbar,
|
||||||
|
CircularProgress,
|
||||||
|
} from '@mui/material';
|
||||||
|
import LoginRoundedIcon from '@mui/icons-material/LoginRounded';
|
||||||
|
import VerifiedRoundedIcon from '@mui/icons-material/VerifiedRounded';
|
||||||
|
import AssignmentIndRoundedIcon from '@mui/icons-material/AssignmentIndRounded';
|
||||||
|
import {
|
||||||
|
generateVerificationCode,
|
||||||
|
registerUser,
|
||||||
|
getVerificationStatus,
|
||||||
|
} from '../api';
|
||||||
|
import QRCodeStyling from 'qr-code-styling';
|
||||||
|
import popalogo from '../../../assets/icons/popa-popa.svg';
|
||||||
|
|
||||||
|
const ColorlibConnector = styled(StepConnector)(({ theme }) => ({
|
||||||
|
[`&.${stepConnectorClasses.alternativeLabel}`]: {
|
||||||
|
top: 22,
|
||||||
|
},
|
||||||
|
[`&.${stepConnectorClasses.active}`]: {
|
||||||
|
[`& .${stepConnectorClasses.line}`]: {
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[`&.${stepConnectorClasses.completed}`]: {
|
||||||
|
[`& .${stepConnectorClasses.line}`]: {
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[`& .${stepConnectorClasses.line}`]: {
|
||||||
|
height: 3,
|
||||||
|
border: 0,
|
||||||
|
backgroundColor: '#eaeaf0',
|
||||||
|
borderRadius: 1,
|
||||||
|
...theme.applyStyles('dark', {
|
||||||
|
backgroundColor: theme.palette.grey[800],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ColorlibStepIconRoot = styled('div')<{
|
||||||
|
ownerState: { completed?: boolean; active?: boolean };
|
||||||
|
}>(({ theme }) => ({
|
||||||
|
backgroundColor: '#ccc',
|
||||||
|
zIndex: 1,
|
||||||
|
color: '#fff',
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
display: 'flex',
|
||||||
|
borderRadius: '50%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
...theme.applyStyles('dark', {
|
||||||
|
backgroundColor: theme.palette.grey[700],
|
||||||
|
}),
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
props: ({ ownerState }) => ownerState.active,
|
||||||
|
style: {
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
|
||||||
|
boxShadow: '0 4px 10px 0 rgba(0,0,0,.25)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
props: ({ ownerState }) => ownerState.completed,
|
||||||
|
style: {
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
function ColorlibStepIcon(props: StepIconProps) {
|
||||||
|
const { active, completed, className } = props;
|
||||||
|
|
||||||
|
const icons: { [index: string]: React.ReactElement<unknown> } = {
|
||||||
|
1: <AssignmentIndRoundedIcon />,
|
||||||
|
2: <VerifiedRoundedIcon />,
|
||||||
|
3: <LoginRoundedIcon />,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColorlibStepIconRoot
|
||||||
|
ownerState={{ completed, active }}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{icons[String(props.icon)]}
|
||||||
|
</ColorlibStepIconRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const qrCode = new QRCodeStyling({
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
image: popalogo,
|
||||||
|
data: 'https://t.me/popa_popa_popa_bot?start=test',
|
||||||
|
shape: 'square',
|
||||||
|
margin: 10,
|
||||||
|
dotsOptions: {
|
||||||
|
gradient: {
|
||||||
|
type: 'linear',
|
||||||
|
colorStops: [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: 'rgb(242,113,33)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: 'rgb(233,64,87)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
type: 'extra-rounded',
|
||||||
|
},
|
||||||
|
imageOptions: {
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
margin: 20,
|
||||||
|
imageSize: 0.5,
|
||||||
|
},
|
||||||
|
backgroundOptions: {
|
||||||
|
color: 'transparent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Registration = () => {
|
||||||
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [verificationCode, setVerificationCode] = useState<string | null>(null);
|
||||||
|
const ref = useRef(null);
|
||||||
|
const [url, setUrl] = useState('');
|
||||||
|
const steps = ['Создание аккаунта', 'Верификация аккаунта в телеграмме'];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
qrCode.append(ref.current);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
qrCode.update({
|
||||||
|
data: url,
|
||||||
|
});
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
const handleCreateAccount = async () => {
|
||||||
|
const response = await registerUser(username, password);
|
||||||
|
if (response.status === 'success') {
|
||||||
|
setActiveStep(1);
|
||||||
|
} else {
|
||||||
|
setOpen(true);
|
||||||
|
setMessage(response.status);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeStep === 1) {
|
||||||
|
handleGenerateVerificationCode(username);
|
||||||
|
setUrl(`https://t.me/popa_popa_popa_bot?start=${username}`);
|
||||||
|
|
||||||
|
while (ref.current.firstChild) {
|
||||||
|
ref.current.removeChild(ref.current.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newQrCode = new QRCodeStyling({
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
image: popalogo,
|
||||||
|
data: 'https://t.me/popa_popa_popa_bot?start=test',
|
||||||
|
shape: 'square',
|
||||||
|
margin: 10,
|
||||||
|
dotsOptions: {
|
||||||
|
gradient: {
|
||||||
|
type: 'linear',
|
||||||
|
colorStops: [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: 'rgb(242,113,33)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: 'rgb(233,64,87)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
type: 'extra-rounded',
|
||||||
|
},
|
||||||
|
imageOptions: {
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
margin: 20,
|
||||||
|
imageSize: 0.5,
|
||||||
|
},
|
||||||
|
backgroundOptions: {
|
||||||
|
color: 'transparent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
newQrCode.append(ref.current);
|
||||||
|
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
handleVerifyCode();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [activeStep]);
|
||||||
|
|
||||||
|
const handleGenerateVerificationCode = async (username: string) => {
|
||||||
|
console.log(username);
|
||||||
|
const response = await generateVerificationCode(username);
|
||||||
|
setVerificationCode(response.code);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyCode = async () => {
|
||||||
|
const response = await getVerificationStatus(username);
|
||||||
|
if (response.is_verified) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenBot = () => {
|
||||||
|
window.open(`https://t.me/popa_popa_popa_bot?start=${username}`, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<Stepper
|
||||||
|
activeStep={activeStep}
|
||||||
|
alternativeLabel
|
||||||
|
connector={<ColorlibConnector />}
|
||||||
|
>
|
||||||
|
{steps.map((label) => (
|
||||||
|
<Step key={label}>
|
||||||
|
<StepLabel
|
||||||
|
sx={{
|
||||||
|
'& .MuiStepLabel-label': {
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
'& .Mui-completed': {
|
||||||
|
color: 'white !important',
|
||||||
|
},
|
||||||
|
'& .Mui-active': {
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
StepIconComponent={ColorlibStepIcon}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</StepLabel>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
{activeStep === 0 && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6">Создание аккаунта</Typography>
|
||||||
|
<Typography variant="body1">Введите ваш никнейм</Typography>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
name="username"
|
||||||
|
variant="outlined"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
// '& .MuiFormLabel-root': {
|
||||||
|
// color: 'white',
|
||||||
|
// },
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
'& .MuiInput-underline:after': {
|
||||||
|
borderBottomColor: '#B2BAC2',
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
'& fieldset': {
|
||||||
|
borderColor: '#E0E3E7',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
'&:hover fieldset': {
|
||||||
|
borderColor: '#B2BAC2',
|
||||||
|
},
|
||||||
|
'&.Mui-focused fieldset': {
|
||||||
|
borderColor: '#6F7E8C',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="body1">Введите ваш пароль</Typography>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
variant="outlined"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
// '& .MuiFormLabel-root': {
|
||||||
|
// color: 'white',
|
||||||
|
// },
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
'& .MuiInput-underline:after': {
|
||||||
|
borderBottomColor: '#B2BAC2',
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
'& fieldset': {
|
||||||
|
borderColor: '#E0E3E7',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
'&:hover fieldset': {
|
||||||
|
borderColor: '#B2BAC2',
|
||||||
|
},
|
||||||
|
'&.Mui-focused fieldset': {
|
||||||
|
borderColor: '#6F7E8C',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ width: '100%', mt: 2 }}
|
||||||
|
onClick={handleCreateAccount}
|
||||||
|
>
|
||||||
|
Создать
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{activeStep === 1 && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6">Откройте бота в телеграмме</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ width: '100%', mt: 2 }}
|
||||||
|
onClick={handleOpenBot}
|
||||||
|
>
|
||||||
|
Открыть бота
|
||||||
|
</Button>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
minHeight: 300,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="body1">
|
||||||
|
Введите код верификации в боте
|
||||||
|
</Typography>
|
||||||
|
{verificationCode ? (
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
sx={{
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{verificationCode}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">Ждем ответа от бота</Typography>
|
||||||
|
<CircularProgress />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<CircularProgress />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Snackbar
|
||||||
|
open={open}
|
||||||
|
autoHideDuration={6000}
|
||||||
|
onClose={handleClose}
|
||||||
|
message={message}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
130
src/renderer/pages/Shop.tsx
Normal file
130
src/renderer/pages/Shop.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { Box } from '@mui/material';
|
||||||
|
import { Typography } from '@mui/material';
|
||||||
|
import CapeCard from '../components/CapeCard';
|
||||||
|
import {
|
||||||
|
Cape,
|
||||||
|
fetchCapes,
|
||||||
|
fetchCapesStore,
|
||||||
|
purchaseCape,
|
||||||
|
StoreCape,
|
||||||
|
} from '../api';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function Shop() {
|
||||||
|
const [storeCapes, setStoreCapes] = useState<StoreCape[]>([]);
|
||||||
|
const [userCapes, setUserCapes] = useState<Cape[]>([]);
|
||||||
|
const [username, setUsername] = useState<string>('');
|
||||||
|
const [uuid, setUuid] = useState<string>('');
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Функция для загрузки плащей из магазина
|
||||||
|
const loadStoreCapes = async () => {
|
||||||
|
try {
|
||||||
|
const capes = await fetchCapesStore();
|
||||||
|
setStoreCapes(capes);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении плащей магазина:', error);
|
||||||
|
setStoreCapes([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для загрузки плащей пользователя
|
||||||
|
const loadUserCapes = async (username: string) => {
|
||||||
|
try {
|
||||||
|
const userCapes = await fetchCapes(username);
|
||||||
|
setUserCapes(userCapes);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении плащей пользователя:', error);
|
||||||
|
setUserCapes([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePurchaseCape = async (cape_id: string) => {
|
||||||
|
try {
|
||||||
|
await purchaseCape(username, cape_id);
|
||||||
|
await loadUserCapes(username);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при покупке плаща:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Загружаем данные при монтировании
|
||||||
|
useEffect(() => {
|
||||||
|
const savedConfig = localStorage.getItem('launcher_config');
|
||||||
|
if (savedConfig) {
|
||||||
|
const config = JSON.parse(savedConfig);
|
||||||
|
if (config.uuid && config.username) {
|
||||||
|
setUsername(config.username);
|
||||||
|
setUuid(config.uuid);
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Загружаем оба списка плащей
|
||||||
|
Promise.all([loadStoreCapes(), loadUserCapes(config.username)]).finally(
|
||||||
|
() => {
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Фильтруем плащи, которые уже куплены пользователем
|
||||||
|
const availableCapes = storeCapes.filter(
|
||||||
|
(storeCape) =>
|
||||||
|
!userCapes.some((userCape) => userCape.cape_id === storeCape.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '2vw',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Typography>Загрузка...</Typography>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignContent: 'flex-start',
|
||||||
|
width: '90%',
|
||||||
|
height: '80%',
|
||||||
|
gap: '2vw',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6">Доступные плащи</Typography>
|
||||||
|
{availableCapes.length > 0 ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: '2vw',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{availableCapes.map((cape) => (
|
||||||
|
<CapeCard
|
||||||
|
key={cape.id}
|
||||||
|
cape={cape}
|
||||||
|
mode="shop"
|
||||||
|
onAction={handlePurchaseCape}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Typography>У вас уже есть все доступные плащи!</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
394
src/renderer/pages/VersionsExplorer.tsx
Normal file
394
src/renderer/pages/VersionsExplorer.tsx
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardMedia,
|
||||||
|
CardContent,
|
||||||
|
CardActions,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
Modal,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
IconButton,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
|
|
||||||
|
interface VersionCardProps {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
imageUrl: string;
|
||||||
|
version: string;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VersionCard: React.FC<VersionCardProps> = ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
imageUrl,
|
||||||
|
version,
|
||||||
|
onSelect,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'rgba(30, 30, 50, 0.8)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
width: '35vw',
|
||||||
|
height: '35vh',
|
||||||
|
minWidth: 'unset',
|
||||||
|
minHeight: 'unset',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
borderRadius: '16px',
|
||||||
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)',
|
||||||
|
transition: 'transform 0.3s, box-shadow 0.3s',
|
||||||
|
overflow: 'hidden',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={() => onSelect(id)}
|
||||||
|
>
|
||||||
|
<CardContent
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '10%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
gutterBottom
|
||||||
|
variant="h5"
|
||||||
|
component="div"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface VersionInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
config?: {
|
||||||
|
downloadUrl: string;
|
||||||
|
apiReleaseUrl: string;
|
||||||
|
versionFileName: string;
|
||||||
|
packName: string;
|
||||||
|
memory: number;
|
||||||
|
baseVersion: string;
|
||||||
|
serverIp: string;
|
||||||
|
fabricVersion: string;
|
||||||
|
preserveFiles: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AvailableVersionInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
config: {
|
||||||
|
downloadUrl: string;
|
||||||
|
apiReleaseUrl: string;
|
||||||
|
versionFileName: string;
|
||||||
|
packName: string;
|
||||||
|
memory: number;
|
||||||
|
baseVersion: string;
|
||||||
|
serverIp: string;
|
||||||
|
fabricVersion: string;
|
||||||
|
preserveFiles: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// В компоненте VersionsExplorer
|
||||||
|
export const VersionsExplorer = () => {
|
||||||
|
const [installedVersions, setInstalledVersions] = useState<VersionInfo[]>([]);
|
||||||
|
const [availableVersions, setAvailableVersions] = useState<
|
||||||
|
AvailableVersionInfo[]
|
||||||
|
>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [downloadLoading, setDownloadLoading] = useState<string | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchVersions = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Получаем список установленных версий через IPC
|
||||||
|
const installedResult = await window.electron.ipcRenderer.invoke(
|
||||||
|
'get-installed-versions',
|
||||||
|
);
|
||||||
|
if (installedResult.success) {
|
||||||
|
setInstalledVersions(installedResult.versions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем доступные версии с GitHub Gist
|
||||||
|
const availableResult = await window.electron.ipcRenderer.invoke(
|
||||||
|
'get-available-versions',
|
||||||
|
{
|
||||||
|
gistUrl:
|
||||||
|
'https://gist.githubusercontent.com/DIKER0K/06cd12fb3a4d08b1f0f8c763a7d05e06/raw/versions.json',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (availableResult.success) {
|
||||||
|
setAvailableVersions(availableResult.versions);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке версий:', error);
|
||||||
|
// Можно добавить обработку ошибки, например показать уведомление
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchVersions();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSelectVersion = (version: VersionInfo) => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'selected_version_config',
|
||||||
|
JSON.stringify(version.config || {}),
|
||||||
|
);
|
||||||
|
navigate(`/launch/${version.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddVersion = () => {
|
||||||
|
setModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadVersion = async (version: AvailableVersionInfo) => {
|
||||||
|
try {
|
||||||
|
setDownloadLoading(version.id);
|
||||||
|
|
||||||
|
// Скачивание и установка выбранной версии
|
||||||
|
const downloadResult = await window.electron.ipcRenderer.invoke(
|
||||||
|
'download-and-extract',
|
||||||
|
{
|
||||||
|
downloadUrl: version.config.downloadUrl,
|
||||||
|
apiReleaseUrl: version.config.apiReleaseUrl,
|
||||||
|
versionFileName: version.config.versionFileName,
|
||||||
|
packName: version.id,
|
||||||
|
preserveFiles: version.config.preserveFiles || [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (downloadResult?.success) {
|
||||||
|
// Добавляем скачанную версию в список установленных
|
||||||
|
setInstalledVersions((prev) => [...prev, version]);
|
||||||
|
setModalOpen(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Ошибка при скачивании версии ${version.id}:`, error);
|
||||||
|
} finally {
|
||||||
|
setDownloadLoading(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Карточка добавления новой версии
|
||||||
|
const AddVersionCard = () => (
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'rgba(30, 30, 50, 0.8)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
width: '35vw',
|
||||||
|
height: '35vh',
|
||||||
|
minWidth: 'unset',
|
||||||
|
minHeight: 'unset',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
borderRadius: '16px',
|
||||||
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)',
|
||||||
|
transition: 'transform 0.3s, box-shadow 0.3s',
|
||||||
|
overflow: 'hidden',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={handleAddVersion}
|
||||||
|
>
|
||||||
|
<AddIcon sx={{ fontSize: 60, color: '#fff' }} />
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
color: '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Добавить
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
color: '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
версию
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingLeft: '5vw',
|
||||||
|
paddingRight: '5vw',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Box display="flex" justifyContent="center" my={5}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={3}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
overflowY: 'auto',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Показываем установленные версии или дефолтную, если она есть */}
|
||||||
|
{installedVersions.length > 0 ? (
|
||||||
|
installedVersions.map((version) => (
|
||||||
|
<Grid
|
||||||
|
key={version.id}
|
||||||
|
size={{ xs: 'auto', sm: 'auto', md: 'auto' }}
|
||||||
|
>
|
||||||
|
<VersionCard
|
||||||
|
id={version.id}
|
||||||
|
name={version.name}
|
||||||
|
imageUrl={
|
||||||
|
version.imageUrl ||
|
||||||
|
'https://via.placeholder.com/300x140?text=Minecraft'
|
||||||
|
}
|
||||||
|
version={version.version}
|
||||||
|
onSelect={() => handleSelectVersion(version)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
// Если нет ни одной версии, показываем карточку добавления
|
||||||
|
<Grid size={{ xs: 'auto', sm: 'auto', md: 'auto' }}>
|
||||||
|
<AddVersionCard />
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Всегда добавляем карточку для добавления новых версий */}
|
||||||
|
{installedVersions.length > 0 && (
|
||||||
|
<Grid size={{ xs: 'auto', sm: 'auto', md: 'auto' }}>
|
||||||
|
<AddVersionCard />
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Модальное окно для выбора версии для скачивания */}
|
||||||
|
<Modal
|
||||||
|
open={modalOpen}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
aria-labelledby="modal-versions"
|
||||||
|
aria-describedby="modal-available-versions"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 400,
|
||||||
|
maxHeight: '80vh',
|
||||||
|
overflowY: 'auto',
|
||||||
|
background: 'linear-gradient(45deg, #000000 10%, #3b4187 184.73%)',
|
||||||
|
border: '2px solid #000',
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4,
|
||||||
|
borderRadius: '3vw',
|
||||||
|
gap: '1vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" component="h2" sx={{ color: '#fff' }}>
|
||||||
|
Доступные версии для скачивания
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{availableVersions.length === 0 ? (
|
||||||
|
<Typography sx={{ color: '#fff', mt: 2 }}>
|
||||||
|
Загрузка доступных версий...
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<List sx={{ mt: 2 }}>
|
||||||
|
{availableVersions.map((version) => (
|
||||||
|
<ListItem
|
||||||
|
key={version.id}
|
||||||
|
sx={{
|
||||||
|
borderRadius: '8px',
|
||||||
|
mb: 1,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => handleSelectVersion(version)}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={version.name}
|
||||||
|
secondary={version.version}
|
||||||
|
primaryTypographyProps={{ color: '#fff' }}
|
||||||
|
secondaryTypographyProps={{
|
||||||
|
color: 'rgba(255,255,255,0.7)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
mt: 3,
|
||||||
|
alignSelf: 'center',
|
||||||
|
borderColor: '#fff',
|
||||||
|
color: '#fff',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: '#ccc',
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Закрыть
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
128
src/renderer/utils/playerOnlineCheck.ts
Normal file
128
src/renderer/utils/playerOnlineCheck.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { fetchActiveServers, fetchOnlinePlayers, Server } from '../api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, находится ли указанный игрок онлайн на любом из серверов
|
||||||
|
* @param username Имя игрока для проверки
|
||||||
|
* @returns {Promise<boolean>} true, если игрок онлайн хотя бы на одном сервере
|
||||||
|
*/
|
||||||
|
export async function isPlayerOnline(username: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
console.log('Начинаем проверку статуса для:', username);
|
||||||
|
|
||||||
|
// Получаем список активных серверов (теперь это массив)
|
||||||
|
const servers = await fetchActiveServers();
|
||||||
|
console.log('Ответ API активных серверов:', servers);
|
||||||
|
|
||||||
|
// Фильтруем серверы с игроками
|
||||||
|
const serversWithPlayers = servers.filter(
|
||||||
|
(server) => server.online_players > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Если нет серверов с игроками, игрок точно не онлайн
|
||||||
|
if (serversWithPlayers.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем каждый сервер на наличие игрока
|
||||||
|
const checkPromises = serversWithPlayers.map(async (server) => {
|
||||||
|
try {
|
||||||
|
const onlinePlayers = await fetchOnlinePlayers(server.id);
|
||||||
|
|
||||||
|
// Проверяем, есть ли игрок с указанным именем в списке
|
||||||
|
// Предполагая, что онлайн игроки хранятся в массиве online_players
|
||||||
|
return (
|
||||||
|
Array.isArray(onlinePlayers.online_players) &&
|
||||||
|
onlinePlayers.online_players.some(
|
||||||
|
(player) => player.username === username,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Ошибка при проверке сервера ${server.id}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ожидаем результаты всех проверок
|
||||||
|
const results = await Promise.all(checkPromises);
|
||||||
|
|
||||||
|
// Игрок онлайн, если хотя бы одна проверка вернула true
|
||||||
|
return results.some((result) => result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при проверке онлайн-статуса игрока:', error);
|
||||||
|
return false; // В случае ошибки считаем, что игрок не онлайн
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, находится ли указанный игрок онлайн на конкретном сервере
|
||||||
|
* @param username Имя игрока для проверки
|
||||||
|
* @param serverId ID сервера для проверки
|
||||||
|
* @returns {Promise<boolean>} true, если игрок онлайн на указанном сервере
|
||||||
|
*/
|
||||||
|
export async function isPlayerOnlineOnServer(
|
||||||
|
username: string,
|
||||||
|
serverId: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const onlinePlayers = await fetchOnlinePlayers(serverId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
Array.isArray(onlinePlayers.online_players) &&
|
||||||
|
onlinePlayers.online_players.some(
|
||||||
|
(player) => player.username === username,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Ошибка при проверке игрока на сервере ${serverId}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, на каком сервере находится игрок
|
||||||
|
* @param username Имя игрока для проверки
|
||||||
|
* @returns Объект с информацией: онлайн ли игрок и на каком сервере
|
||||||
|
*/
|
||||||
|
export async function getPlayerServer(
|
||||||
|
username: string,
|
||||||
|
): Promise<{ online: boolean; server: Server | null }> {
|
||||||
|
try {
|
||||||
|
// Получаем список активных серверов
|
||||||
|
const servers = await fetchActiveServers();
|
||||||
|
|
||||||
|
// Фильтруем серверы с игроками
|
||||||
|
const serversWithPlayers = servers.filter(
|
||||||
|
(server) => server.online_players > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Если нет серверов с игроками, игрок точно не онлайн
|
||||||
|
if (serversWithPlayers.length === 0) {
|
||||||
|
return { online: false, server: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем каждый сервер на наличие игрока
|
||||||
|
for (const server of serversWithPlayers) {
|
||||||
|
try {
|
||||||
|
const onlinePlayers = await fetchOnlinePlayers(server.id);
|
||||||
|
|
||||||
|
if (
|
||||||
|
Array.isArray(onlinePlayers.online_players) &&
|
||||||
|
onlinePlayers.online_players.some(
|
||||||
|
(player) => player.username === username,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Игрок найден на этом сервере
|
||||||
|
return { online: true, server };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Ошибка при проверке сервера ${server.id}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Игрок не найден ни на одном сервере
|
||||||
|
return { online: false, server: null };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при проверке сервера игрока:', error);
|
||||||
|
return { online: false, server: null };
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user