Files
popa-launcher/src/renderer/pages/VersionsExplorer.tsx
2025-12-13 16:18:47 +05:00

488 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from 'react';
import {
Box,
Typography,
Grid,
Card,
CardContent,
Button,
Modal,
List,
ListItem,
ListItemText,
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
import AddIcon from '@mui/icons-material/Add';
import { FullScreenLoader } from '../components/FullScreenLoader';
interface VersionCardProps {
id: string;
name: string;
imageUrl: string;
version: string;
onSelect: (id: string) => void;
isHovered: boolean;
onHover: (id: string | null) => void;
hoveredCardId: string | null;
}
const gradientPrimary =
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)';
export const VersionCard: React.FC<VersionCardProps> = ({
id,
name,
imageUrl, // пока не используется, но оставляем для будущего
version,
onSelect,
isHovered,
onHover,
hoveredCardId,
}) => {
return (
<Card
sx={{
background:
'radial-gradient(circle at top left, rgba(242,113,33,0.2), transparent 55%), rgba(10,10,20,0.95)',
backdropFilter: 'blur(18px)',
width: '35vw',
height: '35vh',
minWidth: 'unset',
minHeight: 'unset',
display: 'flex',
flexDirection: 'column',
borderRadius: '2.5vw',
boxShadow: isHovered
? '0 0 10px rgba(233,64,205,0.55)'
: '0 14px 40px rgba(0, 0, 0, 0.6)',
transition:
'transform 0.35s ease, box-shadow 0.35s ease, border-color 0.35s ease',
overflow: 'hidden',
cursor: 'pointer',
transform: isHovered ? 'scale(1.04)' : 'scale(1)',
zIndex: isHovered ? 10 : 1,
'&:hover': {
borderColor: 'rgba(242,113,33,0.8)',
},
}}
onClick={() => onSelect(id)}
onMouseEnter={() => onHover(id)}
onMouseLeave={() => onHover(null)}
>
<CardContent
sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1vh',
textAlign: 'center',
}}
>
<Typography
gutterBottom
variant="h5"
component="div"
sx={{
fontWeight: 'bold',
fontFamily: 'Benzin-Bold',
fontSize: '2vw',
backgroundImage: gradientPrimary,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
>
{name}
</Typography>
<Typography
variant="subtitle1"
sx={{
color: 'rgba(255,255,255,0.8)',
fontSize: '1.1vw',
}}
>
Версия {version}
</Typography>
</CardContent>
</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[];
};
}
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 [installedVersions, setInstalledVersions] = useState<VersionInfo[]>([]);
const [availableVersions, setAvailableVersions] = useState<
AvailableVersionInfo[]
>([]);
const [loading, setLoading] = useState(true);
const [modalOpen, setModalOpen] = useState(false);
const [downloadLoading, setDownloadLoading] = useState<string | null>(null);
const [hoveredCardId, setHoveredCardId] = useState<string | null>(null);
const navigate = useNavigate();
useEffect(() => {
const fetchVersions = async () => {
try {
setLoading(true);
// Получаем список установленных версий через IPC
const installedResult = await window.electron.ipcRenderer.invoke(
'get-installed-versions',
);
if (installedResult.success) {
setInstalledVersions(installedResult.versions);
}
// Получаем доступные версии с 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);
} finally {
setLoading(false);
}
};
fetchVersions();
}, []);
const handleSelectVersion = (version: VersionInfo | AvailableVersionInfo) => {
const cfg: any = (version as any).config;
if (cfg && (cfg.downloadUrl || cfg.apiReleaseUrl)) {
localStorage.setItem('selected_version_config', JSON.stringify(cfg));
} else {
localStorage.removeItem('selected_version_config');
}
navigate(`/launch/${version.id}`);
};
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 = () => (
<Card
sx={{
background:
'radial-gradient(circle at top left, rgba(233,64,205,0.3), rgba(10,10,20,0.95))',
width: '35vw',
height: '35vh',
display: 'flex',
flexDirection: 'column',
borderRadius: '2.5vw',
position: 'relative',
border: 'none',
boxShadow: '0 14px 40px rgba(0, 0, 0, 0.6)',
overflow: 'hidden',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
transition: 'transform 0.35s ease, box-shadow 0.35s ease',
willChange: 'transform, box-shadow',
'&:hover': {
boxShadow: '0 0 40px rgba(242,113,33,0.7)',
transform: 'scale(1.02)',
zIndex: 10,
},
}}
onClick={handleAddVersion}
>
<AddIcon sx={{ fontSize: '4vw', color: '#fff' }} />
<Typography
variant="h6"
sx={{
color: '#fff',
fontFamily: 'Benzin-Bold',
fontSize: '1.5vw',
mt: 1,
}}
>
Добавить версию
</Typography>
</Card>
);
return (
<Box
sx={{
px: '5vw',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: '2vh',
height: '100%',
}}
>
{loading ? (
<FullScreenLoader message="Загрузка ваших версий..." />
) : (
<Grid
container
spacing={3}
sx={{
width: '100%',
height: '100%',
overflowY: 'auto',
justifyContent: 'center',
alignContent: 'flex-start',
position: 'relative',
zIndex: 1,
pt: '3vh',
}}
>
{installedVersions.length > 0 ? (
installedVersions.map((version) => (
<Grid
key={version.id}
item
xs={12}
sm={6}
md={4}
sx={{
display: 'flex',
justifyContent: 'center',
marginBottom: '2vh',
}}
>
<VersionCard
id={version.id}
name={version.name}
imageUrl={
version.imageUrl ||
'https://via.placeholder.com/300x140?text=Minecraft'
}
version={version.version}
onSelect={() => handleSelectVersion(version)}
isHovered={hoveredCardId === version.id}
onHover={setHoveredCardId}
hoveredCardId={hoveredCardId}
/>
</Grid>
))
) : (
<Grid
item
xs={12}
sm={6}
md={4}
sx={{
display: 'flex',
justifyContent: 'center',
marginBottom: '2vh',
}}
>
<AddVersionCard />
</Grid>
)}
{installedVersions.length > 0 && (
<Grid
item
xs={12}
sm={6}
md={4}
sx={{
display: 'flex',
justifyContent: 'center',
marginBottom: '2vh',
}}
>
<AddVersionCard />
</Grid>
)}
</Grid>
)}
{/* Модальное окно для выбора версии для скачивания */}
<Modal
open={modalOpen}
onClose={handleCloseModal}
aria-labelledby="modal-versions"
aria-describedby="modal-available-versions"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 420,
maxWidth: '90vw',
maxHeight: '80vh',
overflowY: 'auto',
background: 'linear-gradient(145deg, #000000 10%, #8A2387 100%)',
boxShadow: '0 20px 60px rgba(0,0,0,0.85)',
p: 4,
borderRadius: '2.5vw',
gap: '1.5vh',
display: 'flex',
flexDirection: 'column',
backdropFilter: 'blur(18px)',
}}
>
<Typography
variant="h6"
component="h2"
sx={{
color: '#fff',
fontFamily: 'Benzin-Bold',
mb: 1,
}}
>
Доступные версии для скачивания
</Typography>
{availableVersions.length === 0 ? (
<Typography sx={{ color: '#fff', mt: 2 }}>
Загрузка доступных версий...
</Typography>
) : (
<List sx={{ mt: 1 }}>
{availableVersions.map((version) => (
<ListItem
key={version.id}
sx={{
borderRadius: '1vw',
mb: 1,
backgroundColor: 'rgba(0, 0, 0, 0.35)',
border: '1px solid rgba(20,20,20,0.2)',
cursor: 'pointer',
transition:
'background-color 0.25s ease, transform 0.25s ease, box-shadow 0.25s ease',
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.08)',
transform: 'translateY(-2px)',
boxShadow: '0 8px 24px rgba(0,0,0,0.6)',
},
}}
onClick={() => handleSelectVersion(version)}
>
<ListItemText
primary={version.name}
secondary={version.version}
primaryTypographyProps={{
color: '#fff',
fontFamily: 'Benzin-Bold',
}}
secondaryTypographyProps={{
color: 'rgba(255,255,255,0.7)',
}}
/>
{downloadLoading === version.id && (
<Typography
variant="body2"
sx={{ color: 'rgba(255,255,255,0.7)' }}
>
Загрузка...
</Typography>
)}
</ListItem>
))}
</List>
)}
<Button
onClick={handleCloseModal}
variant="contained"
sx={{
mt: 3,
alignSelf: 'center',
px: 6,
py: 1.2,
borderRadius: '2.5vw',
background: gradientPrimary,
fontFamily: 'Benzin-Bold',
fontSize: '1vw',
textTransform: 'none',
'&:hover': {
transform: 'scale(1.01)',
boxShadow: '0 10px 30px rgba(0,0,0,0.6)',
},
}}
>
Закрыть
</Button>
</Box>
</Modal>
</Box>
);
};