Compare commits
4 Commits
1.0.0
...
b65b9538bb
Author | SHA1 | Date | |
---|---|---|---|
b65b9538bb | |||
4717132b05 | |||
12f7ea8d1c | |||
e21a51482a |
@ -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' })],
|
||||||
|
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.
1131
package-lock.json
generated
1131
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -102,17 +102,29 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/notarize": "^3.0.0",
|
"@electron/notarize": "^3.0.0",
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@mui/material": "^7.2.0",
|
||||||
|
"@xmcl/core": "^2.14.1",
|
||||||
|
"@xmcl/installer": "^6.1.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"
|
||||||
},
|
},
|
||||||
"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",
|
||||||
|
513
src/main/main.ts
513
src/main/main.ts
@ -14,6 +14,121 @@ 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 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 { Task } from '@xmcl/task';
|
||||||
|
// import findJavaHome from 'find-java-home';
|
||||||
|
|
||||||
|
// Функция для поиска Java
|
||||||
|
async function findJava(): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
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)) {
|
||||||
|
return resolve(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',
|
||||||
|
].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)) {
|
||||||
|
return resolve(javaPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Ошибка при сканировании ${basePath}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Пробуем найти java в PATH через команду which/where
|
||||||
|
try {
|
||||||
|
const command =
|
||||||
|
process.platform === 'win32' ? 'where java' : 'which java';
|
||||||
|
const javaPath = execSync(command).toString().trim();
|
||||||
|
|
||||||
|
if (javaPath && fs.existsSync(javaPath)) {
|
||||||
|
return resolve(javaPath);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка при поиске java через PATH:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если Java не найдена, выдаем ошибку
|
||||||
|
reject(
|
||||||
|
new Error('Java не найдена. Установите Java и повторите попытку.'),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class AppUpdater {
|
class AppUpdater {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -43,6 +158,403 @@ if (isDebug) {
|
|||||||
require('electron-debug').default();
|
require('electron-debug').default();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Minecraft
|
||||||
|
|
||||||
|
const COMFORT_DOWNLOAD_URL =
|
||||||
|
'https://github.com/DIKER0K/Comfort/releases/latest/download/Comfort.zip';
|
||||||
|
const GITHUB_API_RELEASE_URL =
|
||||||
|
'https://api.github.com/repos/DIKER0K/Comfort/releases/latest';
|
||||||
|
const COMFORT_VERSION_FILE = 'comfort_version.txt';
|
||||||
|
|
||||||
|
async function getLatestReleaseVersion(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(GITHUB_API_RELEASE_URL);
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Модифицируем обработчик IPC
|
||||||
|
ipcMain.handle('download-and-extract', async (event) => {
|
||||||
|
try {
|
||||||
|
const appPath = path.dirname(app.getPath('exe'));
|
||||||
|
const minecraftDir = path.join(appPath, '.minecraft');
|
||||||
|
const versionsDir = path.join(minecraftDir, 'versions');
|
||||||
|
const versionFilePath = path.join(minecraftDir, COMFORT_VERSION_FILE);
|
||||||
|
|
||||||
|
// Получаем текущую и последнюю версии
|
||||||
|
const latestVersion = await getLatestReleaseVersion();
|
||||||
|
let currentVersion = '';
|
||||||
|
|
||||||
|
// Проверяем текущую версию, если файл существует
|
||||||
|
if (fs.existsSync(versionFilePath)) {
|
||||||
|
currentVersion = fs.readFileSync(versionFilePath, 'utf-8').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, нужно ли обновление
|
||||||
|
if (currentVersion === latestVersion) {
|
||||||
|
return { success: true, updated: false, version: currentVersion };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempDir = path.join(appPath, 'temp');
|
||||||
|
|
||||||
|
// Создаем/очищаем временную директорию
|
||||||
|
if (fs.existsSync(tempDir)) {
|
||||||
|
fs.rmSync(tempDir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
const zipPath = path.join(tempDir, 'Comfort.zip');
|
||||||
|
|
||||||
|
// Скачиваем файл
|
||||||
|
await downloadFile(COMFORT_DOWNLOAD_URL, 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Распаковываем архив напрямую в папку versions
|
||||||
|
await extract(zipPath, { dir: versionsDir });
|
||||||
|
fs.unlinkSync(zipPath);
|
||||||
|
|
||||||
|
// Сохраняем новую версию
|
||||||
|
fs.writeFileSync(versionFilePath, latestVersion);
|
||||||
|
|
||||||
|
// Удаляем временную директорию
|
||||||
|
fs.rmSync(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
// После распаковки архива и перед запуском
|
||||||
|
const versionsContents = fs.readdirSync(versionsDir);
|
||||||
|
console.log('Доступные версии:', versionsContents);
|
||||||
|
|
||||||
|
return { success: true, updated: true, version: latestVersion };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in download-and-extract:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('launch-minecraft', async (event) => {
|
||||||
|
try {
|
||||||
|
const baseVersion = '1.21.4';
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Найти версию Comfort или версию с Fabric
|
||||||
|
let versionToLaunch = '';
|
||||||
|
if (versionsContents.includes('1.21.4-fabric0.16.14')) {
|
||||||
|
versionToLaunch = '1.21.4-fabric0.16.14';
|
||||||
|
} else if (versionsContents.includes('Comfort')) {
|
||||||
|
versionToLaunch = 'Comfort';
|
||||||
|
} else {
|
||||||
|
versionToLaunch = '1.21.4';
|
||||||
|
}
|
||||||
|
|
||||||
|
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'; // Пробуем использовать системную 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...',
|
||||||
|
});
|
||||||
|
|
||||||
|
const fabricVersions = await getFabricLoaders();
|
||||||
|
const fabricVersion = fabricVersions[0]; // Последняя версия
|
||||||
|
|
||||||
|
if (fabricVersion) {
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'fabric-install',
|
||||||
|
message: `Установка Fabric ${fabricVersion.version}...`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await installFabric({
|
||||||
|
minecraftVersion: baseVersion,
|
||||||
|
version: fabricVersion.version,
|
||||||
|
minecraft: minecraftDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Ошибка при установке Fabric, продолжаем:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Подготовка версии и установка зависимостей
|
||||||
|
try {
|
||||||
|
// Используем идентификатор Fabric-версии
|
||||||
|
const fabricVersionId = `${baseVersion}-fabric0.16.14`;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Запускаем Minecraft - НЕЗАВИСИМО ОТ ПРЕДЫДУЩИХ ОШИБОК
|
||||||
|
event.sender.send('installation-status', {
|
||||||
|
step: 'launch',
|
||||||
|
message: 'Запуск игры...',
|
||||||
|
});
|
||||||
|
|
||||||
|
const comfortDir = path.join(versionsDir, 'Comfort');
|
||||||
|
const proc = await launch({
|
||||||
|
gamePath: comfortDir,
|
||||||
|
resourcePath: minecraftDir,
|
||||||
|
javaPath,
|
||||||
|
version: '1.21.4-fabric0.16.14',
|
||||||
|
extraJVMArgs: ['-Dlog4j2.formatMsgNoLookups=true', '-Xmx2G'], // Добавляем больше памяти
|
||||||
|
});
|
||||||
|
|
||||||
|
// Логирование
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
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;
|
||||||
@ -75,6 +587,7 @@ const createWindow = async () => {
|
|||||||
height: 728,
|
height: 728,
|
||||||
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'),
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
// 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';
|
||||||
|
|
||||||
const electronHandler = {
|
const electronHandler = {
|
||||||
ipcRenderer: {
|
ipcRenderer: {
|
||||||
@ -21,6 +23,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: string, ...args: unknown[]) {
|
||||||
|
return ipcRenderer.invoke(channel, ...args);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,62 +1,41 @@
|
|||||||
/*
|
@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(200.96deg, #000000, #3b4187);
|
||||||
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 {
|
|
||||||
opacity: 1;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Hello {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
}
|
||||||
|
@ -1,50 +1,78 @@
|
|||||||
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';
|
||||||
|
|
||||||
function Hello() {
|
const AuthCheck = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||||
<div>
|
|
||||||
<div className="Hello">
|
useEffect(() => {
|
||||||
<img width="200" alt="icon" src={icon} />
|
const checkAuth = async () => {
|
||||||
</div>
|
try {
|
||||||
<h1>electron-react-boilerplate</h1>
|
const savedConfig = localStorage.getItem('launcher_config');
|
||||||
<div className="Hello">
|
if (savedConfig) {
|
||||||
<a
|
const config = JSON.parse(savedConfig);
|
||||||
href="https://electron-react-boilerplate.js.org/"
|
if (config.accessToken) {
|
||||||
target="_blank"
|
// Можно добавить дополнительную проверку токена
|
||||||
rel="noreferrer"
|
const isValid = await validateToken(config.accessToken);
|
||||||
>
|
setIsAuthenticated(isValid);
|
||||||
<button type="button">
|
return;
|
||||||
<span role="img" aria-label="books">
|
}
|
||||||
📚
|
}
|
||||||
</span>
|
setIsAuthenticated(false);
|
||||||
Read our docs
|
} catch (error) {
|
||||||
</button>
|
console.error('Ошибка проверки авторизации:', error);
|
||||||
</a>
|
setIsAuthenticated(false);
|
||||||
<a
|
}
|
||||||
href="https://github.com/sponsors/electron-react-boilerplate"
|
};
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
checkAuth();
|
||||||
>
|
}, []);
|
||||||
<button type="button">
|
|
||||||
<span role="img" aria-label="folded hands">
|
const validateToken = async (token: string) => {
|
||||||
🙏
|
try {
|
||||||
</span>
|
const response = await fetch('https://authserver.ely.by/auth/validate', {
|
||||||
Donate
|
method: 'POST',
|
||||||
</button>
|
headers: {
|
||||||
</a>
|
'Content-Type': 'application/json',
|
||||||
</div>
|
},
|
||||||
</div>
|
body: JSON.stringify({ accessToken: token }),
|
||||||
);
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isAuthenticated === null) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
return isAuthenticated ? children : <Navigate to="/login" replace />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Hello />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<AuthCheck>
|
||||||
|
<LaunchPage />
|
||||||
|
</AuthCheck>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
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;
|
177
src/renderer/hooks/useAuth.ts
Normal file
177
src/renderer/hooks/useAuth.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const useAuth = () => {
|
||||||
|
const [status, setStatus] = useState('');
|
||||||
|
|
||||||
|
const validateSession = async (accessToken: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://authserver.ely.by/auth/validate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
accessToken: accessToken,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Ошибка при проверке токена: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshSession = async (accessToken: string, clientToken: string) => {
|
||||||
|
try {
|
||||||
|
const refreshData = {
|
||||||
|
accessToken: accessToken,
|
||||||
|
clientToken: clientToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch('https://authserver.ely.by/auth/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(refreshData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
const newAccessToken = data.accessToken;
|
||||||
|
const profile = data.selectedProfile;
|
||||||
|
const uuid = profile.id;
|
||||||
|
const name = profile.name;
|
||||||
|
|
||||||
|
if (newAccessToken && uuid && name) {
|
||||||
|
return {
|
||||||
|
accessToken: newAccessToken,
|
||||||
|
uuid: uuid,
|
||||||
|
username: name,
|
||||||
|
clientToken: clientToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Ошибка при обновлении сессии: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const authenticateWithElyBy = async (
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
saveConfig: Function,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const clientToken = crypto.randomUUID();
|
||||||
|
const authData = {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
clientToken: clientToken,
|
||||||
|
requestUser: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`Аутентификация пользователя ${username} на Ely.by...`);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
'https://authserver.ely.by/auth/authenticate',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(authData),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
const accessToken = responseData.accessToken;
|
||||||
|
const profile = responseData.selectedProfile;
|
||||||
|
const uuid = profile.id;
|
||||||
|
const name = profile.name;
|
||||||
|
|
||||||
|
if (accessToken && uuid && name) {
|
||||||
|
saveConfig(
|
||||||
|
username,
|
||||||
|
4096, // default memory
|
||||||
|
accessToken,
|
||||||
|
clientToken,
|
||||||
|
'',
|
||||||
|
password,
|
||||||
|
);
|
||||||
|
console.log(`Аутентификация успешна: UUID=${uuid}, Username=${name}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: accessToken,
|
||||||
|
uuid: uuid,
|
||||||
|
username: name,
|
||||||
|
clientToken: clientToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (responseData.error === 'Account protected with two factor auth') {
|
||||||
|
const totpToken = prompt(
|
||||||
|
'Введите код двухфакторной аутентификации:',
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (totpToken) {
|
||||||
|
authData.password = `${password}:${totpToken}`;
|
||||||
|
const totpResponse = await fetch(
|
||||||
|
'https://authserver.ely.by/auth/authenticate',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(authData),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (totpResponse.ok) {
|
||||||
|
const totpData = await totpResponse.json();
|
||||||
|
const newAccessToken = totpData.accessToken;
|
||||||
|
const newProfile = totpData.selectedProfile;
|
||||||
|
const newUuid = newProfile.id;
|
||||||
|
const newName = newProfile.name;
|
||||||
|
|
||||||
|
if (newAccessToken && newUuid && newName) {
|
||||||
|
saveConfig(
|
||||||
|
username,
|
||||||
|
4096, // default memory
|
||||||
|
newAccessToken,
|
||||||
|
clientToken,
|
||||||
|
'',
|
||||||
|
password,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
accessToken: newAccessToken,
|
||||||
|
uuid: newUuid,
|
||||||
|
username: newName,
|
||||||
|
clientToken: clientToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(responseData.error || 'Ошибка авторизации');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Ошибка авторизации: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
setStatus,
|
||||||
|
validateSession,
|
||||||
|
refreshSession,
|
||||||
|
authenticateWithElyBy,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAuth;
|
69
src/renderer/hooks/useConfig.ts
Normal file
69
src/renderer/hooks/useConfig.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const useConfig = () => {
|
||||||
|
const [config, setConfig] = useState({
|
||||||
|
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: '',
|
||||||
|
memory: 4096,
|
||||||
|
comfortVersion: '',
|
||||||
|
accessToken: '',
|
||||||
|
clientToken: '',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedConfig = loadInitialConfig();
|
||||||
|
setConfig(savedConfig);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveConfig = (
|
||||||
|
username: string,
|
||||||
|
memory: number,
|
||||||
|
accessToken = '',
|
||||||
|
clientToken = '',
|
||||||
|
comfortVersion = '',
|
||||||
|
password = '',
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const newConfig = {
|
||||||
|
username,
|
||||||
|
memory,
|
||||||
|
accessToken: accessToken || config.accessToken,
|
||||||
|
clientToken: clientToken || config.clientToken,
|
||||||
|
comfortVersion: comfortVersion || config.comfortVersion,
|
||||||
|
password: password || config.password,
|
||||||
|
};
|
||||||
|
setConfig(newConfig);
|
||||||
|
localStorage.setItem('launcher_config', JSON.stringify(newConfig));
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Ошибка при сохранении конфигурации: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setConfig((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return { config, setConfig, saveConfig, handleInputChange };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useConfig;
|
166
src/renderer/pages/LaunchPage.tsx
Normal file
166
src/renderer/pages/LaunchPage.tsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { Box, Typography, Button, Snackbar, Alert } from '@mui/material';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LaunchPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
|
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||||
|
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('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedConfig = localStorage.getItem('launcher_config');
|
||||||
|
if (!savedConfig || !JSON.parse(savedConfig).accessToken) {
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.electron.ipcRenderer.on('download-progress', (progress: any) => {
|
||||||
|
setDownloadProgress(progress as number);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавляем слушатель для статуса установки
|
||||||
|
window.electron.ipcRenderer.on('installation-progress', (data: any) => {
|
||||||
|
setInstallStatus((data as { status: string }).status);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем слушатель для статуса установки
|
||||||
|
window.electron.ipcRenderer.on('installation-status', (data: any) => {
|
||||||
|
const { step, message } = data as { step: string; message: string };
|
||||||
|
setInstallStep(step);
|
||||||
|
setInstallMessage(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.electron.ipcRenderer.removeAllListeners('download-progress');
|
||||||
|
window.electron.ipcRenderer.removeAllListeners('installation-progress');
|
||||||
|
window.electron.ipcRenderer.removeAllListeners('installation-status');
|
||||||
|
};
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem('launcher_config');
|
||||||
|
navigate('/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Сначала проверяем и обновляем файлы
|
||||||
|
const downloadResult = await window.electron.ipcRenderer.invoke(
|
||||||
|
'download-and-extract',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (downloadResult?.success) {
|
||||||
|
if (downloadResult.updated) {
|
||||||
|
showNotification(
|
||||||
|
`Сборка успешно обновлена до версии ${downloadResult.version}`,
|
||||||
|
'success',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showNotification(
|
||||||
|
`Установлена актуальная версия сборки ${downloadResult.version}`,
|
||||||
|
'info',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Затем запускаем Minecraft
|
||||||
|
const launchResult =
|
||||||
|
await window.electron.ipcRenderer.invoke('launch-minecraft');
|
||||||
|
|
||||||
|
if (launchResult?.success) {
|
||||||
|
showNotification('Minecraft успешно запущен!', 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showNotification(`Ошибка: ${error.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setIsDownloading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<Typography variant="h4" sx={{ mb: 3 }}>
|
||||||
|
Добро пожаловать в лаунчер
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{isDownloading ? (
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography>Загрузка и установка: {downloadProgress}%</Typography>
|
||||||
|
{installMessage && (
|
||||||
|
<Typography variant="body1" sx={{ mt: 1, color: 'white' }}>
|
||||||
|
{installMessage}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{installStep && (
|
||||||
|
<Typography variant="body2" sx={{ color: 'white' }}>
|
||||||
|
Шаг: {installStep}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
onClick={handleLaunchMinecraft}
|
||||||
|
>
|
||||||
|
Запустить Minecraft
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button onClick={handleLogout} variant="contained" color="error">
|
||||||
|
Выйти
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Snackbar
|
||||||
|
open={notification.open}
|
||||||
|
autoHideDuration={6000}
|
||||||
|
onClose={handleCloseNotification}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
onClose={handleCloseNotification}
|
||||||
|
severity={notification.severity}
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{notification.message}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LaunchPage;
|
104
src/renderer/pages/Login.tsx
Normal file
104
src/renderer/pages/Login.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { Box, Typography } from '@mui/material';
|
||||||
|
import useConfig from '../hooks/useConfig';
|
||||||
|
import useAuth from '../hooks/useAuth';
|
||||||
|
import AuthForm from '../components/Login/AuthForm';
|
||||||
|
import MemorySlider from '../components/Login/MemorySlider';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
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>
|
||||||
|
<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>
|
||||||
|
<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