feat: improved launch settings

This commit is contained in:
2025-07-07 06:56:30 +05:00
parent b14de1d15a
commit 1b50a7d4e4
8 changed files with 510 additions and 20 deletions

View File

@ -18,6 +18,7 @@ import {
initMinecraftHandlers,
initAuthHandlers,
initServerStatusHandler,
initPackConfigHandlers,
} from './minecraft-launcher';
class AppUpdater {
@ -133,6 +134,7 @@ const createWindow = async () => {
initAuthHandlers();
initMinecraftHandlers();
initServerStatusHandler();
initPackConfigHandlers();
};
/**

View File

@ -477,7 +477,7 @@ export function initMinecraftHandlers() {
accessToken,
uuid,
username,
memory = 2048,
memory = 4096,
baseVersion = '1.21.4',
fabricVersion = 'fabric0.16.14',
packName = 'Comfort', // Название основной сборки
@ -761,6 +761,47 @@ export function initMinecraftHandlers() {
return { success: false, error: error.message };
}
});
// Добавьте в функцию initMinecraftHandlers или создайте новую
ipcMain.handle('get-pack-files', async (event, packName) => {
try {
const appPath = path.dirname(app.getPath('exe'));
const minecraftDir = path.join(appPath, '.minecraft');
const packDir = path.join(minecraftDir, 'versions', packName);
if (!fs.existsSync(packDir)) {
return { success: false, error: 'Директория сборки не найдена' };
}
// Функция для рекурсивного обхода директории
const scanDir: any = (dir: any, basePath: any = '') => {
const result = [];
const items = fs.readdirSync(dir);
for (const item of items) {
const itemPath = path.join(dir, item);
const relativePath = basePath ? path.join(basePath, item) : item;
const isDirectory = fs.statSync(itemPath).isDirectory();
result.push({
name: item,
path: relativePath,
isDirectory,
// Если это директория, рекурсивно сканируем ее
children: isDirectory ? scanDir(itemPath, relativePath) : [],
});
}
return result;
};
const files = scanDir(packDir);
return { success: true, files };
} catch (error) {
console.error('Ошибка при получении файлов сборки:', error);
return { success: false, error: error.message };
}
});
}
// Добавляем обработчики IPC для аутентификации
@ -855,3 +896,84 @@ export function initServerStatusHandler() {
}
});
}
// Функция для работы с конфигурацией сборки
export function initPackConfigHandlers() {
// Файл конфигурации
const CONFIG_FILENAME = 'popa-launcher-config.json';
// Обработчик для сохранения настроек сборки
ipcMain.handle('save-pack-config', async (event, { packName, config }) => {
try {
const appPath = path.dirname(app.getPath('exe'));
const minecraftDir = path.join(appPath, '.minecraft');
const packDir = path.join(minecraftDir, 'versions', packName);
// Создаем папку для сборки, если она не существует
if (!fs.existsSync(packDir)) {
fs.mkdirSync(packDir, { recursive: true });
}
const configPath = path.join(packDir, CONFIG_FILENAME);
// Сохраняем конфигурацию в файл
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
// Добавляем файл конфигурации в список файлов, которые не удаляются
if (!config.preserveFiles.includes(CONFIG_FILENAME)) {
config.preserveFiles.push(CONFIG_FILENAME);
// Перезаписываем файл с обновленным списком
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
}
return { success: true };
} catch (error) {
console.error('Ошибка при сохранении настроек сборки:', error);
return { success: false, error: error.message };
}
});
// Обработчик для загрузки настроек сборки
ipcMain.handle('load-pack-config', async (event, { packName }) => {
try {
const appPath = path.dirname(app.getPath('exe'));
const minecraftDir = path.join(appPath, '.minecraft');
const packDir = path.join(minecraftDir, 'versions', packName);
const configPath = path.join(packDir, CONFIG_FILENAME);
// Проверяем существование файла конфигурации
if (!fs.existsSync(configPath)) {
// Если файла нет, возвращаем дефолтную конфигурацию
return {
success: true,
config: {
memory: 4096,
preserveFiles: [CONFIG_FILENAME], // По умолчанию сохраняем файл конфигурации
},
};
}
// Читаем и парсим конфигурацию
const configData = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(configData);
// Добавляем файл конфигурации в список сохраняемых файлов, если его там нет
if (!config.preserveFiles.includes(CONFIG_FILENAME)) {
config.preserveFiles.push(CONFIG_FILENAME);
}
return { success: true, config };
} catch (error) {
console.error('Ошибка при загрузке настроек сборки:', error);
return {
success: false,
error: error.message,
// Возвращаем дефолтную конфигурацию при ошибке
config: {
memory: 4096,
preserveFiles: [CONFIG_FILENAME],
},
};
}
});
}

View File

@ -7,7 +7,9 @@ export type Channels =
| 'installation-status'
| 'get-server-status'
| 'close-app'
| 'minimize-app';
| 'minimize-app'
| 'save-pack-config'
| 'load-pack-config';
const electronHandler = {
ipcRenderer: {

View File

@ -23,13 +23,6 @@ const launchOptions = {
baseVersion: '1.21.4',
serverIp: 'popa-popa.ru',
fabricVersion: 'fabric0.16.14',
preserveFiles: [
'options.txt',
'screenshots',
'schematics',
'syncmatics',
'saves',
],
};
const AuthCheck = ({ children }: { children: ReactNode }) => {

View File

@ -0,0 +1,205 @@
import { useState, useEffect } from 'react';
import {
Box,
Checkbox,
Typography,
List,
ListItem,
ListItemIcon,
ListItemText,
Collapse,
CircularProgress,
} from '@mui/material';
import FolderIcon from '@mui/icons-material/Folder';
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
interface FileNode {
name: string;
path: string;
isDirectory: boolean;
children: FileNode[];
}
interface FilesSelectorProps {
packName: string;
onSelectionChange: (selectedFiles: string[]) => void;
initialSelected?: string[]; // Добавляем этот параметр
}
export default function FilesSelector({
packName,
onSelectionChange,
initialSelected = [], // Значение по умолчанию
}: FilesSelectorProps) {
const [files, setFiles] = useState<FileNode[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Используем initialSelected для начального состояния
const [selectedFiles, setSelectedFiles] = useState<string[]>(initialSelected);
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
new Set(),
);
useEffect(() => {
const fetchFiles = async () => {
try {
setLoading(true);
const result = await window.electron.ipcRenderer.invoke(
'get-pack-files',
packName,
);
if (result.success) {
setFiles(result.files);
} else {
setError(result.error);
}
} catch (err) {
setError('Ошибка при загрузке файлов');
} finally {
setLoading(false);
}
};
fetchFiles();
}, [packName]);
// Обработка выбора файла/папки
const handleToggle = (
path: string,
isDirectory: boolean,
children: FileNode[],
) => {
let newSelected = [...selectedFiles];
if (isDirectory) {
if (selectedFiles.includes(path)) {
// Если папка выбрана, убираем ее и все вложенные файлы
newSelected = newSelected.filter((p) => !p.startsWith(path));
} else {
// Если папка не выбрана, добавляем ее и все вложенные файлы
newSelected.push(path);
const addChildPaths = (nodes: FileNode[]) => {
for (const node of nodes) {
newSelected.push(node.path);
if (node.isDirectory) {
addChildPaths(node.children);
}
}
};
addChildPaths(children);
}
} else {
// Для обычного файла просто переключаем состояние
if (selectedFiles.includes(path)) {
newSelected = newSelected.filter((p) => p !== path);
} else {
newSelected.push(path);
}
}
setSelectedFiles(newSelected);
onSelectionChange(newSelected);
};
// Переключение раскрытия папки
const toggleFolder = (path: string) => {
const newExpanded = new Set(expandedFolders);
if (expandedFolders.has(path)) {
newExpanded.delete(path);
} else {
newExpanded.add(path);
}
setExpandedFolders(newExpanded);
};
// Рекурсивный компонент для отображения файлов и папок
const renderFileTree = (nodes: FileNode[]) => {
// Сортировка: сначала папки, потом файлы
const sortedNodes = [...nodes].sort((a, b) => {
// Если у элементов разные типы (папка/файл)
if (a.isDirectory !== b.isDirectory) {
return a.isDirectory ? -1 : 1; // Папки идут первыми
}
// Если оба элемента одного типа, сортируем по алфавиту
return a.name.localeCompare(b.name);
});
return (
<List dense>
{sortedNodes.map((node) => (
<div key={node.path}>
<ListItem
sx={{
borderRadius: '3vw',
backgroundColor: '#FFFFFF1A',
marginBottom: '1vh',
}}
>
<ListItemIcon>
<Checkbox
edge="start"
checked={selectedFiles.includes(node.path)}
onChange={() =>
handleToggle(node.path, node.isDirectory, node.children)
}
tabIndex={-1}
sx={{ color: 'white' }}
/>
</ListItemIcon>
{node.isDirectory && (
<ListItemIcon onClick={() => toggleFolder(node.path)}>
{expandedFolders.has(node.path) ? (
<ExpandLessIcon sx={{ color: 'white' }} />
) : (
<ExpandMoreIcon sx={{ color: 'white' }} />
)}
</ListItemIcon>
)}
<ListItemIcon>
{node.isDirectory ? (
<FolderIcon sx={{ color: 'white' }} />
) : (
<InsertDriveFileIcon sx={{ color: 'white' }} />
)}
</ListItemIcon>
<ListItemText
primary={node.name}
sx={{ color: 'white', fontFamily: 'Benzin-Bold' }}
/>
</ListItem>
{node.isDirectory && node.children.length > 0 && (
<Collapse
in={expandedFolders.has(node.path)}
timeout="auto"
unmountOnExit
>
<Box sx={{ pl: 4 }}>{renderFileTree(node.children)}</Box>
</Collapse>
)}
</div>
))}
</List>
);
};
if (loading) {
return <CircularProgress />;
}
if (error) {
return <Typography color="error">{error}</Typography>;
}
return (
<Box sx={{ maxHeight: '300px', overflow: 'auto' }}>
{renderFileTree(files)}
</Box>
);
}

View File

@ -33,7 +33,6 @@ const useConfig = () => {
return {
username: '',
password: '',
memory: 4096,
comfortVersion: '',
accessToken: '',
clientToken: '',

View File

@ -5,11 +5,16 @@ import {
Snackbar,
Alert,
LinearProgress,
Modal,
} from '@mui/material';
import { useEffect, useState } from 'react';
import { useNavigate } 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 MemorySlider from '../components/Login/MemorySlider';
import FilesSelector from '../components/FilesSelector';
declare global {
interface Window {
@ -34,12 +39,19 @@ interface LaunchPageProps {
baseVersion: string;
serverIp: string;
fabricVersion: string;
preserveFiles: string[];
};
}
const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
const navigate = useNavigate();
// Начальное состояние должно быть пустым или с минимальными значениями
const [config, setConfig] = useState<{
memory: number;
preserveFiles: string[];
}>({
memory: 0,
preserveFiles: [],
});
const [isDownloading, setIsDownloading] = useState(false);
const [downloadProgress, setDownloadProgress] = useState(0);
const [buffer, setBuffer] = useState(10);
@ -51,6 +63,9 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
}>({ open: false, message: '', severity: 'info' });
const [installStep, setInstallStep] = useState('');
const [installMessage, setInstallMessage] = useState('');
const [open, setOpen] = React.useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
useEffect(() => {
const savedConfig = localStorage.getItem('launcher_config');
@ -85,6 +100,29 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
};
}, [navigate]);
useEffect(() => {
// Загрузка конфигурации сборки при монтировании
const loadPackConfig = async () => {
try {
const result = await window.electron.ipcRenderer.invoke(
'load-pack-config',
{
packName: launchOptions.packName,
},
);
if (result.success && result.config) {
// Полностью заменяем config значениями из файла
setConfig(result.config);
}
} catch (error) {
console.error('Ошибка при загрузке настроек:', error);
}
};
loadPackConfig();
}, [launchOptions.packName]);
const showNotification = (
message: string,
severity: 'success' | 'error' | 'info',
@ -102,6 +140,19 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
setDownloadProgress(0);
setBuffer(10);
// Загружаем настройки сборки
const result = await window.electron.ipcRenderer.invoke(
'load-pack-config',
{
packName: launchOptions.packName,
},
);
// Используйте уже существующий state вместо локальной переменной
if (result.success && result.config) {
setConfig(result.config); // Обновляем state
}
const savedConfig = JSON.parse(
localStorage.getItem('launcher_config') || '{}',
);
@ -112,7 +163,7 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
apiReleaseUrl: launchOptions.apiReleaseUrl,
versionFileName: launchOptions.versionFileName,
packName: launchOptions.packName,
preserveFiles: launchOptions.preserveFiles,
preserveFiles: config.preserveFiles,
};
// Передаем опции для скачивания
@ -142,7 +193,7 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
accessToken: savedConfig.accessToken,
uuid: savedConfig.uuid,
username: savedConfig.username,
memory: launchOptions.memory,
memory: config.memory, // Используем state
baseVersion: launchOptions.baseVersion,
packName: launchOptions.packName,
serverIp: launchOptions.serverIp,
@ -180,6 +231,29 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
}
};
// Функция для сохранения настроек
const savePackConfig = async () => {
try {
const configToSave = {
memory: config.memory,
preserveFiles: config.preserveFiles || [],
};
await window.electron.ipcRenderer.invoke('save-pack-config', {
packName: launchOptions.packName,
config: configToSave,
});
// Обновляем launchOptions
launchOptions.memory = config.memory;
showNotification('Настройки сохранены', 'success');
} catch (error) {
console.error('Ошибка при сохранении настроек:', error);
showNotification('Ошибка сохранения настроек', 'error');
}
};
return (
<Box sx={{ gap: '1vh', display: 'flex', flexDirection: 'column' }}>
<PopaPopa />
@ -235,13 +309,46 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
</Box>
</Box>
) : (
<Button
variant="contained"
color="primary"
onClick={handleLaunchMinecraft}
<Box
sx={{
display: 'flex',
gap: '1vw',
width: '100%', // родитель занимает всю ширину
}}
>
Запустить Minecraft
</Button>
{/* Первая кнопка — растягивается на всё доступное пространство */}
<Button
variant="contained"
color="primary"
onClick={handleLaunchMinecraft}
sx={{
flexGrow: 1, // занимает всё свободное место
width: 'auto', // ширина подстраивается
borderRadius: '3vw',
fontFamily: 'Benzin-Bold',
background: 'linear-gradient(90deg, #3B96FF 0%, #FFB7ED 100%)',
}}
>
Запустить Minecraft
</Button>
{/* Вторая кнопка — квадратная, фиксированного размера (ширина = высоте) */}
<Button
variant="contained"
sx={{
flexShrink: 0, // не сжимается
aspectRatio: '1', // ширина = высоте
backgroundColor: 'grey',
borderRadius: '3vw',
minHeight: 'unset',
minWidth: 'unset',
height: '100%', // занимает полную высоту родителя
}}
onClick={handleOpen}
>
<SettingsIcon />
</Button>
</Box>
)}
<Snackbar
@ -257,6 +364,66 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => {
{notification.message}
</Alert>
</Snackbar>
<Modal
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
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>
);
};

View File

@ -1,10 +1,10 @@
import { Box, Typography } from '@mui/material';
import useConfig from '../hooks/useConfig';
import useAuth from '../hooks/useAuth';
import AuthForm from '../components/Login/AuthForm';
import MemorySlider from '../components/Login/MemorySlider';
import { useNavigate } from 'react-router-dom';
import PopaPopa from '../components/popa-popa';
import useConfig from '../hooks/useConfig';
const Login = () => {
const navigate = useNavigate();