374 lines
11 KiB
TypeScript
374 lines
11 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
||
import SkinViewer from '../components/SkinViewer';
|
||
import {
|
||
fetchPlayer,
|
||
uploadSkin,
|
||
fetchCapes,
|
||
Cape,
|
||
activateCape,
|
||
deactivateCape,
|
||
} from '../api';
|
||
|
||
import {
|
||
Box,
|
||
Typography,
|
||
Paper,
|
||
Button,
|
||
FormControl,
|
||
InputLabel,
|
||
Select,
|
||
MenuItem,
|
||
Alert,
|
||
CircularProgress,
|
||
} from '@mui/material';
|
||
|
||
import CapeCard from '../components/CapeCard';
|
||
|
||
export default function Profile() {
|
||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
const [walkingSpeed, setWalkingSpeed] = useState<number>(0.5);
|
||
const [skin, setSkin] = useState<string>('');
|
||
const [cape, setCape] = useState<string>('');
|
||
const [username, setUsername] = useState<string>('');
|
||
const [skinFile, setSkinFile] = useState<File | null>(null);
|
||
const [skinModel, setSkinModel] = useState<string>(''); // slim или classic
|
||
const [uploadStatus, setUploadStatus] = useState<
|
||
'idle' | 'loading' | 'success' | 'error'
|
||
>('idle');
|
||
const [statusMessage, setStatusMessage] = useState<string>('');
|
||
const [isDragOver, setIsDragOver] = useState<boolean>(false);
|
||
const [capes, setCapes] = useState<Cape[]>([]);
|
||
const [uuid, setUuid] = useState<string>('');
|
||
const [loading, setLoading] = useState<boolean>(false);
|
||
|
||
useEffect(() => {
|
||
const savedConfig = localStorage.getItem('launcher_config');
|
||
if (savedConfig) {
|
||
const config = JSON.parse(savedConfig);
|
||
if (config.uuid) {
|
||
loadPlayerData(config.uuid);
|
||
setUsername(config.username || '');
|
||
loadCapesData(config.username || '');
|
||
setUuid(config.uuid || '');
|
||
}
|
||
}
|
||
}, []);
|
||
|
||
const loadPlayerData = async (uuid: string) => {
|
||
try {
|
||
setLoading(true);
|
||
const player = await fetchPlayer(uuid);
|
||
setSkin(player.skin_url);
|
||
setCape(player.cloak_url);
|
||
setLoading(false);
|
||
} catch (error) {
|
||
console.error('Ошибка при получении данных игрока:', error);
|
||
setSkin('');
|
||
setCape('');
|
||
}
|
||
};
|
||
|
||
const loadCapesData = async (username: string) => {
|
||
try {
|
||
setLoading(true);
|
||
const capesData = await fetchCapes(username);
|
||
setCapes(capesData);
|
||
setLoading(false);
|
||
} catch (error) {
|
||
console.error('Ошибка при получении плащей:', error);
|
||
setCapes([]);
|
||
}
|
||
};
|
||
|
||
// Обработка перетаскивания файла
|
||
const handleFileDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||
e.preventDefault();
|
||
setIsDragOver(false);
|
||
|
||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||
const file = e.dataTransfer.files[0];
|
||
if (file.type === 'image/png') {
|
||
setSkinFile(file);
|
||
setStatusMessage(`Файл "${file.name}" готов к загрузке`);
|
||
} else {
|
||
setStatusMessage('Пожалуйста, выберите файл в формате PNG');
|
||
setUploadStatus('error');
|
||
}
|
||
}
|
||
};
|
||
|
||
// Обработка выбора файла
|
||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
if (e.target.files && e.target.files.length > 0) {
|
||
const file = e.target.files[0];
|
||
if (file.type === 'image/png') {
|
||
setSkinFile(file);
|
||
setStatusMessage(`Файл "${file.name}" готов к загрузке`);
|
||
} else {
|
||
setStatusMessage('Пожалуйста, выберите файл в формате PNG');
|
||
setUploadStatus('error');
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleActivateCape = async (cape_id: string) => {
|
||
setLoading(true);
|
||
await activateCape(username, cape_id);
|
||
await loadCapesData(username);
|
||
setLoading(false);
|
||
};
|
||
|
||
const handleDeactivateCape = async (cape_id: string) => {
|
||
setLoading(true);
|
||
await deactivateCape(username, cape_id);
|
||
await loadCapesData(username);
|
||
await loadPlayerData(uuid);
|
||
setLoading(false);
|
||
};
|
||
|
||
// Отправка запроса на установку скина
|
||
const handleUploadSkin = async () => {
|
||
setLoading(true);
|
||
if (!skinFile || !username) {
|
||
setStatusMessage('Необходимо выбрать файл и указать имя пользователя');
|
||
setUploadStatus('error');
|
||
return;
|
||
}
|
||
|
||
setUploadStatus('loading');
|
||
|
||
try {
|
||
await uploadSkin(username, skinFile, skinModel);
|
||
|
||
setStatusMessage('Скин успешно загружен!');
|
||
setUploadStatus('success');
|
||
|
||
// Обновляем информацию о игроке, чтобы увидеть новый скин
|
||
const config = JSON.parse(
|
||
localStorage.getItem('launcher_config') || '{}',
|
||
);
|
||
if (config.uuid) {
|
||
loadPlayerData(config.uuid);
|
||
}
|
||
} catch (error) {
|
||
setStatusMessage(
|
||
`Ошибка: ${error instanceof Error ? error.message : 'Не удалось загрузить скин'}`,
|
||
);
|
||
setUploadStatus('error');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Box
|
||
sx={{
|
||
my: 4,
|
||
display: 'flex',
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: '100px',
|
||
width: '100%',
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
{loading ? (
|
||
<CircularProgress />
|
||
) : (
|
||
<>
|
||
<Paper
|
||
elevation={0}
|
||
sx={{
|
||
p: 0,
|
||
borderRadius: 2,
|
||
overflow: 'hidden',
|
||
mb: 4,
|
||
bgcolor: 'transparent',
|
||
}}
|
||
>
|
||
{/* Используем переработанный компонент SkinViewer */}
|
||
<Typography
|
||
sx={{
|
||
fontFamily: 'Benzin-Bold',
|
||
alignSelf: 'center',
|
||
justifySelf: 'center',
|
||
textAlign: 'center',
|
||
width: '100%',
|
||
mb: '5vw',
|
||
fontSize: '3vw',
|
||
color: 'white',
|
||
}}
|
||
>
|
||
{username}
|
||
</Typography>
|
||
<SkinViewer
|
||
width={300}
|
||
height={400}
|
||
skinUrl={skin}
|
||
capeUrl={cape}
|
||
walkingSpeed={walkingSpeed}
|
||
autoRotate={true}
|
||
/>
|
||
</Paper>
|
||
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: 2,
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
width: '100%',
|
||
maxWidth: '500px',
|
||
bgcolor: 'rgba(255, 255, 255, 0.05)',
|
||
padding: '3vw',
|
||
borderRadius: '1vw',
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
border: '2px dashed',
|
||
borderColor: isDragOver ? 'primary.main' : 'grey.400',
|
||
borderRadius: 2,
|
||
p: 3,
|
||
mb: 2,
|
||
textAlign: 'center',
|
||
cursor: 'pointer',
|
||
bgcolor: isDragOver
|
||
? 'rgba(25, 118, 210, 0.08)'
|
||
: 'transparent',
|
||
transition: 'all 0.2s',
|
||
}}
|
||
onDragOver={(e) => {
|
||
e.preventDefault();
|
||
setIsDragOver(true);
|
||
}}
|
||
onDragLeave={() => setIsDragOver(false)}
|
||
onDrop={handleFileDrop}
|
||
onClick={() => fileInputRef.current?.click()}
|
||
>
|
||
<input
|
||
type="file"
|
||
ref={fileInputRef}
|
||
accept=".png"
|
||
style={{ display: 'none' }}
|
||
onChange={handleFileSelect}
|
||
/>
|
||
<Typography sx={{ color: 'white' }}>
|
||
{skinFile
|
||
? `Выбран файл: ${skinFile.name}`
|
||
: 'Перетащите PNG файл скина или кликните для выбора'}
|
||
</Typography>
|
||
</Box>
|
||
|
||
<FormControl
|
||
color="primary"
|
||
fullWidth
|
||
sx={{ mb: 2, color: 'white' }}
|
||
>
|
||
<InputLabel sx={{ color: 'white' }}>Модель скина</InputLabel>
|
||
<Select
|
||
value={skinModel}
|
||
label="Модель скина"
|
||
onChange={(e) => setSkinModel(e.target.value)}
|
||
sx={{
|
||
color: 'white',
|
||
borderColor: 'white',
|
||
'& .MuiInputBase-input': {
|
||
border: '1px solid white',
|
||
transition: 'unset',
|
||
},
|
||
'&:focus': {
|
||
borderRadius: 4,
|
||
borderColor: '#80bdff',
|
||
boxShadow: '0 0 0 0.2rem rgba(0,123,255,.25)',
|
||
},
|
||
}}
|
||
>
|
||
<MenuItem value="">По умолчанию</MenuItem>
|
||
<MenuItem value="slim">Тонкая (Alex)</MenuItem>
|
||
<MenuItem value="classic">Классическая (Steve)</MenuItem>
|
||
</Select>
|
||
</FormControl>
|
||
|
||
{uploadStatus === 'error' && (
|
||
<Alert severity="error" sx={{ mb: 2 }}>
|
||
{statusMessage}
|
||
</Alert>
|
||
)}
|
||
|
||
{uploadStatus === 'success' && (
|
||
<Alert severity="success" sx={{ mb: 2 }}>
|
||
{statusMessage}
|
||
</Alert>
|
||
)}
|
||
|
||
<Button
|
||
sx={{
|
||
color: 'white',
|
||
borderRadius: '20px',
|
||
p: '10px 25px',
|
||
backgroundColor: 'rgb(0, 134, 0)',
|
||
'&:hover': {
|
||
backgroundColor: 'rgba(0, 134, 0, 0.5)',
|
||
},
|
||
fontFamily: 'Benzin-Bold',
|
||
}}
|
||
variant="contained"
|
||
fullWidth
|
||
onClick={handleUploadSkin}
|
||
disabled={uploadStatus === 'loading' || !skinFile}
|
||
startIcon={
|
||
uploadStatus === 'loading' ? (
|
||
<CircularProgress size={20} color="inherit" />
|
||
) : null
|
||
}
|
||
>
|
||
{uploadStatus === 'loading' ? (
|
||
<Typography sx={{ color: 'white' }}>Загрузка...</Typography>
|
||
) : (
|
||
<Typography sx={{ color: 'white' }}>
|
||
Установить скин
|
||
</Typography>
|
||
)}
|
||
</Button>
|
||
</Box>
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
justifyContent: 'center',
|
||
flexDirection: 'column',
|
||
gap: 2,
|
||
}}
|
||
>
|
||
<Typography>Ваши плащи</Typography>
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
flexDirection: 'row',
|
||
gap: 2,
|
||
flexWrap: 'wrap',
|
||
}}
|
||
>
|
||
{capes.map((cape) => (
|
||
<CapeCard
|
||
key={cape.cape_id}
|
||
cape={cape}
|
||
mode="profile"
|
||
onAction={
|
||
cape.is_active ? handleDeactivateCape : handleActivateCape
|
||
}
|
||
actionDisabled={loading}
|
||
/>
|
||
))}
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
</>
|
||
)}
|
||
</Box>
|
||
);
|
||
}
|