feat: improve Minecraft version handling

This commit is contained in:
2025-07-13 23:37:46 +05:00
parent 815ce286f7
commit 942066ea76
9 changed files with 491 additions and 217 deletions

View File

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

View File

@ -805,8 +805,11 @@ export function initMinecraftHandlers() {
}
});
// Добавьте в функцию initMinecraftHandlers новый обработчик
ipcMain.handle('get-available-versions', async (event) => {
// ПРОБЛЕМА: У вас два обработчика для одного и того же канала 'get-installed-versions'
// РЕШЕНИЕ: Объединим логику в один обработчик, а из второго обработчика вызовем функцию getInstalledVersions
// Сначала создаем общую функцию для получения установленных версий
function getInstalledVersions() {
try {
const appPath = path.dirname(app.getPath('exe'));
const minecraftDir = path.join(appPath, '.minecraft');
@ -846,9 +849,59 @@ export function initMinecraftHandlers() {
}
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 };
return { success: false, error: error.message, versions: [] };
}
});
}

View File

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

View File

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

View File

@ -43,15 +43,37 @@ const AuthCheck = ({ children }: { children: ReactNode }) => {
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;
// Используем IPC для валидации токена через main процесс
const result = await window.electron.ipcRenderer.invoke(
'validate-token',
token,
);
// Если токен недействителен, очищаем сохраненные данные в localStorage
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) {
console.error('Ошибка проверки токена:', error);
return false;
}
};
@ -80,6 +102,7 @@ const App = () => {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
overflowX: 'hidden',
}}
>
<MinecraftBackround />

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

