add: VersionExplorer, don't work first run Minecraft

This commit is contained in:
2025-07-08 03:29:36 +05:00
parent 31a26dc1ce
commit 815ce286f7
5 changed files with 559 additions and 106 deletions

View File

@ -804,6 +804,53 @@ export function initMinecraftHandlers() {
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
// Добавьте в функцию initMinecraftHandlers новый обработчик
ipcMain.handle('get-available-versions', async (event) => {
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 };
}
});
} }
// Добавляем обработчики IPC для аутентификации // Добавляем обработчики IPC для аутентификации
@ -979,3 +1026,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

@ -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);
@ -102,7 +91,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

@ -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

@ -8,7 +8,7 @@ import {
Modal, 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';
@ -30,7 +30,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 +44,11 @@ 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 +106,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,35 +194,34 @@ 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 === '';
}
const savedConfig = JSON.parse( if (!isVanillaVersion) {
localStorage.getItem('launcher_config') || '{}', // Если это не ванильная версия, выполняем загрузку и распаковку
);
// Опции для скачивания сборки
const packOptions = { const packOptions = {
downloadUrl: launchOptions.downloadUrl, downloadUrl: currentConfig.downloadUrl,
apiReleaseUrl: launchOptions.apiReleaseUrl, apiReleaseUrl: currentConfig.apiReleaseUrl,
versionFileName: launchOptions.versionFileName, versionFileName: currentConfig.versionFileName,
packName: launchOptions.packName, packName: versionId || currentConfig.packName,
preserveFiles: config.preserveFiles, preserveFiles: config.preserveFiles,
}; };
@ -173,31 +232,39 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
); );
if (downloadResult?.success) { if (downloadResult?.success) {
let needsSecondAttempt = false;
if (downloadResult.updated) { if (downloadResult.updated) {
showNotification( showNotification(
`Сборка ${downloadResult.packName} успешно обновлена до версии ${downloadResult.version}`, `Сборка ${downloadResult.packName} успешно обновлена до версии ${downloadResult.version}`,
'success', 'success',
); );
needsSecondAttempt = true;
} else { } else {
showNotification( showNotification(
`Установлена актуальная версия сборки ${downloadResult.packName} (${downloadResult.version})`, `Установлена актуальная версия сборки ${downloadResult.packName} (${downloadResult.version})`,
'info', 'info',
); );
} }
}
} else {
showNotification('Запускаем ванильный Minecraft...', 'info');
}
// Опции для запуска Minecraft
const savedConfig = JSON.parse(
localStorage.getItem('launcher_config') || '{}',
);
// Опции для запуска
const options = { const options = {
accessToken: savedConfig.accessToken, accessToken: savedConfig.accessToken,
uuid: savedConfig.uuid, uuid: savedConfig.uuid,
username: savedConfig.username, username: savedConfig.username,
memory: config.memory, // Используем state memory: config.memory,
baseVersion: launchOptions.baseVersion, baseVersion: currentConfig.baseVersion,
packName: launchOptions.packName, packName: versionId || currentConfig.packName,
serverIp: launchOptions.serverIp, serverIp: currentConfig.serverIp,
fabricVersion: launchOptions.fabricVersion, fabricVersion: currentConfig.fabricVersion,
// Для ванильной версии устанавливаем флаг
isVanillaVersion: isVanillaVersion,
versionToLaunchOverride: isVanillaVersion ? versionId : undefined,
}; };
const launchResult = await window.electron.ipcRenderer.invoke( const launchResult = await window.electron.ipcRenderer.invoke(
@ -205,23 +272,8 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
options, options,
); );
if (needsSecondAttempt) { if (launchResult?.success) {
showNotification(
'Завершаем настройку компонентов, повторный запуск...',
'info',
);
await new Promise((resolve) => setTimeout(resolve, 2000));
const secondAttempt = await window.electron.ipcRenderer.invoke(
'launch-minecraft',
options,
);
showNotification('Minecraft успешно запущен!', 'success'); showNotification('Minecraft успешно запущен!', 'success');
} else if (launchResult?.success) {
showNotification('Minecraft успешно запущен!', 'success');
}
} }
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
@ -240,7 +292,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 +337,7 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
<Box> <Box>
<ServerStatus <ServerStatus
serverIp={launchOptions.serverIp} serverIp={versionConfig?.serverIp || 'popa-popa.ru'}
refreshInterval={30000} refreshInterval={30000}
/> />
</Box> </Box>
@ -393,8 +445,8 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
Файлы и папки, которые будут сохранены после переустановки сборки Файлы и папки, которые будут сохранены после переустановки сборки
</Typography> </Typography>
<FilesSelector <FilesSelector
packName={launchOptions.packName} packName={versionId || versionConfig?.packName || 'Comfort'}
initialSelected={config.preserveFiles} // Передаем текущие выбранные файлы initialSelected={config.preserveFiles}
onSelectionChange={(selected) => { onSelectionChange={(selected) => {
setConfig((prev) => ({ ...prev, preserveFiles: selected })); setConfig((prev) => ({ ...prev, preserveFiles: selected }));
}} }}

View File

@ -0,0 +1,268 @@
import { useEffect, useState } from 'react';
import {
Box,
Typography,
Grid,
Card,
CardMedia,
CardContent,
CardActions,
Button,
CircularProgress,
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
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: '100%',
height: '100%',
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)',
},
}}
>
<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"
component="div"
sx={{
fontWeight: 'bold',
color: '#ffffff',
fontSize: '1.5rem',
}}
>
{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>
);
};
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[];
};
}
// В компоненте VersionsExplorer
export const VersionsExplorer = () => {
const [versions, setVersions] = useState<VersionInfo[]>([]);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
const fetchVersions = async () => {
try {
const result = await window.electron.ipcRenderer.invoke(
'get-available-versions',
);
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);
}
} catch (error) {
console.error('Ошибка при запросе версий:', error);
} finally {
setLoading(false);
}
};
fetchVersions();
}, []);
const handleSelectVersion = (version: VersionInfo) => {
// Сохраняем конфигурацию в localStorage для использования в LaunchPage
localStorage.setItem(
'selected_version_config',
JSON.stringify(version.config || {}),
);
navigate(`/launch/${version.id}`);
};
// Тестовая версия, если нет доступных
const displayVersions =
versions.length > 0
? versions
: [
{
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'],
},
},
];
return (
<Box
sx={{
width: '100vw',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography variant="h4" sx={{ mb: 4 }}>
Доступные версии
</Typography>
{loading ? (
<Box display="flex" justifyContent="center" my={5}>
<CircularProgress />
</Box>
) : (
<Grid
container
spacing={3}
sx={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
width: '100%',
}}
>
{displayVersions.map((version) => (
<Grid key={version.id} size={{ xs: 12, sm: 6, md: 4 }}>
<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>
)}
</Box>
);
};