Compare commits

...

2 Commits

Author SHA1 Message Date
942066ea76 feat: improve Minecraft version handling 2025-07-13 23:37:46 +05:00
815ce286f7 add: VersionExplorer, don't work first run Minecraft 2025-07-08 03:29:36 +05:00
10 changed files with 908 additions and 181 deletions

View File

@ -119,7 +119,7 @@ const createWindow = async () => {
width: 1024, width: 1024,
height: 850, height: 850,
autoHideMenuBar: true, autoHideMenuBar: true,
resizable: false, resizable: true,
frame: false, frame: false,
icon: getAssetPath('icon.png'), icon: getAssetPath('icon.png'),
webPreferences: { webPreferences: {

View File

@ -804,6 +804,106 @@ export function initMinecraftHandlers() {
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
// ПРОБЛЕМА: У вас два обработчика для одного и того же канала 'get-installed-versions'
// РЕШЕНИЕ: Объединим логику в один обработчик, а из второго обработчика вызовем функцию getInstalledVersions
// Сначала создаем общую функцию для получения установленных версий
function getInstalledVersions() {
try {
const appPath = path.dirname(app.getPath('exe'));
const minecraftDir = path.join(appPath, '.minecraft');
const versionsDir = path.join(minecraftDir, 'versions');
if (!fs.existsSync(versionsDir)) {
return { success: true, versions: [] };
}
const items = fs.readdirSync(versionsDir);
const versions = [];
for (const item of items) {
const versionPath = path.join(versionsDir, item);
if (fs.statSync(versionPath).isDirectory()) {
// Проверяем, есть ли конфигурация для пакета
const versionJsonPath = path.join(versionPath, `${item}.json`);
let versionInfo = {
id: item,
name: item,
version: item,
};
if (fs.existsSync(versionJsonPath)) {
try {
const versionData = JSON.parse(
fs.readFileSync(versionJsonPath, 'utf8'),
);
versionInfo.version = versionData.id || item;
} catch (error) {
console.warn(`Ошибка при чтении файла версии ${item}:`, error);
}
}
versions.push(versionInfo);
}
}
return { success: true, versions };
} catch (error) {
console.error('Ошибка при получении установленных версий:', error);
return { success: false, error: error.message, versions: [] };
}
}
// Регистрируем обработчик для get-installed-versions
ipcMain.handle('get-installed-versions', async () => {
return getInstalledVersions();
});
// Обработчик get-available-versions использует функцию getInstalledVersions
ipcMain.handle('get-available-versions', async (event, { gistUrl }) => {
try {
// Используем URL из параметров или значение по умолчанию
const url =
gistUrl ||
'https://gist.githubusercontent.com/DIKER0K/06cd12fb3a4d08b1f0f8c763a7d05e06/raw/versions.json';
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Failed to fetch versions from Gist: ${response.status} ${response.statusText}`,
);
}
const versions = await response.json();
// Получаем уже установленные версии
const installedResult = getInstalledVersions();
const installedVersions = installedResult.success
? installedResult.versions
: [];
// Добавляем флаг installed к каждой версии
const versionsWithInstallStatus = versions.map((version: any) => {
const isInstalled = installedVersions.some(
(installed: any) => installed.id === version.id,
);
return {
...version,
installed: isInstalled,
};
});
return {
success: true,
versions: versionsWithInstallStatus,
};
} catch (error) {
console.error('Ошибка при получении доступных версий:', error);
return { success: false, error: error.message, versions: [] };
}
});
} }
// Добавляем обработчики IPC для аутентификации // Добавляем обработчики IPC для аутентификации
@ -979,3 +1079,57 @@ export function initPackConfigHandlers() {
} }
}); });
} }
// Добавляем после обработчика get-available-versions
ipcMain.handle('get-version-config', async (event, { versionId }) => {
try {
const appPath = path.dirname(app.getPath('exe'));
const minecraftDir = path.join(appPath, '.minecraft');
const versionsDir = path.join(minecraftDir, 'versions');
const versionPath = path.join(versionsDir, versionId);
// Проверяем существование директории версии
if (!fs.existsSync(versionPath)) {
return { success: false, error: `Версия ${versionId} не найдена` };
}
// Проверяем конфигурационный файл версии
const configPath = path.join(versionPath, 'popa-launcher-config.json');
// Определяем базовые настройки по умолчанию
let config = {
downloadUrl: '',
apiReleaseUrl: '',
versionFileName: `${versionId}_version.txt`,
packName: versionId,
memory: 4096,
baseVersion: '1.21.4',
serverIp: 'popa-popa.ru',
fabricVersion: '0.16.14',
preserveFiles: ['popa-launcher-config.json'],
};
// Если это Comfort, используем настройки по умолчанию
if (versionId === 'Comfort') {
config.downloadUrl =
'https://github.com/DIKER0K/Comfort/releases/latest/download/Comfort.zip';
config.apiReleaseUrl =
'https://api.github.com/repos/DIKER0K/Comfort/releases/latest';
}
// Если есть конфигурационный файл, загружаем из него
if (fs.existsSync(configPath)) {
try {
const savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
config = { ...config, ...savedConfig };
} catch (error) {
console.warn(`Ошибка чтения конфигурации ${versionId}:`, error);
}
}
return { success: true, config };
} catch (error) {
console.error('Ошибка получения настроек версии:', error);
return { success: false, error: error.message };
}
});

View File

@ -11,7 +11,9 @@ export type Channels =
| 'save-pack-config' | 'save-pack-config'
| 'load-pack-config' | 'load-pack-config'
| 'update-available' | 'update-available'
| 'install-update'; | 'install-update'
| 'get-installed-versions'
| 'get-available-versions';
const electronHandler = { const electronHandler = {
ipcRenderer: { ipcRenderer: {

View File

@ -47,3 +47,7 @@ h4 {
h5 { h5 {
font-family: 'Benzin-Bold' !important; font-family: 'Benzin-Bold' !important;
} }
h6 {
font-family: 'Benzin-Bold' !important;
}

View File

@ -3,6 +3,7 @@ import {
Routes, Routes,
Route, Route,
Navigate, Navigate,
useNavigate,
} from 'react-router-dom'; } from 'react-router-dom';
import Login from './pages/Login'; import Login from './pages/Login';
import LaunchPage from './pages/LaunchPage'; import LaunchPage from './pages/LaunchPage';
@ -12,19 +13,7 @@ import TopBar from './components/TopBar';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import MinecraftBackround from './components/MinecraftBackround'; import MinecraftBackround from './components/MinecraftBackround';
import { Notifier } from './components/Notifier'; import { Notifier } from './components/Notifier';
import { VersionsExplorer } from './pages/VersionsExplorer';
// Переместите launchOptions сюда, вне компонентов
const launchOptions = {
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',
memory: 4096,
baseVersion: '1.21.4',
serverIp: 'popa-popa.ru',
fabricVersion: '0.16.14', // Уберите префикс "fabric"
};
const AuthCheck = ({ children }: { children: ReactNode }) => { const AuthCheck = ({ children }: { children: ReactNode }) => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null); const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
@ -54,15 +43,37 @@ const AuthCheck = ({ children }: { children: ReactNode }) => {
const validateToken = async (token: string) => { const validateToken = async (token: string) => {
try { try {
const response = await fetch('https://authserver.ely.by/auth/validate', { // Используем IPC для валидации токена через main процесс
method: 'POST', const result = await window.electron.ipcRenderer.invoke(
headers: { 'validate-token',
'Content-Type': 'application/json', token,
}, );
body: JSON.stringify({ accessToken: token }),
}); // Если токен недействителен, очищаем сохраненные данные в localStorage
return response.ok; if (!result.valid) {
console.log(
'Токен недействителен, очищаем данные авторизации из localStorage',
);
const savedConfig = localStorage.getItem('launcher_config');
if (savedConfig) {
const config = JSON.parse(savedConfig);
// Сохраняем только логин и другие настройки, но удаляем токены
const cleanedConfig = {
username: config.username,
memory: config.memory || 4096,
comfortVersion: config.comfortVersion || '',
password: '', // Очищаем пароль для безопасности
};
localStorage.setItem(
'launcher_config',
JSON.stringify(cleanedConfig),
);
}
}
return result.valid;
} catch (error) { } catch (error) {
console.error('Ошибка проверки токена:', error);
return false; return false;
} }
}; };
@ -91,6 +102,7 @@ const App = () => {
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
overflowX: 'hidden',
}} }}
> >
<MinecraftBackround /> <MinecraftBackround />
@ -102,7 +114,15 @@ const App = () => {
path="/" path="/"
element={ element={
<AuthCheck> <AuthCheck>
<LaunchPage launchOptions={launchOptions} /> <VersionsExplorer />
</AuthCheck>
}
/>
<Route
path="/launch/:versionId"
element={
<AuthCheck>
<LaunchPage />
</AuthCheck> </AuthCheck>
} }
/> />

View File

@ -0,0 +1,92 @@
import { Box, Typography, Button, Modal } from '@mui/material';
import React from 'react';
import MemorySlider from '../Login/MemorySlider';
import FilesSelector from '../FilesSelector';
interface SettingsModalProps {
open: boolean;
onClose: () => void;
config: {
memory: number;
preserveFiles: string[];
};
onConfigChange: (newConfig: {
memory: number;
preserveFiles: string[];
}) => void;
packName: string;
onSave: () => void;
}
const SettingsModal = ({
open,
onClose,
config,
onConfigChange,
packName,
onSave,
}: SettingsModalProps) => {
return (
<Modal
open={open}
onClose={onClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
background:
'linear-gradient(-242.94deg, #000000 39.07%, #3b4187 184.73%)',
border: '2px solid #000',
boxShadow: 24,
p: 4,
borderRadius: '3vw',
gap: '1vh',
display: 'flex',
flexDirection: 'column',
}}
>
<Typography id="modal-modal-title" variant="body1" component="h2">
Файлы и папки, которые будут сохранены после переустановки сборки
</Typography>
<FilesSelector
packName={packName}
initialSelected={config.preserveFiles}
onSelectionChange={(selected) => {
onConfigChange({ ...config, preserveFiles: selected });
}}
/>
<Typography variant="body1" sx={{ color: 'white' }}>
Оперативная память выделенная для Minecraft
</Typography>
<MemorySlider
memory={config.memory}
onChange={(e, value) => {
onConfigChange({ ...config, memory: value as number });
}}
/>
<Button
variant="contained"
color="success"
onClick={() => {
onSave();
onClose();
}}
sx={{
borderRadius: '3vw',
fontFamily: 'Benzin-Bold',
}}
>
Сохранить
</Button>
</Box>
</Modal>
);
};
export default SettingsModal;

View File

@ -1,7 +1,8 @@
import { Box, Button, Typography } from '@mui/material'; import { Box, Button, Typography } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import MinimizeIcon from '@mui/icons-material/Minimize'; import MinimizeIcon from '@mui/icons-material/Minimize';
import { useLocation } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
declare global { declare global {
interface Window { interface Window {
@ -24,6 +25,12 @@ export default function TopBar({ onRegister }: TopBarProps) {
// Получаем текущий путь // Получаем текущий путь
const location = useLocation(); const location = useLocation();
const isLoginPage = location.pathname === '/login'; const isLoginPage = location.pathname === '/login';
const isLaunchPage = location.pathname.startsWith('/launch');
const navigate = useNavigate();
const handleLaunchPage = () => {
navigate('/');
};
return ( return (
<Box <Box
@ -38,9 +45,37 @@ export default function TopBar({ onRegister }: TopBarProps) {
width: '100%', width: '100%',
WebkitAppRegion: 'drag', WebkitAppRegion: 'drag',
overflow: 'hidden', overflow: 'hidden',
justifyContent: 'flex-end', // Всё содержимое справа justifyContent: 'space-between', // Всё содержимое справа
}} }}
> >
<Box
sx={{
display: 'flex',
WebkitAppRegion: 'no-drag',
gap: '2vw',
padding: '1em',
alignItems: 'center',
}}
>
{isLaunchPage && (
<Button
variant="outlined"
color="primary"
onClick={() => handleLaunchPage()}
sx={{
width: '3em',
height: '3em',
borderRadius: '50%',
border: 'unset',
color: 'white',
minWidth: 'unset',
minHeight: 'unset',
}}
>
<ArrowBackIcon />
</Button>
)}
</Box>
{/* Правая часть со всеми кнопками */} {/* Правая часть со всеми кнопками */}
<Box <Box
sx={{ sx={{

View File

@ -5,16 +5,14 @@ import {
Snackbar, Snackbar,
Alert, Alert,
LinearProgress, LinearProgress,
Modal,
} from '@mui/material'; } from '@mui/material';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import ServerStatus from '../components/ServerStatus/ServerStatus'; import ServerStatus from '../components/ServerStatus/ServerStatus';
import PopaPopa from '../components/popa-popa'; import PopaPopa from '../components/popa-popa';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import React from 'react'; import React from 'react';
import MemorySlider from '../components/Login/MemorySlider'; import SettingsModal from '../components/Settings/SettingsModal';
import FilesSelector from '../components/FilesSelector';
declare global { declare global {
interface Window { interface Window {
@ -30,7 +28,9 @@ declare global {
// Определяем тип для props // Определяем тип для props
interface LaunchPageProps { interface LaunchPageProps {
launchOptions: { onLaunchPage?: () => void;
launchOptions?: {
// Делаем опциональным
downloadUrl: string; downloadUrl: string;
apiReleaseUrl: string; apiReleaseUrl: string;
versionFileName: string; versionFileName: string;
@ -42,8 +42,14 @@ interface LaunchPageProps {
}; };
} }
const LaunchPage = ({ launchOptions }: LaunchPageProps) => { const LaunchPage = ({
onLaunchPage,
launchOptions = {} as any,
}: LaunchPageProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { versionId } = useParams();
const [versionConfig, setVersionConfig] = useState<any>(null);
// Начальное состояние должно быть пустым или с минимальными значениями // Начальное состояние должно быть пустым или с минимальными значениями
const [config, setConfig] = useState<{ const [config, setConfig] = useState<{
memory: number; memory: number;
@ -101,27 +107,82 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
}, [navigate]); }, [navigate]);
useEffect(() => { useEffect(() => {
// Загрузка конфигурации сборки при монтировании const fetchVersionConfig = async () => {
const loadPackConfig = async () => { if (!versionId) return;
try { try {
// Сначала проверяем, есть ли конфигурация в localStorage
const savedConfig = localStorage.getItem('selected_version_config');
if (savedConfig) {
const parsedConfig = JSON.parse(savedConfig);
setVersionConfig(parsedConfig);
// Устанавливаем значения памяти и preserveFiles из конфигурации
setConfig({
memory: parsedConfig.memory || 4096,
preserveFiles: parsedConfig.preserveFiles || [],
});
// Очищаем localStorage
localStorage.removeItem('selected_version_config');
return;
}
// Если нет в localStorage, запрашиваем с сервера
const result = await window.electron.ipcRenderer.invoke( const result = await window.electron.ipcRenderer.invoke(
'load-pack-config', 'get-version-config',
{ { versionId },
packName: launchOptions.packName,
},
); );
if (result.success && result.config) { if (result.success) {
// Полностью заменяем config значениями из файла setVersionConfig(result.config);
setConfig(result.config); setConfig({
memory: result.config.memory || 4096,
preserveFiles: result.config.preserveFiles || [],
});
} else {
// Если не удалось получить конфигурацию, используем значения по умолчанию
const defaultConfig = {
downloadUrl: '',
apiReleaseUrl: '',
versionFileName: `${versionId}_version.txt`,
packName: versionId || 'Comfort',
memory: 4096,
baseVersion: '1.21.4',
serverIp: 'popa-popa.ru',
fabricVersion: '0.16.14',
preserveFiles: ['popa-launcher-config.json'],
};
setVersionConfig(defaultConfig);
setConfig({
memory: defaultConfig.memory,
preserveFiles: defaultConfig.preserveFiles || [],
});
} }
} catch (error) { } catch (error) {
console.error('Ошибка при загрузке настроек:', error); console.error('Ошибка при получении настроек версии:', error);
// Используем значения по умолчанию
const defaultConfig = {
downloadUrl: '',
apiReleaseUrl: '',
versionFileName: `${versionId}_version.txt`,
packName: versionId || 'Comfort',
memory: 4096,
baseVersion: '1.21.4',
serverIp: 'popa-popa.ru',
fabricVersion: '0.16.14',
preserveFiles: ['popa-launcher-config.json'],
};
setVersionConfig(defaultConfig);
setConfig({
memory: defaultConfig.memory,
preserveFiles: defaultConfig.preserveFiles || [],
});
} }
}; };
loadPackConfig(); fetchVersionConfig();
}, [launchOptions.packName]); }, [versionId]);
const showNotification = ( const showNotification = (
message: string, message: string,
@ -134,94 +195,86 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
setNotification({ ...notification, open: false }); setNotification({ ...notification, open: false });
}; };
// Функция для запуска игры с настройками выбранной версии
const handleLaunchMinecraft = async () => { const handleLaunchMinecraft = async () => {
try { try {
setIsDownloading(true); setIsDownloading(true);
setDownloadProgress(0); setDownloadProgress(0);
setBuffer(10); setBuffer(10);
// Загружаем настройки сборки // Используем настройки выбранной версии или дефолтные
const result = await window.electron.ipcRenderer.invoke( const currentConfig = versionConfig || {
'load-pack-config', packName: versionId || 'Comfort',
{ memory: 4096,
packName: launchOptions.packName, baseVersion: '1.21.4',
}, serverIp: 'popa-popa.ru',
); fabricVersion: '0.16.14',
preserveFiles: [],
};
// Используйте уже существующий state вместо локальной переменной // Проверяем, является ли это ванильной версией
if (result.success && result.config) { const isVanillaVersion =
setConfig(result.config); // Обновляем state !currentConfig.downloadUrl || currentConfig.downloadUrl === '';
if (!isVanillaVersion) {
// Если это не ванильная версия, выполняем загрузку и распаковку
const packOptions = {
downloadUrl: currentConfig.downloadUrl,
apiReleaseUrl: currentConfig.apiReleaseUrl,
versionFileName: currentConfig.versionFileName,
packName: versionId || currentConfig.packName,
preserveFiles: config.preserveFiles,
};
// Передаем опции для скачивания
const downloadResult = await window.electron.ipcRenderer.invoke(
'download-and-extract',
packOptions,
);
if (downloadResult?.success) {
if (downloadResult.updated) {
showNotification(
`Сборка ${downloadResult.packName} успешно обновлена до версии ${downloadResult.version}`,
'success',
);
} else {
showNotification(
`Установлена актуальная версия сборки ${downloadResult.packName} (${downloadResult.version})`,
'info',
);
}
}
} else {
showNotification('Запускаем ванильный Minecraft...', 'info');
} }
// Опции для запуска Minecraft
const savedConfig = JSON.parse( const savedConfig = JSON.parse(
localStorage.getItem('launcher_config') || '{}', localStorage.getItem('launcher_config') || '{}',
); );
// Опции для скачивания сборки const options = {
const packOptions = { accessToken: savedConfig.accessToken,
downloadUrl: launchOptions.downloadUrl, uuid: savedConfig.uuid,
apiReleaseUrl: launchOptions.apiReleaseUrl, username: savedConfig.username,
versionFileName: launchOptions.versionFileName, memory: config.memory,
packName: launchOptions.packName, baseVersion: currentConfig.baseVersion,
preserveFiles: config.preserveFiles, packName: versionId || currentConfig.packName,
serverIp: currentConfig.serverIp,
fabricVersion: currentConfig.fabricVersion,
// Для ванильной версии устанавливаем флаг
isVanillaVersion: isVanillaVersion,
versionToLaunchOverride: isVanillaVersion ? versionId : undefined,
}; };
// Передаем опции для скачивания const launchResult = await window.electron.ipcRenderer.invoke(
const downloadResult = await window.electron.ipcRenderer.invoke( 'launch-minecraft',
'download-and-extract', options,
packOptions,
); );
if (downloadResult?.success) { if (launchResult?.success) {
let needsSecondAttempt = false; showNotification('Minecraft успешно запущен!', 'success');
if (downloadResult.updated) {
showNotification(
`Сборка ${downloadResult.packName} успешно обновлена до версии ${downloadResult.version}`,
'success',
);
needsSecondAttempt = true;
} else {
showNotification(
`Установлена актуальная версия сборки ${downloadResult.packName} (${downloadResult.version})`,
'info',
);
}
// Опции для запуска
const options = {
accessToken: savedConfig.accessToken,
uuid: savedConfig.uuid,
username: savedConfig.username,
memory: config.memory, // Используем state
baseVersion: launchOptions.baseVersion,
packName: launchOptions.packName,
serverIp: launchOptions.serverIp,
fabricVersion: launchOptions.fabricVersion,
};
const launchResult = await window.electron.ipcRenderer.invoke(
'launch-minecraft',
options,
);
if (needsSecondAttempt) {
showNotification(
'Завершаем настройку компонентов, повторный запуск...',
'info',
);
await new Promise((resolve) => setTimeout(resolve, 2000));
const secondAttempt = await window.electron.ipcRenderer.invoke(
'launch-minecraft',
options,
);
showNotification('Minecraft успешно запущен!', 'success');
} else if (launchResult?.success) {
showNotification('Minecraft успешно запущен!', 'success');
}
} }
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
@ -240,7 +293,7 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
}; };
await window.electron.ipcRenderer.invoke('save-pack-config', { await window.electron.ipcRenderer.invoke('save-pack-config', {
packName: launchOptions.packName, packName: versionId || versionConfig?.packName || 'Comfort',
config: configToSave, config: configToSave,
}); });
@ -285,7 +338,7 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
<Box> <Box>
<ServerStatus <ServerStatus
serverIp={launchOptions.serverIp} serverIp={versionConfig?.serverIp || 'popa-popa.ru'}
refreshInterval={30000} refreshInterval={30000}
/> />
</Box> </Box>
@ -365,65 +418,14 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
</Alert> </Alert>
</Snackbar> </Snackbar>
<Modal <SettingsModal
open={open} open={open}
onClose={handleClose} onClose={handleClose}
aria-labelledby="modal-modal-title" config={config}
aria-describedby="modal-modal-description" onConfigChange={setConfig}
> packName={versionId || versionConfig?.packName || 'Comfort'}
<Box onSave={savePackConfig}
sx={{ />
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
background:
'linear-gradient(-242.94deg, #000000 39.07%, #3b4187 184.73%)',
border: '2px solid #000',
boxShadow: 24,
p: 4,
borderRadius: '3vw',
gap: '1vh',
display: 'flex',
flexDirection: 'column',
}}
>
<Typography id="modal-modal-title" variant="body1" component="h2">
Файлы и папки, которые будут сохранены после переустановки сборки
</Typography>
<FilesSelector
packName={launchOptions.packName}
initialSelected={config.preserveFiles} // Передаем текущие выбранные файлы
onSelectionChange={(selected) => {
setConfig((prev) => ({ ...prev, preserveFiles: selected }));
}}
/>
<Typography variant="body1" sx={{ color: 'white' }}>
Оперативная память выделенная для Minecraft
</Typography>
<MemorySlider
memory={config.memory}
onChange={(e, value) => {
setConfig((prev) => ({ ...prev, memory: value as number }));
}}
/>
<Button
variant="contained"
color="success"
onClick={() => {
savePackConfig();
handleClose();
}}
sx={{
borderRadius: '3vw',
fontFamily: 'Benzin-Bold',
}}
>
Сохранить
</Button>
</Box>
</Modal>
</Box> </Box>
); );
}; };

View File

@ -38,13 +38,25 @@ const Login = () => {
console.log( console.log(
'Не удалось обновить токен, требуется новая авторизация', 'Не удалось обновить токен, требуется новая авторизация',
); );
const newSession = await authenticateWithElyBy( // Очищаем недействительные токены
config.username, saveConfig({
config.password, accessToken: '',
saveConfig, clientToken: '',
); });
if (!newSession) {
console.log('Авторизация не удалась'); // Пытаемся выполнить новую авторизацию
if (config.password) {
const newSession = await authenticateWithElyBy(
config.username,
config.password,
saveConfig,
);
if (!newSession) {
console.log('Авторизация не удалась');
return;
}
} else {
console.log('Требуется ввод пароля для новой авторизации');
return; return;
} }
} }
@ -53,6 +65,13 @@ const Login = () => {
} }
} else { } else {
console.log('Токен отсутствует, выполняем авторизацию...'); console.log('Токен отсутствует, выполняем авторизацию...');
// Проверяем наличие пароля
if (!config.password) {
console.log('Ошибка: не указан пароль');
alert('Введите пароль!');
return;
}
const session = await authenticateWithElyBy( const session = await authenticateWithElyBy(
config.username, config.username,
config.password, config.password,
@ -68,6 +87,11 @@ const Login = () => {
navigate('/'); navigate('/');
} catch (error) { } catch (error) {
console.log(`ОШИБКА при авторизации: ${error.message}`); console.log(`ОШИБКА при авторизации: ${error.message}`);
// Очищаем недействительные токены при ошибке
saveConfig({
accessToken: '',
clientToken: '',
});
} }
}; };

View File

@ -0,0 +1,394 @@
import { useEffect, useState } from 'react';
import {
Box,
Typography,
Grid,
Card,
CardMedia,
CardContent,
CardActions,
Button,
CircularProgress,
Modal,
List,
ListItem,
ListItemText,
IconButton,
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
import AddIcon from '@mui/icons-material/Add';
import DownloadIcon from '@mui/icons-material/Download';
interface VersionCardProps {
id: string;
name: string;
imageUrl: string;
version: string;
onSelect: (id: string) => void;
}
const VersionCard: React.FC<VersionCardProps> = ({
id,
name,
imageUrl,
version,
onSelect,
}) => {
return (
<Card
sx={{
backgroundColor: 'rgba(30, 30, 50, 0.8)',
backdropFilter: 'blur(10px)',
width: '35vw',
height: '35vh',
minWidth: 'unset',
minHeight: 'unset',
display: 'flex',
flexDirection: 'column',
borderRadius: '16px',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)',
transition: 'transform 0.3s, box-shadow 0.3s',
overflow: 'hidden',
cursor: 'pointer',
}}
onClick={() => onSelect(id)}
>
<CardContent
sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '10%',
}}
>
<Typography
gutterBottom
variant="h5"
component="div"
sx={{
fontWeight: 'bold',
color: '#ffffff',
fontSize: '1.5rem',
}}
>
{name}
</Typography>
</CardContent>
</Card>
);
};
interface VersionInfo {
id: string;
name: string;
version: string;
imageUrl?: string;
config?: {
downloadUrl: string;
apiReleaseUrl: string;
versionFileName: string;
packName: string;
memory: number;
baseVersion: string;
serverIp: string;
fabricVersion: string;
preserveFiles: string[];
};
}
interface AvailableVersionInfo {
id: string;
name: string;
version: string;
imageUrl?: string;
config: {
downloadUrl: string;
apiReleaseUrl: string;
versionFileName: string;
packName: string;
memory: number;
baseVersion: string;
serverIp: string;
fabricVersion: string;
preserveFiles: string[];
};
}
// В компоненте VersionsExplorer
export const VersionsExplorer = () => {
const [installedVersions, setInstalledVersions] = useState<VersionInfo[]>([]);
const [availableVersions, setAvailableVersions] = useState<
AvailableVersionInfo[]
>([]);
const [loading, setLoading] = useState(true);
const [modalOpen, setModalOpen] = useState(false);
const [downloadLoading, setDownloadLoading] = useState<string | null>(null);
const navigate = useNavigate();
useEffect(() => {
const fetchVersions = async () => {
try {
setLoading(true);
// Получаем список установленных версий через IPC
const installedResult = await window.electron.ipcRenderer.invoke(
'get-installed-versions',
);
if (installedResult.success) {
setInstalledVersions(installedResult.versions);
}
// Получаем доступные версии с GitHub Gist
const availableResult = await window.electron.ipcRenderer.invoke(
'get-available-versions',
{
gistUrl:
'https://gist.githubusercontent.com/DIKER0K/06cd12fb3a4d08b1f0f8c763a7d05e06/raw/versions.json',
},
);
if (availableResult.success) {
setAvailableVersions(availableResult.versions);
}
} catch (error) {
console.error('Ошибка при загрузке версий:', error);
// Можно добавить обработку ошибки, например показать уведомление
} finally {
setLoading(false);
}
};
fetchVersions();
}, []);
const handleSelectVersion = (version: VersionInfo) => {
localStorage.setItem(
'selected_version_config',
JSON.stringify(version.config || {}),
);
navigate(`/launch/${version.id}`);
};
const handleAddVersion = () => {
setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false);
};
const handleDownloadVersion = async (version: AvailableVersionInfo) => {
try {
setDownloadLoading(version.id);
// Скачивание и установка выбранной версии
const downloadResult = await window.electron.ipcRenderer.invoke(
'download-and-extract',
{
downloadUrl: version.config.downloadUrl,
apiReleaseUrl: version.config.apiReleaseUrl,
versionFileName: version.config.versionFileName,
packName: version.id,
preserveFiles: version.config.preserveFiles || [],
},
);
if (downloadResult?.success) {
// Добавляем скачанную версию в список установленных
setInstalledVersions((prev) => [...prev, version]);
setModalOpen(false);
}
} catch (error) {
console.error(`Ошибка при скачивании версии ${version.id}:`, error);
} finally {
setDownloadLoading(null);
}
};
// Карточка добавления новой версии
const AddVersionCard = () => (
<Card
sx={{
backgroundColor: 'rgba(30, 30, 50, 0.8)',
backdropFilter: 'blur(10px)',
width: '35vw',
height: '35vh',
minWidth: 'unset',
minHeight: 'unset',
display: 'flex',
flexDirection: 'column',
borderRadius: '16px',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)',
transition: 'transform 0.3s, box-shadow 0.3s',
overflow: 'hidden',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
}}
onClick={handleAddVersion}
>
<AddIcon sx={{ fontSize: 60, color: '#fff' }} />
<Typography
variant="h6"
sx={{
color: '#fff',
}}
>
Добавить
</Typography>
<Typography
variant="h6"
sx={{
color: '#fff',
}}
>
версию
</Typography>
</Card>
);
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
paddingLeft: '5vw',
paddingRight: '5vw',
}}
>
{loading ? (
<Box display="flex" justifyContent="center" my={5}>
<CircularProgress />
</Box>
) : (
<Grid
container
spacing={3}
sx={{
width: '100%',
overflowY: 'auto',
justifyContent: 'center',
}}
>
{/* Показываем установленные версии или дефолтную, если она есть */}
{installedVersions.length > 0 ? (
installedVersions.map((version) => (
<Grid
key={version.id}
size={{ xs: 'auto', sm: 'auto', md: 'auto' }}
>
<VersionCard
id={version.id}
name={version.name}
imageUrl={
version.imageUrl ||
'https://via.placeholder.com/300x140?text=Minecraft'
}
version={version.version}
onSelect={() => handleSelectVersion(version)}
/>
</Grid>
))
) : (
// Если нет ни одной версии, показываем карточку добавления
<Grid size={{ xs: 'auto', sm: 'auto', md: 'auto' }}>
<AddVersionCard />
</Grid>
)}
{/* Всегда добавляем карточку для добавления новых версий */}
{installedVersions.length > 0 && (
<Grid size={{ xs: 'auto', sm: 'auto', md: 'auto' }}>
<AddVersionCard />
</Grid>
)}
</Grid>
)}
{/* Модальное окно для выбора версии для скачивания */}
<Modal
open={modalOpen}
onClose={handleCloseModal}
aria-labelledby="modal-versions"
aria-describedby="modal-available-versions"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
maxHeight: '80vh',
overflowY: 'auto',
background: 'linear-gradient(45deg, #000000 10%, #3b4187 184.73%)',
border: '2px solid #000',
boxShadow: 24,
p: 4,
borderRadius: '3vw',
gap: '1vh',
display: 'flex',
flexDirection: 'column',
backdropFilter: 'blur(10px)',
}}
>
<Typography variant="h6" component="h2" sx={{ color: '#fff' }}>
Доступные версии для скачивания
</Typography>
{availableVersions.length === 0 ? (
<Typography sx={{ color: '#fff', mt: 2 }}>
Загрузка доступных версий...
</Typography>
) : (
<List sx={{ mt: 2 }}>
{availableVersions.map((version) => (
<ListItem
key={version.id}
sx={{
borderRadius: '8px',
mb: 1,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
cursor: 'pointer',
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.2)',
},
}}
onClick={() => handleSelectVersion(version)}
>
<ListItemText
primary={version.name}
secondary={version.version}
primaryTypographyProps={{ color: '#fff' }}
secondaryTypographyProps={{
color: 'rgba(255,255,255,0.7)',
}}
/>
</ListItem>
))}
</List>
)}
<Button
onClick={handleCloseModal}
variant="outlined"
sx={{
mt: 3,
alignSelf: 'center',
borderColor: '#fff',
color: '#fff',
'&:hover': {
borderColor: '#ccc',
backgroundColor: 'rgba(255,255,255,0.1)',
},
}}
>
Закрыть
</Button>
</Box>
</Modal>
</Box>
);
};