Compare commits
16 Commits
1.0.0
...
31a26dc1ce
Author | SHA1 | Date | |
---|---|---|---|
31a26dc1ce | |||
5cd483209f | |||
1b50a7d4e4 | |||
b14de1d15a | |||
7eaf7a7610 | |||
76917e3f90 | |||
261b9ac253 | |||
2eda1d7806 | |||
1b496288de | |||
ff91303b18 | |||
8fa6956095 | |||
6f92b2acad | |||
b65b9538bb | |||
4717132b05 | |||
12f7ea8d1c | |||
e21a51482a |
1
.cursorignore
Normal file
1
.cursorignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
@ -1,7 +1,3 @@
|
|||||||
/**
|
|
||||||
* Base webpack config used across other specific configs
|
|
||||||
*/
|
|
||||||
|
|
||||||
import webpack from 'webpack';
|
import webpack from 'webpack';
|
||||||
import TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin';
|
import TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin';
|
||||||
import webpackPaths from './webpack.paths';
|
import webpackPaths from './webpack.paths';
|
||||||
@ -20,7 +16,6 @@ const configuration: webpack.Configuration = {
|
|||||||
use: {
|
use: {
|
||||||
loader: 'ts-loader',
|
loader: 'ts-loader',
|
||||||
options: {
|
options: {
|
||||||
// Remove this line to enable type checking in webpack builds
|
|
||||||
transpileOnly: true,
|
transpileOnly: true,
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
module: 'nodenext',
|
module: 'nodenext',
|
||||||
@ -34,18 +29,22 @@ const configuration: webpack.Configuration = {
|
|||||||
|
|
||||||
output: {
|
output: {
|
||||||
path: webpackPaths.srcPath,
|
path: webpackPaths.srcPath,
|
||||||
// https://github.com/webpack/webpack/issues/1114
|
|
||||||
library: { type: 'commonjs2' },
|
library: { type: 'commonjs2' },
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the array of extensions that should be used to resolve modules.
|
|
||||||
*/
|
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
|
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
|
||||||
modules: [webpackPaths.srcPath, 'node_modules'],
|
modules: [webpackPaths.srcPath, 'node_modules'],
|
||||||
// There is no need to add aliases here, the paths in tsconfig get mirrored
|
|
||||||
plugins: [new TsconfigPathsPlugins()],
|
plugins: [new TsconfigPathsPlugins()],
|
||||||
|
|
||||||
|
// Новые настройки
|
||||||
|
extensionAlias: {
|
||||||
|
'.js': ['.js', '.mjs'],
|
||||||
|
},
|
||||||
|
fullySpecified: false,
|
||||||
|
alias: {
|
||||||
|
'undici/lib/core/util': 'undici/lib/core/util.js',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins: [new webpack.EnvironmentPlugin({ NODE_ENV: 'production' })],
|
plugins: [new webpack.EnvironmentPlugin({ NODE_ENV: 'production' })],
|
||||||
|
@ -36,9 +36,15 @@ const configuration: webpack.Configuration = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
optimization: {
|
optimization: {
|
||||||
|
minimize: false,
|
||||||
minimizer: [
|
minimizer: [
|
||||||
new TerserPlugin({
|
new TerserPlugin({
|
||||||
parallel: true,
|
parallel: true,
|
||||||
|
terserOptions: {
|
||||||
|
ecma: 2020,
|
||||||
|
keep_classnames: true,
|
||||||
|
keep_fnames: true,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -27,3 +27,4 @@ npm-debug.log.*
|
|||||||
*.css.d.ts
|
*.css.d.ts
|
||||||
*.sass.d.ts
|
*.sass.d.ts
|
||||||
*.scss.d.ts
|
*.scss.d.ts
|
||||||
|
.env
|
||||||
|
0
assets/fonts/benzin-bold.eot
Normal file
0
assets/fonts/benzin-bold.eot
Normal file
1745
assets/fonts/benzin-bold.svg
Normal file
1745
assets/fonts/benzin-bold.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 221 KiB |
BIN
assets/fonts/benzin-bold.ttf
Normal file
BIN
assets/fonts/benzin-bold.ttf
Normal file
Binary file not shown.
0
assets/fonts/benzin-bold.woff
Normal file
0
assets/fonts/benzin-bold.woff
Normal file
BIN
assets/fonts/benzin-bold.woff2
Normal file
BIN
assets/fonts/benzin-bold.woff2
Normal file
Binary file not shown.
18
assets/images/heart.svg
Normal file
18
assets/images/heart.svg
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<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="8" y="4" width="4" height="16" fill="#FF2D0F"/>
|
||||||
|
<rect x="4" y="8" 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="16" width="8" height="16" fill="#FF2D0F"/>
|
||||||
|
<rect x="16" y="16" width="4" height="4" fill="#FF2D0F"/>
|
||||||
|
<rect x="24" y="12" 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="12" y="24" width="4" height="4" fill="#BD2211"/>
|
||||||
|
<rect x="8" y="20" 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 y="12" width="4" height="4" fill="#BD2211"/>
|
||||||
|
<rect x="12" y="4" width="4" height="20" fill="#FF2D0F"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 994 B |
1230
package-lock.json
generated
1230
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/electron-react-boilerplate/electron-react-boilerplate.git"
|
"url": "git+https://git.popa-popa.ru/DIKER/popa-launcher.git"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
@ -42,14 +42,15 @@
|
|||||||
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll",
|
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll",
|
||||||
"lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx",
|
"lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
"lint:fix": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
"lint:fix": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||||
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never && npm run build:dll",
|
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish always && npm run build:dll",
|
||||||
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
|
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
|
||||||
"prestart": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack --config ./.erb/configs/webpack.config.main.dev.ts",
|
"prestart": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack --config ./.erb/configs/webpack.config.main.dev.ts",
|
||||||
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run prestart && npm run start:renderer",
|
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run prestart && npm run start:renderer",
|
||||||
"start:main": "concurrently -k -P \"cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --watch --config ./.erb/configs/webpack.config.main.dev.ts\" \"electronmon . -- {@}\" --",
|
"start:main": "concurrently -k -P \"cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --watch --config ./.erb/configs/webpack.config.main.dev.ts\" \"electronmon . -- {@}\" --",
|
||||||
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
|
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
|
||||||
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
|
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
|
||||||
"test": "jest"
|
"test": "jest",
|
||||||
|
"publish-debug": "electron-builder build --publish always"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"extends browserslist-config-erb"
|
"extends browserslist-config-erb"
|
||||||
@ -102,17 +103,32 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/notarize": "^3.0.0",
|
"@electron/notarize": "^3.0.0",
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@mui/icons-material": "^7.2.0",
|
||||||
|
"@mui/material": "^7.2.0",
|
||||||
|
"@xmcl/core": "^2.14.1",
|
||||||
|
"@xmcl/installer": "^6.1.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",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
|
"find-java-home": "^2.0.0",
|
||||||
|
"https-browserify": "^1.0.0",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
"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",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
|
"undici": "^7.11.0",
|
||||||
|
"util": "^0.12.5",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron/rebuild": "^3.7.1",
|
"@electron/rebuild": "^3.7.1",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
|
"@swc/core": "^1.12.9",
|
||||||
"@teamsupercell/typings-for-css-modules-loader": "^2.5.2",
|
"@teamsupercell/typings-for-css-modules-loader": "^2.5.2",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
@ -175,7 +191,7 @@
|
|||||||
"webpack-merge": "^6.0.1"
|
"webpack-merge": "^6.0.1"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"productName": "ElectronReact",
|
"productName": "popa-launcher",
|
||||||
"appId": "org.erb.ElectronReact",
|
"appId": "org.erb.ElectronReact",
|
||||||
"asar": true,
|
"asar": true,
|
||||||
"afterSign": ".erb/scripts/notarize.js",
|
"afterSign": ".erb/scripts/notarize.js",
|
||||||
@ -234,9 +250,12 @@
|
|||||||
"./assets/**"
|
"./assets/**"
|
||||||
],
|
],
|
||||||
"publish": {
|
"publish": {
|
||||||
"provider": "github",
|
"provider": "generic",
|
||||||
"owner": "electron-react-boilerplate",
|
"url": "https://git.popa-popa.ru/DIKER/popa-launcher/releases/download/v${version}",
|
||||||
"repo": "electron-react-boilerplate"
|
"channel": "latest",
|
||||||
|
"requestHeaders": {
|
||||||
|
"Authorization": "token ${env.GH_TOKEN}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"collective": {
|
"collective": {
|
||||||
|
8
release/app/package-lock.json
generated
8
release/app/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "electron-react-boilerplate",
|
"name": "popa-launcher",
|
||||||
"version": "4.6.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "electron-react-boilerplate",
|
"name": "popa-launcher",
|
||||||
"version": "4.6.0",
|
"version": "1.0.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "electron-react-boilerplate",
|
"name": "popa-launcher",
|
||||||
"version": "4.6.0",
|
"version": "1.0.0",
|
||||||
"description": "A foundation for scalable desktop apps",
|
"description": "Popa Launcher",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Electron React Boilerplate Maintainers",
|
"name": "DIKER",
|
||||||
"email": "electronreactboilerplate@gmail.com",
|
"email": "diker0k@gmail.com",
|
||||||
"url": "https://github.com/electron-react-boilerplate"
|
"url": "https://github.com/DIKER0K"
|
||||||
},
|
},
|
||||||
"main": "./dist/main/main.js",
|
"main": "./dist/main/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
90
src/main/auth-service.ts
Normal file
90
src/main/auth-service.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { YggdrasilClient, YggrasilAuthentication } from '@xmcl/user';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
// Ely.by сервер
|
||||||
|
const ELY_BY_AUTH_SERVER = 'https://authserver.ely.by';
|
||||||
|
|
||||||
|
export class AuthService {
|
||||||
|
private client: YggdrasilClient;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.client = new YggdrasilClient(ELY_BY_AUTH_SERVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<YggrasilAuthentication> {
|
||||||
|
try {
|
||||||
|
// Генерируем уникальный clientToken
|
||||||
|
const clientToken = uuidv4();
|
||||||
|
|
||||||
|
// Выполняем запрос напрямую к правильному URL
|
||||||
|
const response = await fetch(`${ELY_BY_AUTH_SERVER}/auth/authenticate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
clientToken,
|
||||||
|
requestUser: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Ошибка авторизации: ${response.status} ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth = await response.json();
|
||||||
|
console.log(`Аутентификация успешна для ${auth.selectedProfile?.name}`);
|
||||||
|
return auth;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при авторизации:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(accessToken: string, clientToken: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${ELY_BY_AUTH_SERVER}/auth/validate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ accessToken, clientToken }),
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при валидации токена:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh(
|
||||||
|
accessToken: string,
|
||||||
|
clientToken: string,
|
||||||
|
): Promise<YggrasilAuthentication | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${ELY_BY_AUTH_SERVER}/auth/refresh`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ accessToken, clientToken }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при обновлении токена:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,12 +14,47 @@ import { autoUpdater } from 'electron-updater';
|
|||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
import MenuBuilder from './menu';
|
import MenuBuilder from './menu';
|
||||||
import { resolveHtmlPath } from './util';
|
import { resolveHtmlPath } from './util';
|
||||||
|
import {
|
||||||
|
initMinecraftHandlers,
|
||||||
|
initAuthHandlers,
|
||||||
|
initServerStatusHandler,
|
||||||
|
initPackConfigHandlers,
|
||||||
|
} from './minecraft-launcher';
|
||||||
|
|
||||||
class AppUpdater {
|
class AppUpdater {
|
||||||
constructor() {
|
constructor() {
|
||||||
log.transports.file.level = 'info';
|
log.transports.file.level = 'info';
|
||||||
autoUpdater.logger = log;
|
autoUpdater.logger = log;
|
||||||
|
|
||||||
|
const server = 'https://git.popa-popa.ru/DIKER/popa-launcher';
|
||||||
|
|
||||||
|
// Для Gitea нужно указать конкретную структуру URL
|
||||||
|
// Обратите внимание на использование пути /download/
|
||||||
|
autoUpdater.setFeedURL({
|
||||||
|
provider: 'generic',
|
||||||
|
url: `${server}/releases/download/latest`, // Укажите конкретную версию
|
||||||
|
channel: 'latest',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверка обновлений
|
||||||
autoUpdater.checkForUpdatesAndNotify();
|
autoUpdater.checkForUpdatesAndNotify();
|
||||||
|
|
||||||
|
// Периодическая проверка обновлений (каждый час)
|
||||||
|
setInterval(
|
||||||
|
() => {
|
||||||
|
autoUpdater.checkForUpdatesAndNotify();
|
||||||
|
},
|
||||||
|
60 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Обработчики событий обновления
|
||||||
|
autoUpdater.on('update-downloaded', () => {
|
||||||
|
log.info('Обновление загружено. Будет установлено при перезапуске.');
|
||||||
|
// Можно отправить событие в renderer для уведомления пользователя
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('update-available');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +78,16 @@ if (isDebug) {
|
|||||||
require('electron-debug').default();
|
require('electron-debug').default();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('close-app', () => {
|
||||||
|
app.quit();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('minimize-app', () => {
|
||||||
|
mainWindow?.minimize();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const installExtensions = async () => {
|
const installExtensions = async () => {
|
||||||
const installer = require('electron-devtools-installer');
|
const installer = require('electron-devtools-installer');
|
||||||
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
|
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
|
||||||
@ -72,9 +117,13 @@ const createWindow = async () => {
|
|||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
show: false,
|
show: false,
|
||||||
width: 1024,
|
width: 1024,
|
||||||
height: 728,
|
height: 850,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
resizable: false,
|
||||||
|
frame: false,
|
||||||
icon: getAssetPath('icon.png'),
|
icon: getAssetPath('icon.png'),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
|
webSecurity: false,
|
||||||
preload: app.isPackaged
|
preload: app.isPackaged
|
||||||
? path.join(__dirname, 'preload.js')
|
? path.join(__dirname, 'preload.js')
|
||||||
: path.join(__dirname, '../../.erb/dll/preload.js'),
|
: path.join(__dirname, '../../.erb/dll/preload.js'),
|
||||||
@ -110,6 +159,11 @@ const createWindow = async () => {
|
|||||||
// Remove this if your app does not use auto updates
|
// Remove this if your app does not use auto updates
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
new AppUpdater();
|
new AppUpdater();
|
||||||
|
|
||||||
|
initAuthHandlers();
|
||||||
|
initMinecraftHandlers();
|
||||||
|
initServerStatusHandler();
|
||||||
|
initPackConfigHandlers();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -135,3 +189,7 @@ app
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(console.log);
|
.catch(console.log);
|
||||||
|
|
||||||
|
ipcMain.handle('install-update', () => {
|
||||||
|
autoUpdater.quitAndInstall();
|
||||||
|
});
|
||||||
|
981
src/main/minecraft-launcher.ts
Normal file
981
src/main/minecraft-launcher.ts
Normal file
@ -0,0 +1,981 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { app, ipcMain } from 'electron';
|
||||||
|
import fs from 'fs';
|
||||||
|
import https from 'https';
|
||||||
|
import extract from 'extract-zip';
|
||||||
|
import { launch, Version, diagnose } from '@xmcl/core';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import {
|
||||||
|
installDependencies,
|
||||||
|
installFabric,
|
||||||
|
getFabricLoaders,
|
||||||
|
getVersionList,
|
||||||
|
install,
|
||||||
|
installTask,
|
||||||
|
installDependenciesTask,
|
||||||
|
} from '@xmcl/installer';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { AuthService } from './auth-service';
|
||||||
|
|
||||||
|
// Константы
|
||||||
|
const AUTHLIB_INJECTOR_FILENAME = 'authlib-injector-1.2.5.jar';
|
||||||
|
const MCSTATUS_API_URL = 'https://api.mcstatus.io/v2/status/java/';
|
||||||
|
|
||||||
|
// Создаем экземпляр сервиса аутентификации
|
||||||
|
const authService = new AuthService();
|
||||||
|
|
||||||
|
// Модифицированная функция для получения последней версии релиза с произвольного URL
|
||||||
|
export async function getLatestReleaseVersion(apiUrl: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiUrl);
|
||||||
|
const data = await response.json();
|
||||||
|
return data.tag_name || '0.0.0';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch latest version:', error);
|
||||||
|
return '0.0.0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для загрузки файла
|
||||||
|
export async function downloadFile(
|
||||||
|
url: string,
|
||||||
|
dest: string,
|
||||||
|
progressCallback: (progress: number) => void,
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const file = fs.createWriteStream(dest);
|
||||||
|
let downloadedSize = 0;
|
||||||
|
let totalSize = 0;
|
||||||
|
let redirectCount = 0;
|
||||||
|
|
||||||
|
const makeRequest = (requestUrl: string) => {
|
||||||
|
https
|
||||||
|
.get(requestUrl, (response) => {
|
||||||
|
// Обрабатываем редиректы
|
||||||
|
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||||
|
if (redirectCount++ > 5) {
|
||||||
|
reject(new Error('Too many redirects'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
makeRequest(response.headers.location!);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
reject(new Error(`Server returned ${response.statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSize = parseInt(response.headers['content-length'] || '0', 10);
|
||||||
|
|
||||||
|
response.on('data', (chunk) => {
|
||||||
|
downloadedSize += chunk.length;
|
||||||
|
const progress = Math.round((downloadedSize / totalSize) * 100);
|
||||||
|
progressCallback(progress);
|
||||||
|
});
|
||||||
|
|
||||||
|
response.pipe(file);
|
||||||
|
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close();
|
||||||
|
// Проверяем, что файл скачан полностью
|
||||||
|
if (downloadedSize !== totalSize && totalSize > 0) {
|
||||||
|
fs.unlink(dest, () => {
|
||||||
|
reject(new Error('File download incomplete'));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
fs.unlink(dest, () => reject(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
makeRequest(url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавим функцию для определения версии Java
|
||||||
|
export async function getJavaVersion(
|
||||||
|
javaPath: string,
|
||||||
|
): Promise<{ version: string; majorVersion: number }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const process = spawn(javaPath, ['-version']);
|
||||||
|
let output = '';
|
||||||
|
|
||||||
|
process.stderr.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
// Извлекаем версию из вывода (например, "java version "1.8.0_291"")
|
||||||
|
const versionMatch = output.match(/version "([^"]+)"/);
|
||||||
|
if (versionMatch) {
|
||||||
|
const version = versionMatch[1];
|
||||||
|
// Определяем major версию
|
||||||
|
let majorVersion = 8; // По умолчанию предполагаем Java 8
|
||||||
|
|
||||||
|
if (version.startsWith('1.8')) {
|
||||||
|
majorVersion = 8;
|
||||||
|
} else if (version.match(/^(9|10|11|12|13|14|15|16)/)) {
|
||||||
|
majorVersion = parseInt(version.split('.')[0], 10);
|
||||||
|
} else if (version.match(/^1\.(9|10|11|12|13|14|15|16)/)) {
|
||||||
|
majorVersion = parseInt(version.split('.')[1], 10);
|
||||||
|
} else if (version.match(/^([0-9]+)/)) {
|
||||||
|
majorVersion = parseInt(version.match(/^([0-9]+)/)?.[1] || '0', 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ version, majorVersion });
|
||||||
|
} else {
|
||||||
|
reject(new Error('Unable to parse Java version'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Java process exited with code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Модифицируем функцию findJava, чтобы она находила все версии Java и определяла их версии
|
||||||
|
export async function findJavaVersions(): Promise<
|
||||||
|
Array<{ path: string; version: string; majorVersion: number }>
|
||||||
|
> {
|
||||||
|
const javaPaths: string[] = [];
|
||||||
|
const results: Array<{
|
||||||
|
path: string;
|
||||||
|
version: string;
|
||||||
|
majorVersion: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Сначала проверяем переменную JAVA_HOME
|
||||||
|
if (process.env.JAVA_HOME) {
|
||||||
|
const javaPath = path.join(
|
||||||
|
process.env.JAVA_HOME,
|
||||||
|
'bin',
|
||||||
|
'java' + (process.platform === 'win32' ? '.exe' : ''),
|
||||||
|
);
|
||||||
|
if (fs.existsSync(javaPath)) {
|
||||||
|
javaPaths.push(javaPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Проверяем стандартные пути установки в зависимости от платформы
|
||||||
|
const checkPaths: string[] = [];
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// Windows
|
||||||
|
const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files';
|
||||||
|
const programFilesX86 =
|
||||||
|
process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
||||||
|
|
||||||
|
// JDK пути
|
||||||
|
[
|
||||||
|
'Java',
|
||||||
|
'AdoptOpenJDK',
|
||||||
|
'Eclipse Adoptium',
|
||||||
|
'BellSoft',
|
||||||
|
'Zulu',
|
||||||
|
'Amazon Corretto',
|
||||||
|
'Microsoft',
|
||||||
|
].forEach((vendor) => {
|
||||||
|
checkPaths.push(path.join(programFiles, vendor));
|
||||||
|
checkPaths.push(path.join(programFilesX86, vendor));
|
||||||
|
});
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
// macOS
|
||||||
|
checkPaths.push('/Library/Java/JavaVirtualMachines');
|
||||||
|
checkPaths.push('/System/Library/Java/JavaVirtualMachines');
|
||||||
|
checkPaths.push('/usr/libexec/java_home');
|
||||||
|
} else {
|
||||||
|
// Linux
|
||||||
|
checkPaths.push('/usr/lib/jvm');
|
||||||
|
checkPaths.push('/usr/java');
|
||||||
|
checkPaths.push('/opt/java');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем каждый путь
|
||||||
|
for (const basePath of checkPaths) {
|
||||||
|
if (fs.existsSync(basePath)) {
|
||||||
|
try {
|
||||||
|
// Находим подпапки с JDK/JRE
|
||||||
|
const entries = fs.readdirSync(basePath, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
// Проверяем наличие исполняемого файла java в bin
|
||||||
|
const javaPath = path.join(
|
||||||
|
basePath,
|
||||||
|
entry.name,
|
||||||
|
'bin',
|
||||||
|
'java' + (process.platform === 'win32' ? '.exe' : ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fs.existsSync(javaPath)) {
|
||||||
|
javaPaths.push(javaPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Ошибка при сканировании ${basePath}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Пробуем найти java в PATH через команду which/where
|
||||||
|
try {
|
||||||
|
const command =
|
||||||
|
process.platform === 'win32' ? 'where java' : 'which java';
|
||||||
|
const javaPathFromCmd = execSync(command).toString().trim().split('\n');
|
||||||
|
|
||||||
|
javaPathFromCmd.forEach((path) => {
|
||||||
|
if (path && fs.existsSync(path) && !javaPaths.includes(path)) {
|
||||||
|
javaPaths.push(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка при поиске java через PATH:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем информацию о версиях найденных Java
|
||||||
|
for (const javaPath of javaPaths) {
|
||||||
|
try {
|
||||||
|
const versionInfo = await getJavaVersion(javaPath);
|
||||||
|
results.push({
|
||||||
|
path: javaPath,
|
||||||
|
version: versionInfo.version,
|
||||||
|
majorVersion: versionInfo.majorVersion,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Ошибка при определении версии для ${javaPath}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортируем результаты по majorVersion (от меньшей к большей)
|
||||||
|
return results.sort((a, b) => a.majorVersion - b.majorVersion);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при поиске версий Java:', error);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновим функцию findJava для использования Java 8
|
||||||
|
export async function findJava(): Promise<string> {
|
||||||
|
try {
|
||||||
|
console.log('Поиск доступных версий Java...');
|
||||||
|
|
||||||
|
const javaVersions = await findJavaVersions();
|
||||||
|
|
||||||
|
if (javaVersions.length === 0) {
|
||||||
|
throw new Error('Java не найдена. Установите Java и повторите попытку.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Найденные версии Java:');
|
||||||
|
javaVersions.forEach((java) => {
|
||||||
|
console.log(
|
||||||
|
`- Java ${java.majorVersion} (${java.version}) по пути: ${java.path}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Предпочитаем Java 21 или 17 для совместимости с authlib-injector
|
||||||
|
const preferredVersions = [21, 17, 11];
|
||||||
|
|
||||||
|
for (const preferredVersion of preferredVersions) {
|
||||||
|
const preferred = javaVersions.find(
|
||||||
|
(java) => java.majorVersion === preferredVersion,
|
||||||
|
);
|
||||||
|
if (preferred) {
|
||||||
|
console.log(
|
||||||
|
`Выбрана предпочтительная версия Java ${preferredVersion}: ${preferred.path}`,
|
||||||
|
);
|
||||||
|
return preferred.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не нашли предпочтительные версии, берем самую старую версию
|
||||||
|
console.log(`Выбрана доступная версия Java: ${javaVersions[0].path}`);
|
||||||
|
return javaVersions[0].path;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при поиске Java:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавим функцию для проверки/копирования authlib-injector (как в C#)
|
||||||
|
async function ensureAuthlibInjectorExists(appPath: string): Promise<string> {
|
||||||
|
const authlibPath = path.join(appPath, AUTHLIB_INJECTOR_FILENAME);
|
||||||
|
|
||||||
|
// Проверяем, существует ли файл
|
||||||
|
if (fs.existsSync(authlibPath)) {
|
||||||
|
console.log(
|
||||||
|
`Файл ${AUTHLIB_INJECTOR_FILENAME} уже существует: ${authlibPath}`,
|
||||||
|
);
|
||||||
|
return authlibPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем authlib в ресурсах приложения
|
||||||
|
const resourcePath = path.join(app.getAppPath(), AUTHLIB_INJECTOR_FILENAME);
|
||||||
|
|
||||||
|
if (fs.existsSync(resourcePath)) {
|
||||||
|
console.log(`Копирование ${AUTHLIB_INJECTOR_FILENAME} из ресурсов...`);
|
||||||
|
fs.copyFileSync(resourcePath, authlibPath);
|
||||||
|
console.log(`Файл успешно скопирован: ${authlibPath}`);
|
||||||
|
return authlibPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не нашли локальный файл - скачиваем его
|
||||||
|
console.log(`Скачивание ${AUTHLIB_INJECTOR_FILENAME}...`);
|
||||||
|
await downloadFile(
|
||||||
|
`https://github.com/yushijinhun/authlib-injector/releases/download/v1.2.5/${AUTHLIB_INJECTOR_FILENAME}`,
|
||||||
|
authlibPath,
|
||||||
|
(progress) => {
|
||||||
|
console.log(`Прогресс скачивания: ${progress}%`);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return authlibPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация IPC обработчиков
|
||||||
|
export function initMinecraftHandlers() {
|
||||||
|
// Обработчик для скачивания и распаковки
|
||||||
|
ipcMain.handle('download-and-extract', async (event, options) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
downloadUrl = 'https://github.com/DIKER0K/Comfort/releases/latest/download/Comfort.zip',
|
||||||
|
apiReleaseUrl = 'https://api.github.com/repos/DIKER0K/Comfort/releases/latest',
|
||||||
|
versionFileName = 'comfort_version.txt',
|
||||||
|
packName = 'Comfort',
|
||||||
|
preserveFiles = [], // Новый параметр: список файлов/папок для сохранения
|
||||||
|
} = options || {};
|
||||||
|
|
||||||
|
const appPath = path.dirname(app.getPath('exe'));
|
||||||
|
const minecraftDir = path.join(appPath, '.minecraft');
|
||||||
|
const versionsDir = path.join(minecraftDir, 'versions');
|
||||||
|
const versionFilePath = path.join(minecraftDir, versionFileName);
|
||||||
|
|
||||||
|
// Получаем текущую и последнюю версии
|
||||||
|
const latestVersion = await getLatestReleaseVersion(apiReleaseUrl);
|
||||||
|
let currentVersion = '';
|
||||||
|
|
||||||
|
// Проверяем текущую версию, если файл существует
|
||||||
|
if (fs.existsSync(versionFilePath)) {
|
||||||
|
currentVersion = fs.readFileSync(versionFilePath, 'utf-8').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, нужно ли обновление
|
||||||
|
if (currentVersion === latestVersion) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
updated: false,
|
||||||
|
version: currentVersion,
|
||||||
|
packName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempDir = path.join(appPath, 'temp');
|
||||||
|
const packDir = path.join(versionsDir, packName); // Директория пакета
|
||||||
|
|
||||||
|
// Создаем/очищаем временную директорию
|
||||||
|
if (fs.existsSync(tempDir)) {
|
||||||
|
fs.rmSync(tempDir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
const zipPath = path.join(tempDir, `${packName}.zip`);
|
||||||
|
|
||||||
|
// Скачиваем файл
|
||||||
|
await downloadFile(downloadUrl, zipPath, (progress) => {
|
||||||
|
event.sender.send('download-progress', progress);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем архив
|
||||||
|
const fileStats = fs.statSync(zipPath);
|
||||||
|
if (fileStats.size < 1024) {
|
||||||
|
throw new Error('Downloaded file is too small, likely corrupted');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем папку versions если её нет
|
||||||
|
if (!fs.existsSync(versionsDir)) {
|
||||||
|
fs.mkdirSync(versionsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем файлы/папки, которые нужно оставить
|
||||||
|
const backupDir = path.join(tempDir, 'backup');
|
||||||
|
fs.mkdirSync(backupDir, { recursive: true });
|
||||||
|
|
||||||
|
// Проверка и бэкап указанных файлов/папок
|
||||||
|
for (const filePath of preserveFiles) {
|
||||||
|
const fullPath = path.join(packDir, filePath);
|
||||||
|
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
const backupPath = path.join(backupDir, filePath);
|
||||||
|
|
||||||
|
// Создаем необходимые директории для бэкапа
|
||||||
|
const backupDirPath = path.dirname(backupPath);
|
||||||
|
if (!fs.existsSync(backupDirPath)) {
|
||||||
|
fs.mkdirSync(backupDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копируем файл или директорию
|
||||||
|
if (fs.lstatSync(fullPath).isDirectory()) {
|
||||||
|
fs.cpSync(fullPath, backupPath, { recursive: true });
|
||||||
|
console.log(`Директория ${filePath} сохранена во временный бэкап`);
|
||||||
|
} else {
|
||||||
|
fs.copyFileSync(fullPath, backupPath);
|
||||||
|
console.log(`Файл ${filePath} сохранен во временный бэкап`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Распаковываем архив напрямую в папку versions
|
||||||
|
await extract(zipPath, { dir: versionsDir });
|
||||||
|
fs.unlinkSync(zipPath);
|
||||||
|
|
||||||
|
// Восстанавливаем файлы/папки из бэкапа
|
||||||
|
for (const filePath of preserveFiles) {
|
||||||
|
const backupPath = path.join(backupDir, filePath);
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
const targetPath = path.join(packDir, filePath);
|
||||||
|
|
||||||
|
// Создаем необходимые директории для восстановления
|
||||||
|
const targetDirPath = path.dirname(targetPath);
|
||||||
|
if (!fs.existsSync(targetDirPath)) {
|
||||||
|
fs.mkdirSync(targetDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копируем обратно файл или директорию
|
||||||
|
if (fs.lstatSync(backupPath).isDirectory()) {
|
||||||
|
fs.cpSync(backupPath, targetPath, { recursive: true });
|
||||||
|
console.log(`Директория ${filePath} восстановлена из бэкапа`);
|
||||||
|
} else {
|
||||||
|
fs.copyFileSync(backupPath, targetPath);
|
||||||
|
console.log(`Файл ${filePath} восстановлен из бэкапа`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем новую версию
|
||||||
|
fs.writeFileSync(versionFilePath, latestVersion);
|
||||||
|
|
||||||
|
// Удаляем временную директорию
|
||||||
|
fs.rmSync(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
return { success: true, updated: true, version: latestVersion, packName };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in download-and-extract:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик для запуска Minecraft
|
||||||
|
ipcMain.handle('launch-minecraft', async (event, gameConfig) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
accessToken,
|
||||||
|
uuid,
|
||||||
|
username,
|
||||||
|
memory = 4096,
|
||||||
|
baseVersion = '1.21.4',
|
||||||
|
fabricVersion = 'fabric0.16.14',
|
||||||
|
packName = 'Comfort', // Название основной сборки
|
||||||
|
versionToLaunchOverride = '', // Возможность переопределить версию для запуска
|
||||||
|
serverIp = 'popa-popa.ru',
|
||||||
|
serverPort, // Добавляем опциональный порт без значения по умолчанию
|
||||||
|
} = gameConfig || {};
|
||||||
|
|
||||||
|
const appPath = path.dirname(app.getPath('exe'));
|
||||||
|
const minecraftDir = path.join(appPath, '.minecraft');
|
||||||
|
const versionsDir = path.join(minecraftDir, 'versions');
|
||||||
|
|
||||||
|
// Определяем версию для запуска
|
||||||
|
const versionsContents = fs.existsSync(versionsDir)
|
||||||
|
? fs.readdirSync(versionsDir)
|
||||||
|
: [];
|
||||||
|
console.log('Доступные версии:', versionsContents);
|
||||||
|
|
||||||
|
// Найти версию пакета, Fabric или базовую версию
|
||||||
|
let versionToLaunch = versionToLaunchOverride;
|
||||||
|
|
||||||
|
if (!versionToLaunch) {
|
||||||
|
if (
|
||||||
|
versionsContents.includes(`${baseVersion}-fabric${fabricVersion}`)
|
||||||
|
) {
|
||||||
|
versionToLaunch = `${baseVersion}-fabric${fabricVersion}`;
|
||||||
|
} else if (versionsContents.includes(packName)) {
|
||||||
|
versionToLaunch = packName;
|
||||||
|
} else {
|
||||||
|
versionToLaunch = baseVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Запускаем версию:', versionToLaunch);
|
||||||
|
|
||||||
|
// Находим путь к Java
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'java',
|
||||||
|
message: 'Поиск Java...',
|
||||||
|
});
|
||||||
|
|
||||||
|
let javaPath;
|
||||||
|
try {
|
||||||
|
javaPath = await findJava();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Ошибка при поиске Java:', error);
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'java-error',
|
||||||
|
message: 'Не удалось найти Java. Используем системную Java.',
|
||||||
|
});
|
||||||
|
javaPath = 'java';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Далее пробуем установить Minecraft, но продолжаем даже при ошибках
|
||||||
|
let resolvedVersion;
|
||||||
|
try {
|
||||||
|
// 1. Получаем список версий и устанавливаем ванильный Minecraft
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'minecraft-list',
|
||||||
|
message: 'Получение списка версий Minecraft...',
|
||||||
|
});
|
||||||
|
|
||||||
|
const versionList = await getVersionList();
|
||||||
|
const minecraftVersion = versionList.versions.find(
|
||||||
|
(v) => v.id === baseVersion,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (minecraftVersion) {
|
||||||
|
// Устанавливаем базовую версию Minecraft
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'minecraft-install',
|
||||||
|
message: `Установка Minecraft ${baseVersion}...`,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const installMcTask = installTask(minecraftVersion, minecraftDir, {
|
||||||
|
skipRevalidate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await installMcTask.startAndWait({
|
||||||
|
onStart(task) {
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: `minecraft-install.${task.path}`,
|
||||||
|
message: `Начало: ${task.name || task.path}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onUpdate(task) {
|
||||||
|
const percentage =
|
||||||
|
Math.round(
|
||||||
|
(installMcTask.progress / installMcTask.total) * 100,
|
||||||
|
) || 0;
|
||||||
|
|
||||||
|
event.sender.send('download-progress', percentage);
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: `minecraft-install.${task.path}`,
|
||||||
|
message: `Прогресс ${task.name || task.path}: ${percentage}% (${installMcTask.progress}/${installMcTask.total})`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onFailed(task, error) {
|
||||||
|
console.warn(
|
||||||
|
`Ошибка при установке ${task.path}, продолжаем:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: `minecraft-install.${task.path}`,
|
||||||
|
message: `Ошибка: ${error.message}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSucceed(task) {
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: `minecraft-install.${task.path}`,
|
||||||
|
message: `Завершено: ${task.name || task.path}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Ошибка при установке Minecraft, продолжаем:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Устанавливаем Fabric
|
||||||
|
try {
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'fabric-list',
|
||||||
|
message: 'Получение списка версий Fabric...',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fabricVersion) {
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'fabric-install',
|
||||||
|
message: `Установка Fabric ${fabricVersion}...`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await installFabric({
|
||||||
|
minecraftVersion: baseVersion,
|
||||||
|
version: fabricVersion, // Используйте напрямую, без .version
|
||||||
|
minecraft: minecraftDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Ошибка при установке Fabric, продолжаем:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Подготовка версии и установка зависимостей
|
||||||
|
try {
|
||||||
|
// Используем идентификатор Fabric-версии
|
||||||
|
const fabricVersionId = `${baseVersion}-fabric${fabricVersion}`;
|
||||||
|
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'version-parse',
|
||||||
|
message: 'Подготовка версии...',
|
||||||
|
});
|
||||||
|
|
||||||
|
resolvedVersion = await Version.parse(
|
||||||
|
minecraftDir,
|
||||||
|
fabricVersionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'dependencies',
|
||||||
|
message: 'Установка библиотек и ресурсов...',
|
||||||
|
});
|
||||||
|
|
||||||
|
const depsTask = installDependenciesTask(resolvedVersion, {
|
||||||
|
assetsDownloadConcurrency: 4,
|
||||||
|
skipRevalidate: true,
|
||||||
|
prevalidSizeOnly: true,
|
||||||
|
checksumValidatorResolver: (checksum) => ({
|
||||||
|
validate: async () => {
|
||||||
|
/* void */
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await depsTask.startAndWait({
|
||||||
|
onStart(task) {
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: `dependencies.${task.path}`,
|
||||||
|
message: `Начало: ${task.name || task.path}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onUpdate(task) {
|
||||||
|
const percentage =
|
||||||
|
Math.round((depsTask.progress / depsTask.total) * 100) || 0;
|
||||||
|
|
||||||
|
event.sender.send('download-progress', percentage);
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: `dependencies.${task.path}`,
|
||||||
|
message: `Установка ${task.name || task.path}: ${percentage}%`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onFailed(task, error) {
|
||||||
|
console.warn(
|
||||||
|
`Ошибка при установке ${task.path}, продолжаем:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: `dependencies.${task.path}`,
|
||||||
|
message: `Ошибка: ${error.message}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSucceed(task) {
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: `dependencies.${task.path}`,
|
||||||
|
message: `Завершено: ${task.name || task.path}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
'Ошибка при загрузке ресурсов, продолжаем запуск:',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Ошибка при подготовке версии, продолжаем:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Произошла ошибка при подготовке Minecraft:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загрузка и проверка authlib-injector
|
||||||
|
const authlibPath = await ensureAuthlibInjectorExists(appPath);
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'authlib-injector',
|
||||||
|
message: 'authlib-injector готов',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Запускаем Minecraft с authlib-injector для Ely.by
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'launch',
|
||||||
|
message: 'Запуск игры...',
|
||||||
|
});
|
||||||
|
|
||||||
|
// При запуске используем переданные параметры
|
||||||
|
const packDir = path.join(versionsDir, packName);
|
||||||
|
|
||||||
|
// При формировании конфигурации запуска создаем объект server только с нужными параметрами
|
||||||
|
const serverConfig: any = { ip: serverIp };
|
||||||
|
|
||||||
|
// Добавляем порт только если он был передан
|
||||||
|
if (serverPort) {
|
||||||
|
serverConfig.port = serverPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proc = await launch({
|
||||||
|
gamePath: packDir,
|
||||||
|
resourcePath: minecraftDir,
|
||||||
|
javaPath,
|
||||||
|
version: versionToLaunch,
|
||||||
|
launcherName: 'popa-popa',
|
||||||
|
server: serverConfig, // Используем созданный объект конфигурации
|
||||||
|
extraJVMArgs: [
|
||||||
|
'-Dlog4j2.formatMsgNoLookups=true',
|
||||||
|
`-javaagent:${authlibPath}=ely.by`,
|
||||||
|
`-Xmx${memory}M`,
|
||||||
|
],
|
||||||
|
// Используем данные аутентификации Yggdrasil
|
||||||
|
accessToken,
|
||||||
|
gameProfile: {
|
||||||
|
id: uuid,
|
||||||
|
name: username,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Логирование
|
||||||
|
proc.stdout?.on('data', (data) => {
|
||||||
|
console.log(`Minecraft stdout: ${data}`);
|
||||||
|
});
|
||||||
|
proc.stderr?.on('data', (data) => {
|
||||||
|
console.error(`Minecraft stderr: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, pid: proc.pid };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при запуске Minecraft:', error);
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'error',
|
||||||
|
message: `Ошибка запуска: ${error.message}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавьте в функцию initMinecraftHandlers или создайте новую
|
||||||
|
ipcMain.handle('get-pack-files', async (event, packName) => {
|
||||||
|
try {
|
||||||
|
const appPath = path.dirname(app.getPath('exe'));
|
||||||
|
const minecraftDir = path.join(appPath, '.minecraft');
|
||||||
|
const packDir = path.join(minecraftDir, 'versions', packName);
|
||||||
|
|
||||||
|
if (!fs.existsSync(packDir)) {
|
||||||
|
return { success: false, error: 'Директория сборки не найдена' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для рекурсивного обхода директории
|
||||||
|
const scanDir: any = (dir: any, basePath: any = '') => {
|
||||||
|
const result = [];
|
||||||
|
const items = fs.readdirSync(dir);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const itemPath = path.join(dir, item);
|
||||||
|
const relativePath = basePath ? path.join(basePath, item) : item;
|
||||||
|
const isDirectory = fs.statSync(itemPath).isDirectory();
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
name: item,
|
||||||
|
path: relativePath,
|
||||||
|
isDirectory,
|
||||||
|
// Если это директория, рекурсивно сканируем ее
|
||||||
|
children: isDirectory ? scanDir(itemPath, relativePath) : [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const files = scanDir(packDir);
|
||||||
|
return { success: true, files };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении файлов сборки:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем обработчики IPC для аутентификации
|
||||||
|
export function initAuthHandlers() {
|
||||||
|
// Аутентификация
|
||||||
|
ipcMain.handle('authenticate', async (event, { username, password }) => {
|
||||||
|
try {
|
||||||
|
const auth = await authService.login(username, password);
|
||||||
|
return {
|
||||||
|
accessToken: auth.accessToken,
|
||||||
|
clientToken: auth.clientToken,
|
||||||
|
selectedProfile: auth.selectedProfile,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка аутентификации:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Валидация токена
|
||||||
|
ipcMain.handle('validate-token', async (event, accessToken) => {
|
||||||
|
try {
|
||||||
|
const clientToken = JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(app.getPath('userData'), 'config.json'),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
|
).clientToken;
|
||||||
|
|
||||||
|
const valid = await authService.validate(accessToken, clientToken);
|
||||||
|
return { valid };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка валидации токена:', error);
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновление токена
|
||||||
|
ipcMain.handle(
|
||||||
|
'refresh-token',
|
||||||
|
async (event, { accessToken, clientToken }) => {
|
||||||
|
try {
|
||||||
|
const auth = await authService.refresh(accessToken, clientToken);
|
||||||
|
if (!auth) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: auth.accessToken,
|
||||||
|
clientToken: auth.clientToken,
|
||||||
|
selectedProfile: auth.selectedProfile,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка обновления токена:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для получения статуса сервера
|
||||||
|
export function initServerStatusHandler() {
|
||||||
|
ipcMain.handle('get-server-status', async (event, { host, port }) => {
|
||||||
|
try {
|
||||||
|
// Формируем адрес с портом, если указан
|
||||||
|
const serverAddress = port ? `${host}:${port}` : host;
|
||||||
|
|
||||||
|
// Делаем запрос к API mcstatus.io
|
||||||
|
const response = await fetch(`${MCSTATUS_API_URL}${serverAddress}`);
|
||||||
|
|
||||||
|
// Проверяем статус ответа
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`API вернул ошибку ${response.status}: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.online) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
online: data.players?.online || 0,
|
||||||
|
max: data.players?.max || 0,
|
||||||
|
version: data.version?.name_clean || 'Unknown',
|
||||||
|
icon: data.icon || null, // Возвращаем иконку
|
||||||
|
motd: data.motd?.clean || '', // Название сервера
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { success: false, error: 'Сервер не доступен' };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении статуса сервера:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для работы с конфигурацией сборки
|
||||||
|
export function initPackConfigHandlers() {
|
||||||
|
// Файл конфигурации
|
||||||
|
const CONFIG_FILENAME = 'popa-launcher-config.json';
|
||||||
|
|
||||||
|
// Обработчик для сохранения настроек сборки
|
||||||
|
ipcMain.handle('save-pack-config', async (event, { packName, config }) => {
|
||||||
|
try {
|
||||||
|
const appPath = path.dirname(app.getPath('exe'));
|
||||||
|
const minecraftDir = path.join(appPath, '.minecraft');
|
||||||
|
const packDir = path.join(minecraftDir, 'versions', packName);
|
||||||
|
|
||||||
|
// Создаем папку для сборки, если она не существует
|
||||||
|
if (!fs.existsSync(packDir)) {
|
||||||
|
fs.mkdirSync(packDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const configPath = path.join(packDir, CONFIG_FILENAME);
|
||||||
|
|
||||||
|
// Сохраняем конфигурацию в файл
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||||
|
|
||||||
|
// Добавляем файл конфигурации в список файлов, которые не удаляются
|
||||||
|
if (!config.preserveFiles.includes(CONFIG_FILENAME)) {
|
||||||
|
config.preserveFiles.push(CONFIG_FILENAME);
|
||||||
|
// Перезаписываем файл с обновленным списком
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при сохранении настроек сборки:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик для загрузки настроек сборки
|
||||||
|
ipcMain.handle('load-pack-config', async (event, { packName }) => {
|
||||||
|
try {
|
||||||
|
const appPath = path.dirname(app.getPath('exe'));
|
||||||
|
const minecraftDir = path.join(appPath, '.minecraft');
|
||||||
|
const packDir = path.join(minecraftDir, 'versions', packName);
|
||||||
|
const configPath = path.join(packDir, CONFIG_FILENAME);
|
||||||
|
|
||||||
|
// Проверяем существование файла конфигурации
|
||||||
|
if (!fs.existsSync(configPath)) {
|
||||||
|
// Если файла нет, возвращаем дефолтную конфигурацию
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
config: {
|
||||||
|
memory: 4096,
|
||||||
|
preserveFiles: [CONFIG_FILENAME], // По умолчанию сохраняем файл конфигурации
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем и парсим конфигурацию
|
||||||
|
const configData = fs.readFileSync(configPath, 'utf8');
|
||||||
|
const config = JSON.parse(configData);
|
||||||
|
|
||||||
|
// Добавляем файл конфигурации в список сохраняемых файлов, если его там нет
|
||||||
|
if (!config.preserveFiles.includes(CONFIG_FILENAME)) {
|
||||||
|
config.preserveFiles.push(CONFIG_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, config };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке настроек сборки:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
// Возвращаем дефолтную конфигурацию при ошибке
|
||||||
|
config: {
|
||||||
|
memory: 4096,
|
||||||
|
preserveFiles: [CONFIG_FILENAME],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -1,8 +1,17 @@
|
|||||||
// Disable no-unused-vars, broken for spread args
|
|
||||||
/* eslint no-unused-vars: off */
|
|
||||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
|
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
|
|
||||||
export type Channels = 'ipc-example';
|
export type Channels =
|
||||||
|
| 'ipc-example'
|
||||||
|
| 'download-progress'
|
||||||
|
| 'launch-minecraft'
|
||||||
|
| 'installation-status'
|
||||||
|
| 'get-server-status'
|
||||||
|
| 'close-app'
|
||||||
|
| 'minimize-app'
|
||||||
|
| 'save-pack-config'
|
||||||
|
| 'load-pack-config'
|
||||||
|
| 'update-available'
|
||||||
|
| 'install-update';
|
||||||
|
|
||||||
const electronHandler = {
|
const electronHandler = {
|
||||||
ipcRenderer: {
|
ipcRenderer: {
|
||||||
@ -21,6 +30,9 @@ const electronHandler = {
|
|||||||
once(channel: Channels, func: (...args: unknown[]) => void) {
|
once(channel: Channels, func: (...args: unknown[]) => void) {
|
||||||
ipcRenderer.once(channel, (_event, ...args) => func(...args));
|
ipcRenderer.once(channel, (_event, ...args) => func(...args));
|
||||||
},
|
},
|
||||||
|
invoke(channel: Channels, ...args: unknown[]): Promise<any> {
|
||||||
|
return ipcRenderer.invoke(channel, ...args);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,62 +1,49 @@
|
|||||||
/*
|
@font-face {
|
||||||
* @NOTE: Prepend a `~` to css file paths that are in your node_modules
|
font-family: 'Benzin-Bold';
|
||||||
* See https://github.com/webpack-contrib/sass-loader#imports
|
src: url('../../assets/fonts/benzin-bold.eot'); /* IE 9 Compatibility Mode */
|
||||||
*/
|
src:
|
||||||
|
url('../../assets/fonts/benzin-bold.eot?#iefix') format('embedded-opentype'),
|
||||||
|
/* IE < 9 */ url('../../assets/fonts/benzin-bold.woff2') format('woff2'),
|
||||||
|
/* Super Modern Browsers */ url('../../assets/fonts/benzin-bold.woff')
|
||||||
|
format('woff'),
|
||||||
|
/* Firefox >= 3.6, any other modern browser */
|
||||||
|
url('../../assets/fonts/benzin-bold.ttf') format('truetype'),
|
||||||
|
/* Safari, Android, iOS */
|
||||||
|
url('../../assets/fonts/benzin-bold.svg#benzin-bold') format('svg'); /* Chrome < 4, Legacy iOS */
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
position: relative;
|
position: relative;
|
||||||
color: white;
|
color: white;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: linear-gradient(
|
background: linear-gradient(242.94deg, #000000 39.07%, #3b4187 184.73%);
|
||||||
200.96deg,
|
font-family: 'Benzin-Bold' !important;
|
||||||
#fedc2a -29.09%,
|
|
||||||
#dd5789 51.77%,
|
|
||||||
#7a2c9e 129.35%
|
|
||||||
);
|
|
||||||
font-family: sans-serif;
|
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
p {
|
||||||
background-color: white;
|
font-family: 'Benzin-Bold' !important;
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: none;
|
|
||||||
appearance: none;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
box-shadow: 0px 8px 28px -6px rgba(24, 39, 75, 0.12),
|
|
||||||
0px 18px 88px -4px rgba(24, 39, 75, 0.14);
|
|
||||||
transition: all ease-in 0.1s;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
h1 {
|
||||||
transform: scale(1.05);
|
font-family: 'Benzin-Bold' !important;
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
h2 {
|
||||||
list-style: none;
|
font-family: 'Benzin-Bold' !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
h3 {
|
||||||
text-decoration: none;
|
font-family: 'Benzin-Bold' !important;
|
||||||
height: fit-content;
|
|
||||||
width: fit-content;
|
|
||||||
margin: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
h4 {
|
||||||
opacity: 1;
|
font-family: 'Benzin-Bold' !important;
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.Hello {
|
h5 {
|
||||||
display: flex;
|
font-family: 'Benzin-Bold' !important;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
}
|
||||||
|
@ -1,50 +1,115 @@
|
|||||||
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
|
import {
|
||||||
import icon from '../../assets/icon.svg';
|
MemoryRouter as Router,
|
||||||
|
Routes,
|
||||||
|
Route,
|
||||||
|
Navigate,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import Login from './pages/Login';
|
||||||
|
import LaunchPage from './pages/LaunchPage';
|
||||||
|
import { ReactNode, useEffect, useState } from 'react';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
import TopBar from './components/TopBar';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import MinecraftBackround from './components/MinecraftBackround';
|
||||||
|
import { Notifier } from './components/Notifier';
|
||||||
|
|
||||||
function Hello() {
|
// Переместите launchOptions сюда, вне компонентов
|
||||||
return (
|
const launchOptions = {
|
||||||
<div>
|
downloadUrl:
|
||||||
<div className="Hello">
|
'https://github.com/DIKER0K/Comfort/releases/latest/download/Comfort.zip',
|
||||||
<img width="200" alt="icon" src={icon} />
|
apiReleaseUrl: 'https://api.github.com/repos/DIKER0K/Comfort/releases/latest',
|
||||||
</div>
|
versionFileName: 'comfort_version.txt',
|
||||||
<h1>electron-react-boilerplate</h1>
|
packName: 'Comfort',
|
||||||
<div className="Hello">
|
memory: 4096,
|
||||||
<a
|
baseVersion: '1.21.4',
|
||||||
href="https://electron-react-boilerplate.js.org/"
|
serverIp: 'popa-popa.ru',
|
||||||
target="_blank"
|
fabricVersion: '0.16.14', // Уберите префикс "fabric"
|
||||||
rel="noreferrer"
|
};
|
||||||
>
|
|
||||||
<button type="button">
|
const AuthCheck = ({ children }: { children: ReactNode }) => {
|
||||||
<span role="img" aria-label="books">
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||||
📚
|
|
||||||
</span>
|
useEffect(() => {
|
||||||
Read our docs
|
const checkAuth = async () => {
|
||||||
</button>
|
try {
|
||||||
</a>
|
const savedConfig = localStorage.getItem('launcher_config');
|
||||||
<a
|
if (savedConfig) {
|
||||||
href="https://github.com/sponsors/electron-react-boilerplate"
|
const config = JSON.parse(savedConfig);
|
||||||
target="_blank"
|
if (config.accessToken) {
|
||||||
rel="noreferrer"
|
// Можно добавить дополнительную проверку токена
|
||||||
>
|
const isValid = await validateToken(config.accessToken);
|
||||||
<button type="button">
|
setIsAuthenticated(isValid);
|
||||||
<span role="img" aria-label="folded hands">
|
return;
|
||||||
🙏
|
}
|
||||||
</span>
|
}
|
||||||
Donate
|
setIsAuthenticated(false);
|
||||||
</button>
|
} catch (error) {
|
||||||
</a>
|
console.error('Ошибка проверки авторизации:', error);
|
||||||
</div>
|
setIsAuthenticated(false);
|
||||||
</div>
|
}
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
checkAuth();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const validateToken = async (token: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://authserver.ely.by/auth/validate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ accessToken: token }),
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isAuthenticated === null) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAuthenticated ? children : <Navigate to="/login" replace />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
// Просто используйте window.open без useNavigate
|
||||||
|
const handleRegister = () => {
|
||||||
|
window.open('https://account.ely.by/register', '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Box
|
||||||
<Route path="/" element={<Hello />} />
|
sx={{
|
||||||
</Routes>
|
height: '100vh',
|
||||||
|
width: '100vw',
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MinecraftBackround />
|
||||||
|
<TopBar onRegister={handleRegister} />
|
||||||
|
<Notifier />
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<AuthCheck>
|
||||||
|
<LaunchPage launchOptions={launchOptions} />
|
||||||
|
</AuthCheck>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</Box>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
205
src/renderer/components/FilesSelector.tsx
Normal file
205
src/renderer/components/FilesSelector.tsx
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Checkbox,
|
||||||
|
Typography,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Collapse,
|
||||||
|
CircularProgress,
|
||||||
|
} from '@mui/material';
|
||||||
|
import FolderIcon from '@mui/icons-material/Folder';
|
||||||
|
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||||
|
|
||||||
|
interface FileNode {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
isDirectory: boolean;
|
||||||
|
children: FileNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilesSelectorProps {
|
||||||
|
packName: string;
|
||||||
|
onSelectionChange: (selectedFiles: string[]) => void;
|
||||||
|
initialSelected?: string[]; // Добавляем этот параметр
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FilesSelector({
|
||||||
|
packName,
|
||||||
|
onSelectionChange,
|
||||||
|
initialSelected = [], // Значение по умолчанию
|
||||||
|
}: FilesSelectorProps) {
|
||||||
|
const [files, setFiles] = useState<FileNode[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
// Используем initialSelected для начального состояния
|
||||||
|
const [selectedFiles, setSelectedFiles] = useState<string[]>(initialSelected);
|
||||||
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchFiles = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await window.electron.ipcRenderer.invoke(
|
||||||
|
'get-pack-files',
|
||||||
|
packName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setFiles(result.files);
|
||||||
|
} else {
|
||||||
|
setError(result.error);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('Ошибка при загрузке файлов');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchFiles();
|
||||||
|
}, [packName]);
|
||||||
|
|
||||||
|
// Обработка выбора файла/папки
|
||||||
|
const handleToggle = (
|
||||||
|
path: string,
|
||||||
|
isDirectory: boolean,
|
||||||
|
children: FileNode[],
|
||||||
|
) => {
|
||||||
|
let newSelected = [...selectedFiles];
|
||||||
|
|
||||||
|
if (isDirectory) {
|
||||||
|
if (selectedFiles.includes(path)) {
|
||||||
|
// Если папка выбрана, убираем ее и все вложенные файлы
|
||||||
|
newSelected = newSelected.filter((p) => !p.startsWith(path));
|
||||||
|
} else {
|
||||||
|
// Если папка не выбрана, добавляем ее и все вложенные файлы
|
||||||
|
newSelected.push(path);
|
||||||
|
const addChildPaths = (nodes: FileNode[]) => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
newSelected.push(node.path);
|
||||||
|
if (node.isDirectory) {
|
||||||
|
addChildPaths(node.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addChildPaths(children);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Для обычного файла просто переключаем состояние
|
||||||
|
if (selectedFiles.includes(path)) {
|
||||||
|
newSelected = newSelected.filter((p) => p !== path);
|
||||||
|
} else {
|
||||||
|
newSelected.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedFiles(newSelected);
|
||||||
|
onSelectionChange(newSelected);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Переключение раскрытия папки
|
||||||
|
const toggleFolder = (path: string) => {
|
||||||
|
const newExpanded = new Set(expandedFolders);
|
||||||
|
if (expandedFolders.has(path)) {
|
||||||
|
newExpanded.delete(path);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(path);
|
||||||
|
}
|
||||||
|
setExpandedFolders(newExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Рекурсивный компонент для отображения файлов и папок
|
||||||
|
const renderFileTree = (nodes: FileNode[]) => {
|
||||||
|
// Сортировка: сначала папки, потом файлы
|
||||||
|
const sortedNodes = [...nodes].sort((a, b) => {
|
||||||
|
// Если у элементов разные типы (папка/файл)
|
||||||
|
if (a.isDirectory !== b.isDirectory) {
|
||||||
|
return a.isDirectory ? -1 : 1; // Папки идут первыми
|
||||||
|
}
|
||||||
|
// Если оба элемента одного типа, сортируем по алфавиту
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List dense>
|
||||||
|
{sortedNodes.map((node) => (
|
||||||
|
<div key={node.path}>
|
||||||
|
<ListItem
|
||||||
|
sx={{
|
||||||
|
borderRadius: '3vw',
|
||||||
|
backgroundColor: '#FFFFFF1A',
|
||||||
|
marginBottom: '1vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Checkbox
|
||||||
|
edge="start"
|
||||||
|
checked={selectedFiles.includes(node.path)}
|
||||||
|
onChange={() =>
|
||||||
|
handleToggle(node.path, node.isDirectory, node.children)
|
||||||
|
}
|
||||||
|
tabIndex={-1}
|
||||||
|
sx={{ color: 'white' }}
|
||||||
|
/>
|
||||||
|
</ListItemIcon>
|
||||||
|
|
||||||
|
{node.isDirectory && (
|
||||||
|
<ListItemIcon onClick={() => toggleFolder(node.path)}>
|
||||||
|
{expandedFolders.has(node.path) ? (
|
||||||
|
<ExpandLessIcon sx={{ color: 'white' }} />
|
||||||
|
) : (
|
||||||
|
<ExpandMoreIcon sx={{ color: 'white' }} />
|
||||||
|
)}
|
||||||
|
</ListItemIcon>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ListItemIcon>
|
||||||
|
{node.isDirectory ? (
|
||||||
|
<FolderIcon sx={{ color: 'white' }} />
|
||||||
|
) : (
|
||||||
|
<InsertDriveFileIcon sx={{ color: 'white' }} />
|
||||||
|
)}
|
||||||
|
</ListItemIcon>
|
||||||
|
|
||||||
|
<ListItemText
|
||||||
|
primary={node.name}
|
||||||
|
sx={{ color: 'white', fontFamily: 'Benzin-Bold' }}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
{node.isDirectory && node.children.length > 0 && (
|
||||||
|
<Collapse
|
||||||
|
in={expandedFolders.has(node.path)}
|
||||||
|
timeout="auto"
|
||||||
|
unmountOnExit
|
||||||
|
>
|
||||||
|
<Box sx={{ pl: 4 }}>{renderFileTree(node.children)}</Box>
|
||||||
|
</Collapse>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <CircularProgress />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Typography color="error">{error}</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ maxHeight: '300px', overflow: 'auto' }}>
|
||||||
|
{renderFileTree(files)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
39
src/renderer/components/Login/AuthForm.tsx
Normal file
39
src/renderer/components/Login/AuthForm.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Box, Button, TextField, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
interface AuthFormProps {
|
||||||
|
config: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
onLogin: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthForm = ({ config, handleInputChange, onLogin }: AuthFormProps) => {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1.5vw' }}>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
name="username"
|
||||||
|
label="Введите ник"
|
||||||
|
variant="outlined"
|
||||||
|
value={config.username}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
label="Введите пароль"
|
||||||
|
variant="outlined"
|
||||||
|
value={config.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Button onClick={onLogin} variant="contained">
|
||||||
|
Войти
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthForm;
|
26
src/renderer/components/Login/MemorySlider.tsx
Normal file
26
src/renderer/components/Login/MemorySlider.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Slider } from '@mui/material';
|
||||||
|
|
||||||
|
interface MemorySliderProps {
|
||||||
|
memory: number;
|
||||||
|
onChange: (e: Event, value: number | number[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemorySlider = ({ memory, onChange }: MemorySliderProps) => {
|
||||||
|
return (
|
||||||
|
<Slider
|
||||||
|
name="memory"
|
||||||
|
aria-label="Memory"
|
||||||
|
defaultValue={4096}
|
||||||
|
valueLabelDisplay="auto"
|
||||||
|
shiftStep={1024}
|
||||||
|
step={1024}
|
||||||
|
marks
|
||||||
|
min={1024}
|
||||||
|
max={32628}
|
||||||
|
value={memory}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemorySlider;
|
110
src/renderer/components/MinecraftBackround.tsx
Normal file
110
src/renderer/components/MinecraftBackround.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { Box } from '@mui/material';
|
||||||
|
import heart from '../../../assets/images/heart.svg';
|
||||||
|
|
||||||
|
export default function MinecraftBackround() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
opacity: 0.25,
|
||||||
|
overflow: 'hidden',
|
||||||
|
zIndex: -10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
gap: '1vw',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
rotate: '-20deg',
|
||||||
|
paddingTop: '30vw',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={heart}
|
||||||
|
draggable={false}
|
||||||
|
style={{
|
||||||
|
width: '20vw',
|
||||||
|
height: '20vw',
|
||||||
|
rotate: '-20deg',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={heart}
|
||||||
|
draggable={false}
|
||||||
|
style={{
|
||||||
|
width: '20vw',
|
||||||
|
height: '20vw',
|
||||||
|
paddingBottom: '5vw',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={heart}
|
||||||
|
draggable={false}
|
||||||
|
style={{
|
||||||
|
width: '20vw',
|
||||||
|
height: '20vw',
|
||||||
|
rotate: '20deg',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
gap: '1vw',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
rotate: '160deg',
|
||||||
|
paddingTop: '80vw',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={heart}
|
||||||
|
draggable={false}
|
||||||
|
style={{
|
||||||
|
width: '20vw',
|
||||||
|
height: '20vw',
|
||||||
|
rotate: '-20deg',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={heart}
|
||||||
|
draggable={false}
|
||||||
|
style={{
|
||||||
|
width: '20vw',
|
||||||
|
height: '20vw',
|
||||||
|
paddingBottom: '5vw',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={heart}
|
||||||
|
draggable={false}
|
||||||
|
style={{
|
||||||
|
width: '20vw',
|
||||||
|
height: '20vw',
|
||||||
|
rotate: '20deg',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
63
src/renderer/components/Notifier.tsx
Normal file
63
src/renderer/components/Notifier.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Alert, Box, Snackbar, Button } from '@mui/material';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const Notifier = () => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [severity, setSeverity] = useState<
|
||||||
|
'error' | 'warning' | 'info' | 'success'
|
||||||
|
>('info');
|
||||||
|
const [hasUpdateAvailable, setHasUpdateAvailable] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Слушаем событие о наличии обновления
|
||||||
|
window.electron.ipcRenderer.on('update-available', () => {
|
||||||
|
setMessage('Доступно новое обновление');
|
||||||
|
setSeverity('info');
|
||||||
|
setHasUpdateAvailable(true);
|
||||||
|
setOpen(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Отписываемся от события при размонтировании
|
||||||
|
window.electron.ipcRenderer.removeAllListeners('update-available');
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = () => {
|
||||||
|
window.electron.ipcRenderer.invoke('install-update');
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Snackbar
|
||||||
|
open={open}
|
||||||
|
autoHideDuration={hasUpdateAvailable ? null : 6000}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
severity={severity}
|
||||||
|
action={
|
||||||
|
hasUpdateAvailable && (
|
||||||
|
<>
|
||||||
|
<Button color="primary" size="small" onClick={handleUpdate}>
|
||||||
|
Обновить сейчас
|
||||||
|
</Button>
|
||||||
|
<Button color="secondary" size="small" onClick={handleClose}>
|
||||||
|
Позже
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
121
src/renderer/components/ServerStatus/ServerStatus.tsx
Normal file
121
src/renderer/components/ServerStatus/ServerStatus.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { Box, Typography, CircularProgress, Avatar } from '@mui/material';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface ServerStatusProps {
|
||||||
|
serverIp: string;
|
||||||
|
serverPort?: number;
|
||||||
|
refreshInterval?: number; // Интервал обновления в миллисекундах
|
||||||
|
}
|
||||||
|
|
||||||
|
const ServerStatus = ({
|
||||||
|
serverIp,
|
||||||
|
serverPort,
|
||||||
|
refreshInterval = 60000, // По умолчанию обновление раз в минуту
|
||||||
|
}: ServerStatusProps) => {
|
||||||
|
const [serverStatus, setServerStatus] = useState<{
|
||||||
|
online: number;
|
||||||
|
max: number;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
icon: string | null;
|
||||||
|
motd: string;
|
||||||
|
}>({
|
||||||
|
online: 0,
|
||||||
|
max: 0,
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
icon: null,
|
||||||
|
motd: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Функция для получения статуса сервера
|
||||||
|
const fetchServerStatus = async () => {
|
||||||
|
try {
|
||||||
|
setServerStatus((prev) => ({ ...prev, loading: true, error: null }));
|
||||||
|
console.log('Отправляем запрос на сервер с параметрами:', {
|
||||||
|
host: serverIp,
|
||||||
|
port: serverPort || 25565,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем, что serverIp имеет значение
|
||||||
|
if (!serverIp) {
|
||||||
|
throw new Error('Адрес сервера не указан');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await window.electron.ipcRenderer.invoke(
|
||||||
|
'get-server-status',
|
||||||
|
{
|
||||||
|
host: serverIp,
|
||||||
|
port: serverPort || 25565,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setServerStatus({
|
||||||
|
online: result.online,
|
||||||
|
max: result.max,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
icon: result.icon,
|
||||||
|
motd: result.motd || serverIp,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setServerStatus({
|
||||||
|
online: 0,
|
||||||
|
max: 0,
|
||||||
|
loading: false,
|
||||||
|
error: result.error || 'Неизвестная ошибка',
|
||||||
|
icon: null,
|
||||||
|
motd: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении статуса сервера:', error);
|
||||||
|
setServerStatus((prev) => ({
|
||||||
|
...prev,
|
||||||
|
loading: false,
|
||||||
|
error: 'Ошибка при получении статуса сервера',
|
||||||
|
icon: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Загрузка при первом рендере
|
||||||
|
fetchServerStatus();
|
||||||
|
|
||||||
|
// Периодическое обновление
|
||||||
|
const interval = setInterval(fetchServerStatus, refreshInterval);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [serverIp, serverPort, refreshInterval]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
{/* Отображаем иконку сервера или иконку по умолчанию */}
|
||||||
|
{serverStatus.icon ? (
|
||||||
|
<Avatar
|
||||||
|
src={serverStatus.icon}
|
||||||
|
alt={serverStatus.motd || 'Minecraft сервер'}
|
||||||
|
sx={{ width: '2em', height: '2em' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Avatar sx={{ width: '2em', height: '2em', bgcolor: 'primary.main' }}>
|
||||||
|
?
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{serverStatus.loading ? (
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
) : serverStatus.error ? (
|
||||||
|
<Typography color="error">Ошибка загрузки</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography sx={{ fontWeight: 'bold' }}>
|
||||||
|
{serverStatus.online} / {serverStatus.max} игроков
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerStatus;
|
109
src/renderer/components/TopBar.tsx
Normal file
109
src/renderer/components/TopBar.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { Box, Button, Typography } from '@mui/material';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import MinimizeIcon from '@mui/icons-material/Minimize';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electron: {
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke(channel: string, ...args: unknown[]): Promise<any>;
|
||||||
|
on(channel: string, func: (...args: unknown[]) => void): void;
|
||||||
|
removeAllListeners(channel: string): void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем пропсы
|
||||||
|
interface TopBarProps {
|
||||||
|
onRegister?: () => void; // Опционально, если нужен обработчик регистрации
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TopBar({ onRegister }: TopBarProps) {
|
||||||
|
// Получаем текущий путь
|
||||||
|
const location = useLocation();
|
||||||
|
const isLoginPage = location.pathname === '/login';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: '50px',
|
||||||
|
zIndex: 1000,
|
||||||
|
width: '100%',
|
||||||
|
WebkitAppRegion: 'drag',
|
||||||
|
overflow: 'hidden',
|
||||||
|
justifyContent: 'flex-end', // Всё содержимое справа
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Правая часть со всеми кнопками */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
WebkitAppRegion: 'no-drag',
|
||||||
|
gap: '2vw',
|
||||||
|
padding: '1em',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Кнопка регистрации, если на странице логина */}
|
||||||
|
{isLoginPage && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => onRegister && onRegister()}
|
||||||
|
sx={{
|
||||||
|
width: '10em',
|
||||||
|
height: '3em',
|
||||||
|
borderRadius: '1.5vw',
|
||||||
|
color: 'white',
|
||||||
|
backgroundImage: 'linear-gradient(to right, #7BB8FF, #FFB7ED)',
|
||||||
|
border: 'unset',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundImage: 'linear-gradient(to right, #6AA8EE, #EEA7DD)',
|
||||||
|
},
|
||||||
|
boxShadow: '0.5em 0.5em 0.5em 0px #00000040 inset',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Регистрация
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Кнопки управления окном */}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
window.electron.ipcRenderer.invoke('minimize-app');
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
minWidth: 'unset',
|
||||||
|
minHeight: 'unset',
|
||||||
|
width: '3em',
|
||||||
|
height: '3em',
|
||||||
|
borderRadius: '50%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MinimizeIcon sx={{ color: 'white' }} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
window.electron.ipcRenderer.invoke('close-app');
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
minWidth: 'unset',
|
||||||
|
minHeight: 'unset',
|
||||||
|
width: '3em',
|
||||||
|
height: '3em',
|
||||||
|
borderRadius: '50%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon sx={{ color: 'white' }} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
22
src/renderer/components/popa-popa.tsx
Normal file
22
src/renderer/components/popa-popa.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Typography } from '@mui/material';
|
||||||
|
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
|
||||||
|
export default function PopaPopa() {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<Typography variant="h3">POPA</Typography>
|
||||||
|
<Typography variant="h3">-</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
background: '-webkit-linear-gradient(200.96deg, #88BCFF, #FD71FF)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
POPA
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
94
src/renderer/hooks/useAuth.ts
Normal file
94
src/renderer/hooks/useAuth.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface AuthSession {
|
||||||
|
accessToken: string;
|
||||||
|
clientToken: string;
|
||||||
|
selectedProfile: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useAuth() {
|
||||||
|
const [status, setStatus] = useState('idle');
|
||||||
|
|
||||||
|
// Проверка валидности токена
|
||||||
|
const validateSession = async (accessToken: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
setStatus('validating');
|
||||||
|
const response = await window.electron.ipcRenderer.invoke(
|
||||||
|
'validate-token',
|
||||||
|
accessToken,
|
||||||
|
);
|
||||||
|
setStatus('idle');
|
||||||
|
return response.valid;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при валидации токена:', error);
|
||||||
|
setStatus('error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обновление токена
|
||||||
|
const refreshSession = async (
|
||||||
|
accessToken: string,
|
||||||
|
clientToken: string,
|
||||||
|
): Promise<AuthSession | null> => {
|
||||||
|
try {
|
||||||
|
setStatus('refreshing');
|
||||||
|
const response = await window.electron.ipcRenderer.invoke(
|
||||||
|
'refresh-token',
|
||||||
|
{ accessToken, clientToken },
|
||||||
|
);
|
||||||
|
setStatus('idle');
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при обновлении токена:', error);
|
||||||
|
setStatus('error');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Аутентификация в Ely.by
|
||||||
|
const authenticateWithElyBy = async (
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
saveConfigFunc: Function,
|
||||||
|
): Promise<AuthSession | null> => {
|
||||||
|
try {
|
||||||
|
setStatus('authenticating');
|
||||||
|
const response = await window.electron.ipcRenderer.invoke(
|
||||||
|
'authenticate',
|
||||||
|
{ username, password },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response && response.accessToken) {
|
||||||
|
// Правильно сохраняем данные в конфигурации
|
||||||
|
saveConfigFunc({
|
||||||
|
username: response.selectedProfile.name, // Имя игрока как строка
|
||||||
|
uuid: response.selectedProfile.id,
|
||||||
|
accessToken: response.accessToken,
|
||||||
|
clientToken: response.clientToken,
|
||||||
|
memory: 4096, // Сохраняем значение по умолчанию или из предыдущей конфигурации
|
||||||
|
});
|
||||||
|
|
||||||
|
setStatus('authenticated');
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('error');
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при аутентификации:', error);
|
||||||
|
setStatus('error');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
validateSession,
|
||||||
|
refreshSession,
|
||||||
|
authenticateWithElyBy,
|
||||||
|
};
|
||||||
|
}
|
61
src/renderer/hooks/useConfig.ts
Normal file
61
src/renderer/hooks/useConfig.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
// Добавляем определение типа Config
|
||||||
|
interface Config {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
memory: number;
|
||||||
|
comfortVersion: string;
|
||||||
|
accessToken: string;
|
||||||
|
clientToken: string;
|
||||||
|
uuid?: string; // Добавляем uuid, который используется для авторизации
|
||||||
|
}
|
||||||
|
|
||||||
|
const useConfig = () => {
|
||||||
|
const [config, setConfig] = useState<Config>({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
memory: 4096,
|
||||||
|
comfortVersion: '',
|
||||||
|
accessToken: '',
|
||||||
|
clientToken: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadInitialConfig = () => {
|
||||||
|
try {
|
||||||
|
const savedConfig = localStorage.getItem('launcher_config');
|
||||||
|
if (savedConfig) {
|
||||||
|
return JSON.parse(savedConfig);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Ошибка загрузки конфигурации:', error);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
comfortVersion: '',
|
||||||
|
accessToken: '',
|
||||||
|
clientToken: '',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedConfig = loadInitialConfig();
|
||||||
|
setConfig(savedConfig);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveConfig = (newConfig: Partial<Config>) => {
|
||||||
|
const updatedConfig = { ...config, ...newConfig };
|
||||||
|
setConfig(updatedConfig);
|
||||||
|
localStorage.setItem('launcher_config', JSON.stringify(updatedConfig));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setConfig((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return { config, setConfig, saveConfig, handleInputChange };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useConfig;
|
@ -6,7 +6,7 @@
|
|||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="script-src 'self' 'unsafe-inline'"
|
content="script-src 'self' 'unsafe-inline'"
|
||||||
/>
|
/>
|
||||||
<title>Hello Electron React!</title>
|
<title>popa-launcher</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
431
src/renderer/pages/LaunchPage.tsx
Normal file
431
src/renderer/pages/LaunchPage.tsx
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
Snackbar,
|
||||||
|
Alert,
|
||||||
|
LinearProgress,
|
||||||
|
Modal,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import ServerStatus from '../components/ServerStatus/ServerStatus';
|
||||||
|
import PopaPopa from '../components/popa-popa';
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
|
import React from 'react';
|
||||||
|
import MemorySlider from '../components/Login/MemorySlider';
|
||||||
|
import FilesSelector from '../components/FilesSelector';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electron: {
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke(channel: string, ...args: unknown[]): Promise<any>;
|
||||||
|
on(channel: string, func: (...args: unknown[]) => void): void;
|
||||||
|
removeAllListeners(channel: string): void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем тип для props
|
||||||
|
interface LaunchPageProps {
|
||||||
|
launchOptions: {
|
||||||
|
downloadUrl: string;
|
||||||
|
apiReleaseUrl: string;
|
||||||
|
versionFileName: string;
|
||||||
|
packName: string;
|
||||||
|
memory: number;
|
||||||
|
baseVersion: string;
|
||||||
|
serverIp: string;
|
||||||
|
fabricVersion: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
// Начальное состояние должно быть пустым или с минимальными значениями
|
||||||
|
const [config, setConfig] = useState<{
|
||||||
|
memory: number;
|
||||||
|
preserveFiles: string[];
|
||||||
|
}>({
|
||||||
|
memory: 0,
|
||||||
|
preserveFiles: [],
|
||||||
|
});
|
||||||
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
|
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||||
|
const [buffer, setBuffer] = useState(10);
|
||||||
|
const [installStatus, setInstallStatus] = useState('');
|
||||||
|
const [notification, setNotification] = useState<{
|
||||||
|
open: boolean;
|
||||||
|
message: string;
|
||||||
|
severity: 'success' | 'error' | 'info';
|
||||||
|
}>({ open: false, message: '', severity: 'info' });
|
||||||
|
const [installStep, setInstallStep] = useState('');
|
||||||
|
const [installMessage, setInstallMessage] = useState('');
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const handleOpen = () => setOpen(true);
|
||||||
|
const handleClose = () => setOpen(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedConfig = localStorage.getItem('launcher_config');
|
||||||
|
if (!savedConfig || !JSON.parse(savedConfig).accessToken) {
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressListener = (...args: unknown[]) => {
|
||||||
|
const progress = args[0] as number;
|
||||||
|
setDownloadProgress(progress);
|
||||||
|
setBuffer(Math.min(progress + 10, 100));
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusListener = (...args: unknown[]) => {
|
||||||
|
const status = args[0] as { step: string; message: string };
|
||||||
|
setInstallStep(status.step);
|
||||||
|
setInstallMessage(status.message);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.electron.ipcRenderer.on('download-progress', progressListener);
|
||||||
|
window.electron.ipcRenderer.on('installation-status', statusListener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Удаляем только конкретных слушателей, а не всех
|
||||||
|
// Это безопаснее, чем removeAllListeners
|
||||||
|
const cleanup = window.electron.ipcRenderer.on;
|
||||||
|
if (typeof cleanup === 'function') {
|
||||||
|
cleanup('download-progress', progressListener);
|
||||||
|
cleanup('installation-status', statusListener);
|
||||||
|
}
|
||||||
|
// Удаляем использование removeAllListeners
|
||||||
|
};
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Загрузка конфигурации сборки при монтировании
|
||||||
|
const loadPackConfig = async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.electron.ipcRenderer.invoke(
|
||||||
|
'load-pack-config',
|
||||||
|
{
|
||||||
|
packName: launchOptions.packName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success && result.config) {
|
||||||
|
// Полностью заменяем config значениями из файла
|
||||||
|
setConfig(result.config);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке настроек:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPackConfig();
|
||||||
|
}, [launchOptions.packName]);
|
||||||
|
|
||||||
|
const showNotification = (
|
||||||
|
message: string,
|
||||||
|
severity: 'success' | 'error' | 'info',
|
||||||
|
) => {
|
||||||
|
setNotification({ open: true, message, severity });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseNotification = () => {
|
||||||
|
setNotification({ ...notification, open: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLaunchMinecraft = async () => {
|
||||||
|
try {
|
||||||
|
setIsDownloading(true);
|
||||||
|
setDownloadProgress(0);
|
||||||
|
setBuffer(10);
|
||||||
|
|
||||||
|
// Загружаем настройки сборки
|
||||||
|
const result = await window.electron.ipcRenderer.invoke(
|
||||||
|
'load-pack-config',
|
||||||
|
{
|
||||||
|
packName: launchOptions.packName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Используйте уже существующий state вместо локальной переменной
|
||||||
|
if (result.success && result.config) {
|
||||||
|
setConfig(result.config); // Обновляем state
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedConfig = JSON.parse(
|
||||||
|
localStorage.getItem('launcher_config') || '{}',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Опции для скачивания сборки
|
||||||
|
const packOptions = {
|
||||||
|
downloadUrl: launchOptions.downloadUrl,
|
||||||
|
apiReleaseUrl: launchOptions.apiReleaseUrl,
|
||||||
|
versionFileName: launchOptions.versionFileName,
|
||||||
|
packName: launchOptions.packName,
|
||||||
|
preserveFiles: config.preserveFiles,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Передаем опции для скачивания
|
||||||
|
const downloadResult = await window.electron.ipcRenderer.invoke(
|
||||||
|
'download-and-extract',
|
||||||
|
packOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (downloadResult?.success) {
|
||||||
|
let needsSecondAttempt = false;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showNotification(`Ошибка: ${error.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setIsDownloading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для сохранения настроек
|
||||||
|
const savePackConfig = async () => {
|
||||||
|
try {
|
||||||
|
const configToSave = {
|
||||||
|
memory: config.memory,
|
||||||
|
preserveFiles: config.preserveFiles || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await window.electron.ipcRenderer.invoke('save-pack-config', {
|
||||||
|
packName: launchOptions.packName,
|
||||||
|
config: configToSave,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем launchOptions
|
||||||
|
launchOptions.memory = config.memory;
|
||||||
|
|
||||||
|
showNotification('Настройки сохранены', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при сохранении настроек:', error);
|
||||||
|
showNotification('Ошибка сохранения настроек', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ gap: '1vh', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<PopaPopa />
|
||||||
|
|
||||||
|
<Typography variant="h4">Игровой сервер</Typography>
|
||||||
|
|
||||||
|
<Typography variant="h4">долбаёбов в Minecraft</Typography>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||||||
|
СЕРВЕР ГДЕ ВСЕМ НА ВАС ПОХУЙ
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||||||
|
СЕРВЕР ГДЕ РАЗРЕШИНЫ ОДНОПОЛЫЕ БРАКИ
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||||||
|
СЕРВЕР ГДЕ ВСЕ ДОЛБАЕБЫ
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||||||
|
СЕРВЕР ГДЕ НА СПАВНЕ БУДЕТ ХУЙ (ВОЗМОЖНО)
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||||||
|
СЕРВЕР ЗА КОТОРЫЙ ВЫ ПРОДАДИТЕ МАТЬ
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||||||
|
ТЫ МОЖЕШЬ КУПИТЬ АДМИНКУ И ПОЛУЧИТЬ ПИЗДЫ
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<ServerStatus
|
||||||
|
serverIp={launchOptions.serverIp}
|
||||||
|
refreshInterval={30000}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{isDownloading ? (
|
||||||
|
<Box sx={{ mb: 3, width: '100%' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Box sx={{ width: '100%', mr: 1 }}>
|
||||||
|
<LinearProgress
|
||||||
|
variant="buffer"
|
||||||
|
value={downloadProgress}
|
||||||
|
valueBuffer={buffer}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ minWidth: 35 }}>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: 'white' }}
|
||||||
|
>{`${Math.round(downloadProgress)}%`}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '1vw',
|
||||||
|
width: '100%', // родитель занимает всю ширину
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Первая кнопка — растягивается на всё доступное пространство */}
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={handleLaunchMinecraft}
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1, // занимает всё свободное место
|
||||||
|
width: 'auto', // ширина подстраивается
|
||||||
|
borderRadius: '3vw',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
background: 'linear-gradient(90deg, #3B96FF 0%, #FFB7ED 100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Запустить Minecraft
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Вторая кнопка — квадратная, фиксированного размера (ширина = высоте) */}
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={{
|
||||||
|
flexShrink: 0, // не сжимается
|
||||||
|
aspectRatio: '1', // ширина = высоте
|
||||||
|
backgroundColor: 'grey',
|
||||||
|
borderRadius: '3vw',
|
||||||
|
minHeight: 'unset',
|
||||||
|
minWidth: 'unset',
|
||||||
|
height: '100%', // занимает полную высоту родителя
|
||||||
|
}}
|
||||||
|
onClick={handleOpen}
|
||||||
|
>
|
||||||
|
<SettingsIcon />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Snackbar
|
||||||
|
open={notification.open}
|
||||||
|
autoHideDuration={6000}
|
||||||
|
onClose={handleCloseNotification}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
onClose={handleCloseNotification}
|
||||||
|
severity={notification.severity}
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{notification.message}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
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={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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LaunchPage;
|
92
src/renderer/pages/Login.tsx
Normal file
92
src/renderer/pages/Login.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { Box, Typography } from '@mui/material';
|
||||||
|
import useAuth from '../hooks/useAuth';
|
||||||
|
import AuthForm from '../components/Login/AuthForm';
|
||||||
|
import MemorySlider from '../components/Login/MemorySlider';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import PopaPopa from '../components/popa-popa';
|
||||||
|
import useConfig from '../hooks/useConfig';
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { config, setConfig, saveConfig, handleInputChange } = useConfig();
|
||||||
|
const { status, validateSession, refreshSession, authenticateWithElyBy } =
|
||||||
|
useAuth();
|
||||||
|
|
||||||
|
const authorization = async () => {
|
||||||
|
console.log('Начинаем процесс авторизации...');
|
||||||
|
|
||||||
|
if (!config.username.trim()) {
|
||||||
|
console.log('Ошибка: не указан никнейм');
|
||||||
|
alert('Введите никнейм!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверяем, есть ли сохранённый токен
|
||||||
|
if (config.accessToken && config.clientToken) {
|
||||||
|
console.log('Проверка валидности существующего токена...');
|
||||||
|
const isValid = await validateSession(config.accessToken);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
console.log('Токен недействителен, пытаемся обновить...');
|
||||||
|
const refreshedSession = await refreshSession(
|
||||||
|
config.accessToken,
|
||||||
|
config.clientToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!refreshedSession) {
|
||||||
|
console.log(
|
||||||
|
'Не удалось обновить токен, требуется новая авторизация',
|
||||||
|
);
|
||||||
|
const newSession = await authenticateWithElyBy(
|
||||||
|
config.username,
|
||||||
|
config.password,
|
||||||
|
saveConfig,
|
||||||
|
);
|
||||||
|
if (!newSession) {
|
||||||
|
console.log('Авторизация не удалась');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Токен действителен');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Токен отсутствует, выполняем авторизацию...');
|
||||||
|
const session = await authenticateWithElyBy(
|
||||||
|
config.username,
|
||||||
|
config.password,
|
||||||
|
saveConfig,
|
||||||
|
);
|
||||||
|
if (!session) {
|
||||||
|
console.log('Авторизация не удалась');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Авторизация успешно завершена');
|
||||||
|
navigate('/');
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`ОШИБКА при авторизации: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<PopaPopa />
|
||||||
|
<AuthForm
|
||||||
|
config={config}
|
||||||
|
handleInputChange={handleInputChange}
|
||||||
|
onLogin={authorization}
|
||||||
|
/>
|
||||||
|
<MemorySlider
|
||||||
|
memory={config.memory}
|
||||||
|
onChange={(e, value) => {
|
||||||
|
setConfig((prev) => ({ ...prev, memory: value as number }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"useUnknownInCatchVariables": false,
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"target": "es2022",
|
"target": "es2022",
|
||||||
"module": "node16",
|
"module": "node16",
|
||||||
|
Reference in New Issue
Block a user