feat: improve Minecraft version handling
This commit is contained in:
@ -119,7 +119,7 @@ const createWindow = async () => {
|
||||
width: 1024,
|
||||
height: 850,
|
||||
autoHideMenuBar: true,
|
||||
resizable: false,
|
||||
resizable: true,
|
||||
frame: false,
|
||||
icon: getAssetPath('icon.png'),
|
||||
webPreferences: {
|
||||
|
@ -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: [] };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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: {
|
||||
|
@ -47,3 +47,7 @@ h4 {
|
||||
h5 {
|
||||
font-family: 'Benzin-Bold' !important;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-family: 'Benzin-Bold' !important;
|
||||
}
|
||||
|
@ -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 />
|
||||
|
92
src/renderer/components/Settings/SettingsModal.tsx
Normal file
92
src/renderer/components/Settings/SettingsModal.tsx
Normal 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;
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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: '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user