@ -5,7 +5,6 @@ import {
Snackbar,
Alert,
LinearProgress,
Modal,
} from '@mui/material';
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
@ -13,8 +12,7 @@ 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';
import SettingsModal from '../components/Settings/SettingsModal';
declare global {
interface Window {
@ -44,7 +42,10 @@ interface LaunchPageProps {
};
}
const LaunchPage = ({ onLaunchPage, launchOptions = {} as any }: LaunchPageProps) => {
const LaunchPage = ({
onLaunchPage,
launchOptions = {} as any,
}: LaunchPageProps) => {
const navigate = useNavigate();
const { versionId } = useParams();
const [versionConfig, setVersionConfig] = useState<any>(null);
@ -417,65 +418,14 @@ const LaunchPage = ({ onLaunchPage, launchOptions = {} as any }: LaunchPageProps
</Alert>
</Snackbar>
<Modal
<SettingsModal
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
config={config}
onConfigChange={setConfig}
packName={versionId || versionConfig?.packName || 'Comfort'}
initialSelected={config.preserveFiles}
onSelectionChange={(selected) => {
setConfig((prev) => ({ ...prev, preserveFiles: selected }));
}}
onSave={savePackConfig}
/>
<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>
);
};

View File

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

View File

@ -9,8 +9,15 @@ import {
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;
@ -32,51 +39,30 @@ const VersionCard: React.FC<VersionCardProps> = ({
sx={{
backgroundColor: 'rgba(30, 30, 50, 0.8)',
backdropFilter: 'blur(10px)',
width: '100%',
height: '100%',
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',
'&:hover': {
transform: 'translateY(-8px)',
boxShadow: '0 12px 40px rgba(0, 0, 0, 0.5)',
},
cursor: 'pointer',
}}
onClick={() => onSelect(id)}
>
<CardContent
sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '10%',
}}
>
<Box sx={{ position: 'relative', overflow: 'hidden', width: '100%' }}>
<CardMedia
component="img"
height="180"
image={'https://placehold.co/300x140?text=' + name}
alt={name}
sx={{
transition: 'transform 0.5s',
'&:hover': {
transform: 'scale(1.05)',
},
}}
/>
<Box
sx={{
position: 'absolute',
top: 0,
right: 0,
backgroundColor: 'rgba(0, 0, 0, 0.6)',
color: '#fff',
padding: '4px 12px',
borderBottomLeftRadius: '12px',
}}
>
<Typography variant="caption" fontWeight="bold">
{version}
</Typography>
</Box>
</Box>
<CardContent sx={{ flexGrow: 1, padding: '16px' }}>
<Typography
gutterBottom
variant="h5"
@ -89,37 +75,7 @@ const VersionCard: React.FC<VersionCardProps> = ({
>
{name}
</Typography>
<Typography
variant="body2"
sx={{
color: 'rgba(255, 255, 255, 0.7)',
marginBottom: '12px',
}}
>
Версия: {version}
</Typography>
</CardContent>
<CardActions sx={{ padding: '0 16px 16px' }}>
<Button
fullWidth
variant="contained"
color="primary"
onClick={() => onSelect(id)}
sx={{
borderRadius: '12px',
textTransform: 'none',
fontWeight: 'bold',
padding: '10px 0',
background: 'linear-gradient(90deg, #3a7bd5, #6d5bf1)',
'&:hover': {
background: 'linear-gradient(90deg, #4a8be5, #7d6bf1)',
},
}}
>
Играть
</Button>
</CardActions>
</Card>
);
};
@ -142,40 +98,62 @@ interface VersionInfo {
};
}
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 [versions, setVersions] = useState<VersionInfo[]>([]);
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 {
const result = await window.electron.ipcRenderer.invoke(
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 (result.success) {
// Для каждой версии получаем её конфигурацию
const versionsWithConfig = await Promise.all(
result.versions.map(async (version: VersionInfo) => {
const configResult = await window.electron.ipcRenderer.invoke(
'get-version-config',
{ versionId: version.id },
);
return {
...version,
config: configResult.success ? configResult.config : undefined,
};
}),
);
setVersions(versionsWithConfig);
} else {
console.error('Ошибка получения версий:', result.error);
if (availableResult.success) {
setAvailableVersions(availableResult.versions);
}
} catch (error) {
console.error('Ошибка при запросе версий:', error);
console.error('Ошибка при загрузке версий:', error);
// Можно добавить обработку ошибки, например показать уведомление
} finally {
setLoading(false);
}
@ -185,7 +163,6 @@ export const VersionsExplorer = () => {
}, []);
const handleSelectVersion = (version: VersionInfo) => {
// Сохраняем конфигурацию в localStorage для использования в LaunchPage
localStorage.setItem(
'selected_version_config',
JSON.stringify(version.config || {}),
@ -193,45 +170,94 @@ export const VersionsExplorer = () => {
navigate(`/launch/${version.id}`);
};
// Тестовая версия, если нет доступных
const displayVersions =
versions.length > 0
? versions
: [
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',
{
id: 'Comfort',
name: 'Comfort',
version: '1.21.4-fabric0.16.14',
imageUrl: 'https://via.placeholder.com/300x140?text=Comfort',
config: {
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',
preserveFiles: ['popa-launcher-config.json'],
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={{
width: '100vw',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
paddingLeft: '5vw',
paddingRight: '5vw',
}}
>
<Typography variant="h4" sx={{ mb: 4 }}>
Доступные версии
</Typography>
{loading ? (
<Box display="flex" justifyContent="center" my={5}>
<CircularProgress />
@ -241,14 +267,18 @@ export const VersionsExplorer = () => {
container
spacing={3}
sx={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
width: '100%',
overflowY: 'auto',
justifyContent: 'center',
}}
>
{displayVersions.map((version) => (
<Grid key={version.id} size={{ xs: 12, sm: 6, md: 4 }}>
{/* Показываем установленные версии или дефолтную, если она есть */}
{installedVersions.length > 0 ? (
installedVersions.map((version) => (
<Grid
key={version.id}
size={{ xs: 'auto', sm: 'auto', md: 'auto' }}
>
<VersionCard
id={version.id}
name={version.name}
@ -260,9 +290,105 @@ export const VersionsExplorer = () => {
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>
);
};