671 lines
24 KiB
TypeScript
671 lines
24 KiB
TypeScript
import { Box, Typography, Button, LinearProgress } from '@mui/material';
|
||
import { useEffect, useState } from 'react';
|
||
import { useNavigate, useParams } from 'react-router-dom';
|
||
import ServerStatus from '../components/ServerStatus/ServerStatus';
|
||
import PopaPopa from '../components/popa-popa';
|
||
import SettingsIcon from '@mui/icons-material/Settings';
|
||
import React from 'react';
|
||
import SettingsModal from '../components/Settings/SettingsModal';
|
||
import CustomNotification from '../components/Notifications/CustomNotification';
|
||
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
|
||
import {
|
||
isNotificationsEnabled,
|
||
getNotifPositionFromSettings,
|
||
} from '../utils/notifications';
|
||
|
||
declare global {
|
||
interface Window {
|
||
electron: {
|
||
ipcRenderer: {
|
||
invoke(channel: string, ...args: unknown[]): Promise<any>;
|
||
on(channel: string, func: (...args: unknown[]) => void): void;
|
||
removeAllListeners(channel: string): void;
|
||
};
|
||
};
|
||
}
|
||
}
|
||
|
||
// Определяем тип для props
|
||
interface LaunchPageProps {
|
||
onLaunchPage?: () => void;
|
||
launchOptions?: {
|
||
downloadUrl: string;
|
||
apiReleaseUrl: string;
|
||
versionFileName: string;
|
||
packName: string;
|
||
memory: number;
|
||
baseVersion: string;
|
||
serverIp: string;
|
||
fabricVersion: string;
|
||
neoForgeVersion?: string;
|
||
loaderType?: string;
|
||
};
|
||
}
|
||
|
||
const LaunchPage = ({
|
||
onLaunchPage,
|
||
launchOptions = {} as any,
|
||
}: LaunchPageProps) => {
|
||
const navigate = useNavigate();
|
||
const { versionId } = useParams();
|
||
const [versionConfig, setVersionConfig] = useState<any>(null);
|
||
const [fullVersionConfig, setFullVersionConfig] = useState<any>(null); // Полная конфигурация из Gist
|
||
|
||
// Начальное состояние должно быть пустым или с минимальными значениями
|
||
const [config, setConfig] = useState<{
|
||
memory: number;
|
||
preserveFiles: string[];
|
||
}>({
|
||
memory: 0,
|
||
preserveFiles: [],
|
||
});
|
||
const [isDownloading, setIsDownloading] = useState(false);
|
||
const [progress, setProgress] = useState(0);
|
||
const [buffer, setBuffer] = useState(10);
|
||
const [installStatus, setInstallStatus] = useState('');
|
||
const [notifOpen, setNotifOpen] = useState(false);
|
||
const [notifMsg, setNotifMsg] = useState<React.ReactNode>('');
|
||
const [notifSeverity, setNotifSeverity] = useState<
|
||
'success' | 'info' | 'warning' | 'error'
|
||
>('info');
|
||
|
||
const [notifPos, setNotifPos] = useState<NotificationPosition>({
|
||
vertical: 'bottom',
|
||
horizontal: 'center',
|
||
});
|
||
const [installStep, setInstallStep] = useState('');
|
||
const [installMessage, setInstallMessage] = useState('');
|
||
const [open, setOpen] = React.useState(false);
|
||
const [isGameRunning, setIsGameRunning] = useState(false);
|
||
const handleOpen = () => setOpen(true);
|
||
const handleClose = () => setOpen(false);
|
||
|
||
useEffect(() => {
|
||
const savedConfig = localStorage.getItem('launcher_config');
|
||
if (!savedConfig || !JSON.parse(savedConfig).accessToken) {
|
||
navigate('/login');
|
||
}
|
||
|
||
const overallProgressListener = (...args: unknown[]) => {
|
||
const value = args[0] as number; // 0..100
|
||
setProgress(value);
|
||
setBuffer(Math.min(value + 10, 100));
|
||
};
|
||
|
||
const statusListener = (...args: unknown[]) => {
|
||
const status = args[0] as { step: string; message: string };
|
||
setInstallStep(status.step);
|
||
setInstallMessage(status.message);
|
||
};
|
||
|
||
const minecraftErrorListener = (...args: unknown[]) => {
|
||
const payload = (args[0] || {}) as {
|
||
message?: string;
|
||
stderr?: string;
|
||
code?: number;
|
||
};
|
||
|
||
// Главное — показать пользователю, что запуск не удался
|
||
showNotification(
|
||
payload.message ||
|
||
'Minecraft завершился с ошибкой. Подробности смотрите в логах.',
|
||
'error',
|
||
);
|
||
};
|
||
|
||
const minecraftStartedListener = () => {
|
||
setIsGameRunning(true);
|
||
|
||
const raw = localStorage.getItem('pending_launch_context');
|
||
if (!raw) return;
|
||
|
||
const context = JSON.parse(raw);
|
||
|
||
localStorage.setItem(
|
||
'last_launched_version',
|
||
JSON.stringify({
|
||
...context,
|
||
launchedAt: Date.now(),
|
||
}),
|
||
);
|
||
|
||
localStorage.removeItem('pending_launch_context');
|
||
};
|
||
|
||
const minecraftStoppedListener = () => {
|
||
setIsGameRunning(false);
|
||
};
|
||
|
||
window.electron.ipcRenderer.on('overall-progress', overallProgressListener);
|
||
window.electron.ipcRenderer.on('minecraft-error', minecraftErrorListener);
|
||
window.electron.ipcRenderer.on('installation-status', statusListener);
|
||
window.electron.ipcRenderer.on(
|
||
'minecraft-started',
|
||
minecraftStartedListener,
|
||
);
|
||
window.electron.ipcRenderer.on(
|
||
'minecraft-stopped',
|
||
minecraftStoppedListener,
|
||
);
|
||
|
||
return () => {
|
||
// Удаляем только конкретных слушателей, а не всех
|
||
// Это безопаснее, чем removeAllListeners
|
||
const cleanup = window.electron.ipcRenderer.on;
|
||
if (typeof cleanup === 'function') {
|
||
cleanup('installation-status', statusListener);
|
||
cleanup('minecraft-error', statusListener);
|
||
cleanup('overall-progress', overallProgressListener);
|
||
}
|
||
// Удаляем использование removeAllListeners
|
||
};
|
||
}, [navigate]);
|
||
|
||
// Функция для загрузки полной конфигурации версии из Gist
|
||
const fetchFullVersionConfig = async (): Promise<any> => {
|
||
if (!versionId) return null;
|
||
|
||
try {
|
||
// Загружаем весь список версий из Gist
|
||
const result = await window.electron.ipcRenderer.invoke(
|
||
'get-available-versions',
|
||
{},
|
||
);
|
||
|
||
if (result.success && result.versions) {
|
||
// Находим нужную версию по ID
|
||
const version = result.versions.find((v: any) => v.id === versionId);
|
||
return version || null;
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка при загрузке полной конфигурации:', error);
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
useEffect(() => {
|
||
const fetchVersionConfig = async () => {
|
||
if (!versionId) return;
|
||
|
||
try {
|
||
// Сначала проверяем, есть ли конфигурация в localStorage
|
||
const savedConfig = localStorage.getItem('selected_version_config');
|
||
if (savedConfig) {
|
||
const parsedConfig = JSON.parse(savedConfig);
|
||
|
||
// Если конфиг пустой — считаем, что он невалидный и идём по IPC-ветке
|
||
if (Object.keys(parsedConfig).length > 0) {
|
||
setVersionConfig(parsedConfig);
|
||
setFullVersionConfig(parsedConfig);
|
||
|
||
setConfig({
|
||
memory: parsedConfig.memory || 4096,
|
||
preserveFiles: parsedConfig.preserveFiles || [],
|
||
});
|
||
|
||
localStorage.removeItem('selected_version_config');
|
||
return;
|
||
} else {
|
||
localStorage.removeItem('selected_version_config');
|
||
}
|
||
}
|
||
|
||
// Загружаем полную конфигурацию из Gist
|
||
const fullConfig = await fetchFullVersionConfig();
|
||
if (fullConfig) {
|
||
setFullVersionConfig(fullConfig);
|
||
|
||
// Сохраняем только config часть для совместимости
|
||
setVersionConfig(fullConfig.config || {});
|
||
|
||
setConfig({
|
||
memory: fullConfig.config?.memory || 4096,
|
||
preserveFiles: fullConfig.config?.preserveFiles || [],
|
||
});
|
||
} else {
|
||
// Если не удалось получить конфигурацию из Gist, используем IPC
|
||
const result = await window.electron.ipcRenderer.invoke(
|
||
'get-version-config',
|
||
{ versionId },
|
||
);
|
||
|
||
if (result.success) {
|
||
setVersionConfig(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) {
|
||
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 || [],
|
||
});
|
||
}
|
||
};
|
||
|
||
fetchVersionConfig();
|
||
}, [versionId]);
|
||
|
||
const showNotification = (
|
||
message: React.ReactNode,
|
||
severity: 'success' | 'info' | 'warning' | 'error' = 'info',
|
||
position: NotificationPosition = getNotifPositionFromSettings(),
|
||
) => {
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg(message);
|
||
setNotifSeverity(severity);
|
||
setNotifPos(position);
|
||
setNotifOpen(true);
|
||
};
|
||
|
||
// Функция для запуска игры с настройками выбранной версии
|
||
const handleLaunchMinecraft = async () => {
|
||
try {
|
||
setIsDownloading(true);
|
||
setBuffer(10);
|
||
|
||
if (isGameRunning) {
|
||
showNotification('Minecraft уже запущен', 'info');
|
||
return;
|
||
}
|
||
|
||
// Загружаем полную конфигурацию, если еще не загружена
|
||
if (!fullVersionConfig) {
|
||
const loadedConfig = await fetchFullVersionConfig();
|
||
if (loadedConfig) {
|
||
setFullVersionConfig(loadedConfig);
|
||
setVersionConfig(loadedConfig.config || {});
|
||
}
|
||
}
|
||
|
||
console.log('fullVersionConfig:', fullVersionConfig);
|
||
console.log('versionFromGist:', fullVersionConfig?.version);
|
||
|
||
// Используем настройки из Gist или дефолтные
|
||
const currentConfig = fullVersionConfig?.config ||
|
||
versionConfig || {
|
||
packName: versionId || 'Comfort',
|
||
memory: 4096,
|
||
baseVersion: '1.21.4',
|
||
serverIp: 'popa-popa.ru',
|
||
fabricVersion: '0.16.14',
|
||
neoForgeVersion: null,
|
||
loaderType: 'fabric',
|
||
preserveFiles: [],
|
||
};
|
||
|
||
// Получаем версию для запуска из Gist
|
||
let versionFromGist = fullVersionConfig?.version || null;
|
||
console.log('versionFromGist before override:', versionFromGist);
|
||
|
||
// Если версия из Gist пустая, используем логику по умолчанию
|
||
if (
|
||
!versionFromGist &&
|
||
currentConfig.loaderType === 'neoforge' &&
|
||
currentConfig.neoForgeVersion
|
||
) {
|
||
versionFromGist = `neoforge-${currentConfig.neoForgeVersion}`;
|
||
console.log('Overriding versionFromGist to:', versionFromGist);
|
||
}
|
||
|
||
// Проверяем, является ли это ванильной версией
|
||
const isVanillaVersion =
|
||
!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(
|
||
localStorage.getItem('launcher_config') || '{}',
|
||
);
|
||
|
||
// Формируем полные опции для запуска
|
||
const options: any = {
|
||
accessToken: savedConfig.accessToken,
|
||
uuid: savedConfig.uuid,
|
||
username: savedConfig.username,
|
||
memory: config.memory,
|
||
baseVersion: currentConfig.baseVersion,
|
||
packName: versionId || currentConfig.packName,
|
||
serverIp: currentConfig.serverIp,
|
||
fabricVersion: currentConfig.fabricVersion,
|
||
neoForgeVersion: currentConfig.neoForgeVersion,
|
||
loaderType: currentConfig.loaderType || 'fabric',
|
||
isVanillaVersion: isVanillaVersion,
|
||
versionToLaunchOverride:
|
||
versionFromGist || (isVanillaVersion ? versionId : undefined),
|
||
// Передаем Gist URL для загрузки конфигурации в процессе запуска
|
||
gistUrl:
|
||
'https://gist.githubusercontent.com/DIKER0K/06cd12fb3a4d08b1f0f8c763a7d05e06/raw/versions.json',
|
||
};
|
||
|
||
const launchContext = {
|
||
versionId,
|
||
packName: versionId || currentConfig.packName,
|
||
baseVersion: currentConfig.baseVersion,
|
||
fabricVersion: currentConfig.fabricVersion,
|
||
neoForgeVersion: currentConfig.neoForgeVersion,
|
||
loaderType: currentConfig.loaderType,
|
||
serverIp: currentConfig.serverIp,
|
||
isVanillaVersion,
|
||
versionToLaunchOverride:
|
||
versionFromGist || (isVanillaVersion ? versionId : undefined),
|
||
memory: config.memory,
|
||
};
|
||
|
||
localStorage.setItem(
|
||
'pending_launch_context',
|
||
JSON.stringify(launchContext),
|
||
);
|
||
|
||
const launchResult = await window.electron.ipcRenderer.invoke(
|
||
'launch-minecraft',
|
||
options,
|
||
);
|
||
|
||
if (launchResult?.success) {
|
||
showNotification('Minecraft успешно запущен!', 'success');
|
||
} else if (launchResult?.error) {
|
||
showNotification(`Ошибка запуска: ${launchResult.error}`, 'error');
|
||
}
|
||
} catch (error: any) {
|
||
console.error('Error:', error);
|
||
showNotification(`Ошибка: ${error.message}`, 'error');
|
||
} finally {
|
||
setIsDownloading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
window.addEventListener('beforeunload', () => {
|
||
localStorage.removeItem('pending_launch_context');
|
||
});
|
||
}, []);
|
||
|
||
const handleStopMinecraft = async () => {
|
||
try {
|
||
const result = await window.electron.ipcRenderer.invoke('stop-minecraft');
|
||
|
||
if (result?.success) {
|
||
showNotification('Minecraft остановлен', 'info');
|
||
setIsGameRunning(false);
|
||
} else if (result?.error) {
|
||
showNotification(
|
||
`Не удалось остановить Minecraft: ${result.error}`,
|
||
'error',
|
||
);
|
||
}
|
||
} catch (error: any) {
|
||
console.error('Ошибка при остановке Minecraft:', error);
|
||
showNotification(
|
||
`Ошибка при остановке Minecraft: ${error.message || String(error)}`,
|
||
'error',
|
||
);
|
||
}
|
||
};
|
||
|
||
// Функция для сохранения настроек
|
||
const savePackConfig = async () => {
|
||
try {
|
||
const configToSave = {
|
||
memory: config.memory,
|
||
preserveFiles: config.preserveFiles || [],
|
||
};
|
||
|
||
await window.electron.ipcRenderer.invoke('save-pack-config', {
|
||
packName: versionId || versionConfig?.packName || 'Comfort',
|
||
config: configToSave,
|
||
});
|
||
|
||
showNotification('Настройки сохранены', 'success');
|
||
} catch (error) {
|
||
console.error('Ошибка при сохранении настроек:', error);
|
||
showNotification('Ошибка сохранения настроек', 'error');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Box sx={{ gap: '1vh', display: 'flex', flexDirection: 'column' }}>
|
||
<PopaPopa />
|
||
|
||
<Typography variant="h4">Игровой сервер</Typography>
|
||
|
||
<Typography variant="h4">долбаёбов в Minecraft</Typography>
|
||
|
||
<Box>
|
||
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||
СЕРВЕР ГДЕ ВСЕМ НА ВАС ПОХУЙ
|
||
</Typography>
|
||
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||
СЕРВЕР ГДЕ РАЗРЕШЕНЫ ОДНОПОЛЫЕ БРАКИ
|
||
</Typography>
|
||
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||
СЕРВЕР ГДЕ ВСЕ ДОЛБАЕБЫ
|
||
</Typography>
|
||
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||
СЕРВЕР ГДЕ НА СПАВНЕ БУДЕТ ХУЙ (ВОЗМОЖНО)
|
||
</Typography>
|
||
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||
СЕРВЕР ЗА КОТОРЫЙ ВЫ ПРОДАДИТЕ МАТЬ
|
||
</Typography>
|
||
<Typography variant="body1" sx={{ color: '#FFFFFF61' }}>
|
||
ТЫ МОЖЕШЬ КУПИТЬ АДМИНКУ И ПОЛУЧИТЬ ПИЗДЫ
|
||
</Typography>
|
||
</Box>
|
||
|
||
<Box>
|
||
<ServerStatus
|
||
serverIp={versionConfig?.serverIp || 'popa-popa.ru'}
|
||
refreshInterval={30000}
|
||
/>
|
||
</Box>
|
||
|
||
{isDownloading ? (
|
||
<Box sx={{ mb: 3, width: '100%' }}>
|
||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||
<Box sx={{ width: '100%', mr: 1 }}>
|
||
<LinearProgress
|
||
variant="buffer"
|
||
value={progress}
|
||
valueBuffer={buffer}
|
||
sx={{
|
||
height: '0.45vw',
|
||
borderRadius: '1vw',
|
||
|
||
// Фон прогресс-бара (buffer background)
|
||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||
|
||
'& .MuiLinearProgress-bar1Buffer': {
|
||
// Основная прогресс-линия
|
||
background:
|
||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
|
||
borderRadius: 6,
|
||
},
|
||
|
||
'& .MuiLinearProgress-bar2Buffer': {
|
||
// Buffer линия (вторая линия)
|
||
backgroundColor: 'rgba(255,255,255,0)',
|
||
borderRadius: 6,
|
||
},
|
||
|
||
'& .MuiLinearProgress-dashed': {
|
||
// Линии пунктирного эффекта
|
||
display: 'none',
|
||
},
|
||
}}
|
||
/>
|
||
</Box>
|
||
<Box sx={{ minWidth: 35 }}>
|
||
<Typography
|
||
variant="body2"
|
||
sx={{ color: 'white' }}
|
||
>{`${Math.round(progress)}%`}</Typography>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
) : (
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
gap: '1vw',
|
||
width: '100%', // родитель занимает всю ширину
|
||
}}
|
||
>
|
||
{/* Первая кнопка — растягивается на всё доступное пространство */}
|
||
<Button
|
||
variant="contained"
|
||
color="primary"
|
||
onClick={
|
||
isGameRunning ? handleStopMinecraft : handleLaunchMinecraft
|
||
}
|
||
sx={{
|
||
flexGrow: 1,
|
||
width: 'auto',
|
||
borderRadius: '3vw',
|
||
fontFamily: 'Benzin-Bold',
|
||
transition: 'transform 0.3s ease',
|
||
|
||
...(isGameRunning
|
||
? {
|
||
// 🔹 Стиль, когда игра запущена (серая кнопка)
|
||
background: 'linear-gradient(71deg, #555 0%, #777 100%)',
|
||
'&:hover': {
|
||
background: 'linear-gradient(71deg, #666 0%, #888 100%)',
|
||
transform: 'scale(1.01)',
|
||
boxShadow: '0 4px 15px rgba(100, 100, 100, 0.4)',
|
||
},
|
||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
||
}
|
||
: {
|
||
// 🔹 Стиль, когда Minecraft НЕ запущен (твоя стандартная красочная кнопка)
|
||
background:
|
||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
|
||
'&:hover': {
|
||
background:
|
||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
|
||
transform: 'scale(1.01)',
|
||
boxShadow: '0 4px 15px rgba(242, 113, 33, 0.4)',
|
||
},
|
||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
||
transition: 'all 0.25s ease',
|
||
}),
|
||
}}
|
||
>
|
||
{isGameRunning ? 'Остановить Minecraft' : 'Запустить Minecraft'}
|
||
</Button>
|
||
|
||
{/* Вторая кнопка — квадратная, фиксированного размера (ширина = высоте) */}
|
||
<Button
|
||
variant="contained"
|
||
sx={{
|
||
flexShrink: 0, // не сжимается
|
||
aspectRatio: '1', // ширина = высоте
|
||
backgroundColor: 'grey',
|
||
borderRadius: '3vw',
|
||
minHeight: 'unset',
|
||
minWidth: 'unset',
|
||
height: '100%', // занимает полную высоту родителя
|
||
'&:hover': {
|
||
background:
|
||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
|
||
transform: 'scale(1.05)',
|
||
boxShadow: '0 4px 15px rgba(242, 113, 33, 0.4)',
|
||
},
|
||
transition: 'transform 0.25s ease, box-shadow 0.25s ease',
|
||
}}
|
||
onClick={handleOpen}
|
||
>
|
||
<SettingsIcon />
|
||
</Button>
|
||
</Box>
|
||
)}
|
||
|
||
<CustomNotification
|
||
open={notifOpen}
|
||
message={notifMsg}
|
||
severity={notifSeverity}
|
||
position={notifPos}
|
||
onClose={() => setNotifOpen(false)}
|
||
autoHideDuration={2500}
|
||
/>
|
||
|
||
<SettingsModal
|
||
open={open}
|
||
onClose={handleClose}
|
||
config={config}
|
||
onConfigChange={setConfig}
|
||
packName={versionId || versionConfig?.packName || 'Comfort'}
|
||
onSave={savePackConfig}
|
||
/>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
export default LaunchPage;
|