diff --git a/package-lock.json b/package-lock.json index bc047c8..fe08761 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "@mui/material": "^7.2.0", "@xmcl/core": "^2.14.1", "@xmcl/installer": "^6.1.0", + "@xmcl/model": "^2.0.4", + "@xmcl/resourcepack": "^1.2.4", "@xmcl/user": "^4.2.0", "electron-debug": "^4.1.0", "electron-log": "^5.3.2", @@ -25,7 +27,9 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.3.0", + "skinview3d": "^3.4.1", "stream-browserify": "^3.0.0", + "three": "^0.178.0", "undici": "^7.11.0", "util": "^0.12.5", "uuid": "^11.1.0" @@ -43,6 +47,7 @@ "@types/react": "^19.0.11", "@types/react-dom": "^19.0.4", "@types/react-test-renderer": "^19.0.0", + "@types/three": "^0.178.1", "@types/webpack-bundle-analyzer": "^4.7.0", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", @@ -2228,6 +2233,13 @@ "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": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -4897,6 +4909,13 @@ "dev": true, "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": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -5418,6 +5437,28 @@ "dev": true, "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": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -5445,6 +5486,12 @@ "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": { "version": "8.5.12", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", @@ -5856,6 +5903,13 @@ "@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": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", @@ -5988,6 +6042,70 @@ "node": ">=20.18.1" } }, + "node_modules/@xmcl/model": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@xmcl/model/-/model-2.0.4.tgz", + "integrity": "sha512-m671ny9FAaM0nF731Coq+F7N7ikQFCeJA+NhrXNcYkyclMXaqL6Q/kCrYpjhnbozMYVOblqu44QPSuEFqR/JHQ==", + "license": "MIT", + "dependencies": { + "@types/three": "^0.150.0", + "@xmcl/resourcepack": "1.2.4", + "three": "0.156.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@xmcl/model/node_modules/@types/three": { + "version": "0.150.2", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.150.2.tgz", + "integrity": "sha512-cvcz/81Mmj4oiAA+uxzwaRK3t8lYw8WxejXKqIBfu6PqvwSAEEiCi3VfCiVY18UflBqL0LDX/za85+sfqjMoIw==", + "license": "MIT", + "dependencies": { + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.6.9", + "lil-gui": "~0.17.0" + } + }, + "node_modules/@xmcl/model/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, + "node_modules/@xmcl/model/node_modules/three": { + "version": "0.156.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.156.1.tgz", + "integrity": "sha512-kP7H0FK9d/k6t/XvQ9FO6i+QrePoDcNhwl0I02+wmUJRNSLCUIDMcfObnzQvxb37/0Uc9TDT0T1HgsRRrO6SYQ==", + "license": "MIT" + }, + "node_modules/@xmcl/resourcepack": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@xmcl/resourcepack/-/resourcepack-1.2.4.tgz", + "integrity": "sha512-OlDOBAX33EKHC0PYC68a6RW/mBtfmskl0OKbsP8gSPM7RH7zqR2SNR+u5AAavEhpmCvjThztv6KwHifGk6/t/Q==", + "license": "MIT", + "dependencies": { + "@xmcl/system": "2.2.8" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@xmcl/system": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@xmcl/system/-/system-2.2.8.tgz", + "integrity": "sha512-G5argPsvKqvYDfUE1z+pVCIuNbhuaC+YXWlQHHXgMSpKSDnJRQoYvlQAcTorlFCqFqtL5a51hV+Rmsug0geJuA==", + "license": "MIT", + "dependencies": { + "@xmcl/unzip": "2.1.2", + "jszip": "^3.10.1", + "yauzl": "^2.10.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@xmcl/task": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@xmcl/task/-/task-4.1.1.tgz", @@ -8435,7 +8553,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, "license": "MIT" }, "node_modules/cosmiconfig": { @@ -11943,6 +12060,13 @@ "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": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -13302,7 +13426,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true, "license": "MIT" }, "node_modules/immutable": { @@ -15358,7 +15481,6 @@ "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dev": true, "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", @@ -15371,14 +15493,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, "license": "MIT" }, "node_modules/jszip/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -15394,14 +15514,12 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/jszip/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -15572,12 +15690,17 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, "license": "MIT", "dependencies": { "immediate": "~3.0.5" } }, + "node_modules/lil-gui": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.17.0.tgz", + "integrity": "sha512-MVBHmgY+uEbmJNApAaPbtvNh1RCAeMnKym82SBjtp5rODTYKWtM+MXHCifLe2H2Ti1HuBGBtK/5SyG4ShQ3pUQ==", + "license": "MIT" + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -15959,6 +16082,12 @@ "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -16939,7 +17068,6 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true, "license": "(MIT AND Zlib)" }, "node_modules/param-case": { @@ -17921,7 +18049,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "license": "MIT" }, "node_modules/progress": { @@ -19482,7 +19609,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, "license": "MIT" }, "node_modules/setprototypeof": { @@ -19656,6 +19782,47 @@ "dev": true, "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -20603,6 +20770,12 @@ "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", diff --git a/package.json b/package.json index b7d5feb..6806849 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,8 @@ "@mui/material": "^7.2.0", "@xmcl/core": "^2.14.1", "@xmcl/installer": "^6.1.0", + "@xmcl/model": "^2.0.4", + "@xmcl/resourcepack": "^1.2.4", "@xmcl/user": "^4.2.0", "electron-debug": "^4.1.0", "electron-log": "^5.3.2", @@ -119,7 +121,9 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.3.0", + "skinview3d": "^3.4.1", "stream-browserify": "^3.0.0", + "three": "^0.178.0", "undici": "^7.11.0", "util": "^0.12.5", "uuid": "^11.1.0" @@ -137,6 +141,7 @@ "@types/react": "^19.0.11", "@types/react-dom": "^19.0.4", "@types/react-test-renderer": "^19.0.0", + "@types/three": "^0.178.1", "@types/webpack-bundle-analyzer": "^4.7.0", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", diff --git a/src/main/minecraft-launcher.ts b/src/main/minecraft-launcher.ts index 7a792e5..d154052 100644 --- a/src/main/minecraft-launcher.ts +++ b/src/main/minecraft-launcher.ts @@ -16,6 +16,7 @@ import { } from '@xmcl/installer'; import { spawn } from 'child_process'; import { AuthService } from './auth-service'; +import { API_BASE_URL } from '../renderer/api'; // Константы const AUTHLIB_INJECTOR_FILENAME = 'authlib-injector-1.2.5.jar'; @@ -740,7 +741,7 @@ export function initMinecraftHandlers() { server: serverConfig, // Используем созданный объект конфигурации extraJVMArgs: [ '-Dlog4j2.formatMsgNoLookups=true', - `-javaagent:${authlibPath}=http://147.78.65.214:8000`, + `-javaagent:${authlibPath}=${API_BASE_URL}`, `-Xmx${memory}M`, '-Dauthlibinjector.skinWhitelist=127.0.0.1,falrfg-213-87-196-173.ru.tuna.am', '-Dauthlibinjector.debug=verbose,authlib', diff --git a/src/renderer/App.css b/src/renderer/App.css index fce6396..b8b074f 100644 --- a/src/renderer/App.css +++ b/src/renderer/App.css @@ -53,3 +53,7 @@ h5 { h6 { font-family: 'Benzin-Bold' !important; } + +span { + font-family: 'Benzin-Bold' !important; +} diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index fe9031f..b84ffad 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -14,6 +14,8 @@ import { Box } from '@mui/material'; import MinecraftBackground from './components/MinecraftBackground'; import { Notifier } from './components/Notifier'; import { VersionsExplorer } from './pages/VersionsExplorer'; +import Profile from './pages/Profile'; +import Shop from './pages/Shop'; const AuthCheck = ({ children }: { children: ReactNode }) => { const [isAuthenticated, setIsAuthenticated] = useState(null); @@ -120,7 +122,7 @@ const App = () => { }} > - + } /> @@ -140,6 +142,22 @@ const App = () => { } /> + + + + } + /> + + + + } + /> diff --git a/src/renderer/api.ts b/src/renderer/api.ts new file mode 100644 index 0000000..e8a98b8 --- /dev/null +++ b/src/renderer/api.ts @@ -0,0 +1,127 @@ +export const API_BASE_URL = 'http://147.78.65.214:8000'; + +export interface Player { + uuid: string; + username: string; + skin_url: string; + cloak_url: string; + coins: number; + is_active: boolean; + created_at: string; +} + +export interface CoinsResponse { + username: string; + coins: number; + total_time_played: { + seconds: number; + formatted: string; + }; +} + +export interface ApiError { + message: string; + details?: string; +} + +// Получение информации о игроке +export async function fetchPlayer(uuid: string): Promise { + 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 { + try { + const response = await fetch(`${API_BASE_URL}/users/${username}/coins`); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Ошибка получения данных игрока'); + } + return await response.json(); + } catch (error) { + console.error('API ошибка:', error); + throw error; + } +} + +// Загрузка скина +export async function uploadSkin( + username: string, + skinFile: File, + skinModel: string, +): Promise { + 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 { + 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 || 'Не удалось загрузить плащ'); + } +} diff --git a/src/renderer/components/SkinViewer.tsx b/src/renderer/components/SkinViewer.tsx new file mode 100644 index 0000000..faeba91 --- /dev/null +++ b/src/renderer/components/SkinViewer.tsx @@ -0,0 +1,72 @@ +import { useEffect, useRef } from 'react'; + +interface SkinViewerProps { + width?: number; + height?: number; + skinUrl?: string; + capeUrl?: string; + walkingSpeed?: number; + autoRotate?: boolean; +} + +export default function SkinViewer({ + width = 300, + height = 400, + skinUrl, + capeUrl, + walkingSpeed = 0.5, + autoRotate = true, +}: SkinViewerProps) { + const canvasRef = useRef(null); + const viewerRef = useRef(null); + + useEffect(() => { + if (!canvasRef.current) return; + + // Используем динамический импорт для обхода проблемы ESM/CommonJS + const initSkinViewer = async () => { + try { + const skinview3d = await import('skinview3d'); + + // Создаем просмотрщик скина по документации + const viewer = new skinview3d.SkinViewer({ + canvas: canvasRef.current, + width, + height, + skin: skinUrl || undefined, + cape: capeUrl || undefined, + }); + + // Настраиваем вращение + viewer.autoRotate = autoRotate; + + // Настраиваем анимацию ходьбы + viewer.animation = new skinview3d.WalkingAnimation(); + viewer.animation.speed = walkingSpeed; + + // Сохраняем экземпляр для очистки + viewerRef.current = viewer; + } catch (error) { + console.error('Ошибка при инициализации skinview3d:', error); + } + }; + + initSkinViewer(); + + // Очистка при размонтировании + return () => { + if (viewerRef.current) { + viewerRef.current.dispose(); + } + }; + }, [width, height, skinUrl, capeUrl, walkingSpeed, autoRotate]); + + return ( + + ); +} diff --git a/src/renderer/components/TopBar.tsx b/src/renderer/components/TopBar.tsx index 0254844..776a9c1 100644 --- a/src/renderer/components/TopBar.tsx +++ b/src/renderer/components/TopBar.tsx @@ -1,9 +1,10 @@ -import { Box, Button, Typography } from '@mui/material'; +import { Box, Button, Tab, Tabs, Typography } from '@mui/material'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import { useLocation, useNavigate } 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 { interface Window { @@ -23,15 +24,6 @@ interface TopBarProps { username?: string; } -interface CoinsResponse { - username: string; - coins: number; - total_time_played: { - seconds: number; - formatted: string; - }; -} - export default function TopBar({ onRegister, username }: TopBarProps) { // Получаем текущий путь const location = useLocation(); @@ -40,6 +32,18 @@ export default function TopBar({ onRegister, username }: TopBarProps) { const isVersionsExplorerPage = location.pathname.startsWith('/'); const navigate = useNavigate(); const [coins, setCoins] = useState(0); + const [value, setValue] = useState(0); + + const handleChange = (event: React.SyntheticEvent, newValue: number) => { + setValue(newValue); + if (newValue === 0) { + navigate('/'); + } else if (newValue === 1) { + navigate('/profile'); + } else if (newValue === 2) { + navigate('/shop'); + } + }; const handleLaunchPage = () => { navigate('/'); @@ -59,17 +63,12 @@ export default function TopBar({ onRegister, username }: TopBarProps) { }; // Функция для получения количества монет - const fetchCoins = async () => { + const fetchCoinsData = async () => { if (!username) return; try { - const response = await fetch( - `http://147.78.65.214:8000/users/${username}/coins`, - ); - if (response.ok) { - const data: CoinsResponse = await response.json(); - setCoins(data.coins); - } + const coinsData = await fetchCoins(username); + setCoins(coinsData.coins); } catch (error) { console.error('Ошибка при получении количества монет:', error); } @@ -77,8 +76,8 @@ export default function TopBar({ onRegister, username }: TopBarProps) { useEffect(() => { if (username) { - fetchCoins(); - const intervalId = setInterval(fetchCoins, 60000); + fetchCoinsData(); + const intervalId = setInterval(fetchCoinsData, 60000); return () => clearInterval(intervalId); } }, [username]); @@ -130,6 +129,28 @@ export default function TopBar({ onRegister, username }: TopBarProps) { )} + {!isLaunchPage && ( + + + + + + + + )} {/* Центр */} (null); + const [walkingSpeed, setWalkingSpeed] = useState(0.5); + const [skin, setSkin] = useState(''); + const [cape, setCape] = useState(''); + const [username, setUsername] = useState(''); + const [skinFile, setSkinFile] = useState(null); + const [skinModel, setSkinModel] = useState(''); // slim или classic + const [uploadStatus, setUploadStatus] = useState< + 'idle' | 'loading' | 'success' | 'error' + >('idle'); + const [statusMessage, setStatusMessage] = useState(''); + const [isDragOver, setIsDragOver] = useState(false); + + useEffect(() => { + const savedConfig = localStorage.getItem('launcher_config'); + if (savedConfig) { + const config = JSON.parse(savedConfig); + if (config.uuid) { + loadPlayerData(config.uuid); + setUsername(config.username || ''); + } + } + }, []); + + const loadPlayerData = async (uuid: string) => { + try { + const player = await fetchPlayer(uuid); + setSkin(player.skin_url); + setCape(player.cloak_url); + } catch (error) { + console.error('Ошибка при получении данных игрока:', error); + setSkin(''); + setCape(''); + } + }; + + // Обработка перетаскивания файла + const handleFileDrop = (e: React.DragEvent) => { + 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) => { + if (e.target.files && e.target.files.length > 0) { + const file = e.target.files[0]; + if (file.type === 'image/png') { + setSkinFile(file); + setStatusMessage(`Файл "${file.name}" готов к загрузке`); + } else { + setStatusMessage('Пожалуйста, выберите файл в формате PNG'); + setUploadStatus('error'); + } + } + }; + + // Отправка запроса на установку скина + const handleUploadSkin = async () => { + if (!skinFile || !username) { + setStatusMessage('Необходимо выбрать файл и указать имя пользователя'); + setUploadStatus('error'); + return; + } + + setUploadStatus('loading'); + + try { + await uploadSkin(username, skinFile, skinModel); + + setStatusMessage('Скин успешно загружен!'); + setUploadStatus('success'); + + // Обновляем информацию о игроке, чтобы увидеть новый скин + const config = JSON.parse( + localStorage.getItem('launcher_config') || '{}', + ); + if (config.uuid) { + loadPlayerData(config.uuid); + } + } catch (error) { + setStatusMessage( + `Ошибка: ${error instanceof Error ? error.message : 'Не удалось загрузить скин'}`, + ); + setUploadStatus('error'); + } + }; + + return ( + + + {/* Используем переработанный компонент SkinViewer */} + + + + + + Установить скин + + + { + e.preventDefault(); + setIsDragOver(true); + }} + onDragLeave={() => setIsDragOver(false)} + onDrop={handleFileDrop} + onClick={() => fileInputRef.current?.click()} + > + + + {skinFile + ? `Выбран файл: ${skinFile.name}` + : 'Перетащите PNG файл скина или кликните для выбора'} + + + + + Модель скина + + + + {uploadStatus === 'error' && ( + + {statusMessage} + + )} + + {uploadStatus === 'success' && ( + + {statusMessage} + + )} + + + + + ); +} diff --git a/src/renderer/pages/Shop.tsx b/src/renderer/pages/Shop.tsx new file mode 100644 index 0000000..8bd2459 --- /dev/null +++ b/src/renderer/pages/Shop.tsx @@ -0,0 +1,3 @@ +export default function Shop() { + return
Shop
; +}