488 lines
14 KiB
TypeScript
488 lines
14 KiB
TypeScript
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>
|
||
);
|
||
};
|