diff --git a/src/main/main.ts b/src/main/main.ts index 5b23377..a724e67 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -119,7 +119,7 @@ const createWindow = async () => { width: 1024, height: 850, autoHideMenuBar: true, - resizable: false, + resizable: true, frame: false, icon: getAssetPath('icon.png'), webPreferences: { diff --git a/src/main/minecraft-launcher.ts b/src/main/minecraft-launcher.ts index 50a55eb..dfe7c4e 100644 --- a/src/main/minecraft-launcher.ts +++ b/src/main/minecraft-launcher.ts @@ -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: [] }; } }); } diff --git a/src/main/preload.ts b/src/main/preload.ts index db18036..c6542dd 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -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: { diff --git a/src/renderer/App.css b/src/renderer/App.css index acfd42a..f888bad 100644 --- a/src/renderer/App.css +++ b/src/renderer/App.css @@ -47,3 +47,7 @@ h4 { h5 { font-family: 'Benzin-Bold' !important; } + +h6 { + font-family: 'Benzin-Bold' !important; +} diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index c2f5f7f..3180893 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -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', }} > diff --git a/src/renderer/components/Settings/SettingsModal.tsx b/src/renderer/components/Settings/SettingsModal.tsx new file mode 100644 index 0000000..71264b2 --- /dev/null +++ b/src/renderer/components/Settings/SettingsModal.tsx @@ -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 ( + + + + Файлы и папки, которые будут сохранены после переустановки сборки + + { + onConfigChange({ ...config, preserveFiles: selected }); + }} + /> + + Оперативная память выделенная для Minecraft + + { + onConfigChange({ ...config, memory: value as number }); + }} + /> + + + + ); +}; + +export default SettingsModal; diff --git a/src/renderer/pages/LaunchPage.tsx b/src/renderer/pages/LaunchPage.tsx index 6a20b6b..562d117 100644 --- a/src/renderer/pages/LaunchPage.tsx +++ b/src/renderer/pages/LaunchPage.tsx @@ -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(null); @@ -417,65 +418,14 @@ const LaunchPage = ({ onLaunchPage, launchOptions = {} as any }: LaunchPageProps - - - - Файлы и папки, которые будут сохранены после переустановки сборки - - { - setConfig((prev) => ({ ...prev, preserveFiles: selected })); - }} - /> - - Оперативная память выделенная для Minecraft - - { - setConfig((prev) => ({ ...prev, memory: value as number })); - }} - /> - - - + config={config} + onConfigChange={setConfig} + packName={versionId || versionConfig?.packName || 'Comfort'} + onSave={savePackConfig} + /> ); }; diff --git a/src/renderer/pages/Login.tsx b/src/renderer/pages/Login.tsx index 1ed3337..3ac0586 100644 --- a/src/renderer/pages/Login.tsx +++ b/src/renderer/pages/Login.tsx @@ -38,13 +38,25 @@ const Login = () => { console.log( 'Не удалось обновить токен, требуется новая авторизация', ); - const newSession = await authenticateWithElyBy( - config.username, - config.password, - saveConfig, - ); - if (!newSession) { - console.log('Авторизация не удалась'); + // Очищаем недействительные токены + saveConfig({ + accessToken: '', + clientToken: '', + }); + + // Пытаемся выполнить новую авторизацию + if (config.password) { + const newSession = await authenticateWithElyBy( + config.username, + config.password, + saveConfig, + ); + if (!newSession) { + console.log('Авторизация не удалась'); + return; + } + } else { + console.log('Требуется ввод пароля для новой авторизации'); return; } } @@ -53,6 +65,13 @@ const Login = () => { } } 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: '', + }); } }; diff --git a/src/renderer/pages/VersionsExplorer.tsx b/src/renderer/pages/VersionsExplorer.tsx index 265fc1d..793627a 100644 --- a/src/renderer/pages/VersionsExplorer.tsx +++ b/src/renderer/pages/VersionsExplorer.tsx @@ -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 = ({ 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)} > - - - - - {version} - - - - - + = ({ > {name} - - Версия: {version} - - - - - ); }; @@ -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([]); + const [installedVersions, setInstalledVersions] = useState([]); + const [availableVersions, setAvailableVersions] = useState< + AvailableVersionInfo[] + >([]); const [loading, setLoading] = useState(true); + const [modalOpen, setModalOpen] = useState(false); + const [downloadLoading, setDownloadLoading] = useState(null); const navigate = useNavigate(); useEffect(() => { const fetchVersions = async () => { try { - const result = await window.electron.ipcRenderer.invoke( - 'get-available-versions', + setLoading(true); + + // Получаем список установленных версий через IPC + const installedResult = await window.electron.ipcRenderer.invoke( + 'get-installed-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 }, - ); + if (installedResult.success) { + setInstalledVersions(installedResult.versions); + } - return { - ...version, - config: configResult.success ? configResult.config : undefined, - }; - }), - ); - - setVersions(versionsWithConfig); - } else { - console.error('Ошибка получения версий:', result.error); + // Получаем доступные версии с GitHub Gist + const availableResult = await window.electron.ipcRenderer.invoke( + 'get-available-versions', + { + gistUrl: + 'https://gist.githubusercontent.com/DIKER0K/06cd12fb3a4d08b1f0f8c763a7d05e06/raw/versions.json', + }, + ); + 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 - : [ - { - 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'], - }, - }, - ]; + 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', + { + 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 = () => ( + + + + Добавить + + + версию + + + ); return ( - - Доступные версии - - {loading ? ( @@ -241,28 +267,128 @@ export const VersionsExplorer = () => { container spacing={3} sx={{ - display: 'flex', - flexWrap: 'wrap', - justifyContent: 'center', width: '100%', + overflowY: 'auto', + justifyContent: 'center', }} > - {displayVersions.map((version) => ( - - handleSelectVersion(version)} - /> + {/* Показываем установленные версии или дефолтную, если она есть */} + {installedVersions.length > 0 ? ( + installedVersions.map((version) => ( + + handleSelectVersion(version)} + /> + + )) + ) : ( + // Если нет ни одной версии, показываем карточку добавления + + - ))} + )} + + {/* Всегда добавляем карточку для добавления новых версий */} + {installedVersions.length > 0 && ( + + + + )} )} + + {/* Модальное окно для выбора версии для скачивания */} + + + + Доступные версии для скачивания + + + {availableVersions.length === 0 ? ( + + Загрузка доступных версий... + + ) : ( + + {availableVersions.map((version) => ( + handleSelectVersion(version)} + > + + + ))} + + )} + + + + ); };