From 1b50a7d4e4ece1e5e343096e1f2dedff28c2cc0f Mon Sep 17 00:00:00 2001 From: DIKER0K Date: Mon, 7 Jul 2025 06:56:30 +0500 Subject: [PATCH] feat: improved launch settings --- src/main/main.ts | 2 + src/main/minecraft-launcher.ts | 124 ++++++++++++- src/main/preload.ts | 4 +- src/renderer/App.tsx | 7 - src/renderer/components/FilesSelector.tsx | 205 ++++++++++++++++++++++ src/renderer/hooks/useConfig.ts | 1 - src/renderer/pages/LaunchPage.tsx | 185 ++++++++++++++++++- src/renderer/pages/Login.tsx | 2 +- 8 files changed, 510 insertions(+), 20 deletions(-) create mode 100644 src/renderer/components/FilesSelector.tsx diff --git a/src/main/main.ts b/src/main/main.ts index f50bd20..684a432 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -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(); }; /** diff --git a/src/main/minecraft-launcher.ts b/src/main/minecraft-launcher.ts index 657edf6..909a078 100644 --- a/src/main/minecraft-launcher.ts +++ b/src/main/minecraft-launcher.ts @@ -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], + }, + }; + } + }); +} diff --git a/src/main/preload.ts b/src/main/preload.ts index 92a8665..6d94468 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -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: { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index e66d7cd..38eaa14 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -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 }) => { diff --git a/src/renderer/components/FilesSelector.tsx b/src/renderer/components/FilesSelector.tsx new file mode 100644 index 0000000..5220411 --- /dev/null +++ b/src/renderer/components/FilesSelector.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + // Используем initialSelected для начального состояния + const [selectedFiles, setSelectedFiles] = useState(initialSelected); + const [expandedFolders, setExpandedFolders] = useState>( + 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 ( + + {sortedNodes.map((node) => ( +
+ + + + handleToggle(node.path, node.isDirectory, node.children) + } + tabIndex={-1} + sx={{ color: 'white' }} + /> + + + {node.isDirectory && ( + toggleFolder(node.path)}> + {expandedFolders.has(node.path) ? ( + + ) : ( + + )} + + )} + + + {node.isDirectory ? ( + + ) : ( + + )} + + + + + + {node.isDirectory && node.children.length > 0 && ( + + {renderFileTree(node.children)} + + )} +
+ ))} +
+ ); + }; + + if (loading) { + return ; + } + + if (error) { + return {error}; + } + + return ( + + {renderFileTree(files)} + + ); +} diff --git a/src/renderer/hooks/useConfig.ts b/src/renderer/hooks/useConfig.ts index 6270e48..0642130 100644 --- a/src/renderer/hooks/useConfig.ts +++ b/src/renderer/hooks/useConfig.ts @@ -33,7 +33,6 @@ const useConfig = () => { return { username: '', password: '', - memory: 4096, comfortVersion: '', accessToken: '', clientToken: '', diff --git a/src/renderer/pages/LaunchPage.tsx b/src/renderer/pages/LaunchPage.tsx index 4152213..bdc357a 100644 --- a/src/renderer/pages/LaunchPage.tsx +++ b/src/renderer/pages/LaunchPage.tsx @@ -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 ( @@ -235,13 +309,46 @@ const LaunchPage = ({ launchOptions }: LaunchPageProps) => { ) : ( - + {/* Первая кнопка — растягивается на всё доступное пространство */} + + + {/* Вторая кнопка — квадратная, фиксированного размера (ширина = высоте) */} + + )} { {notification.message} + + + + + Файлы и папки, которые будут сохранены после переустановки сборки + + { + setConfig((prev) => ({ ...prev, preserveFiles: selected })); + }} + /> + + Оперативная память выделенная для Minecraft + + { + setConfig((prev) => ({ ...prev, memory: value as number })); + }} + /> + + + ); }; diff --git a/src/renderer/pages/Login.tsx b/src/renderer/pages/Login.tsx index 7464565..1ed3337 100644 --- a/src/renderer/pages/Login.tsx +++ b/src/renderer/pages/Login.tsx @@ -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();