add: capes switch in user profile

This commit is contained in:
2025-07-18 20:14:44 +05:00
parent ec54219192
commit 26f601635b
2 changed files with 319 additions and 113 deletions

View File

@ -1,6 +1,13 @@
import { useEffect, useRef, useState } from 'react';
import SkinViewer from '../components/SkinViewer';
import { fetchPlayer, uploadSkin } from '../api';
import {
fetchPlayer,
uploadSkin,
fetchCapes,
Cape,
activateCape,
deactivateCape,
} from '../api';
import {
Box,
@ -13,6 +20,11 @@ import {
MenuItem,
Alert,
CircularProgress,
Card,
CardContent,
CardMedia,
Tooltip,
CardActions,
} from '@mui/material';
export default function Profile() {
@ -28,6 +40,9 @@ export default function Profile() {
>('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');
@ -36,15 +51,19 @@ export default function Profile() {
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('');
@ -52,6 +71,18 @@ export default function Profile() {
}
};
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();
@ -83,8 +114,24 @@ export default function Profile() {
}
};
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');
@ -111,6 +158,8 @@ export default function Profile() {
`Ошибка: ${error instanceof Error ? error.message : 'Не удалось загрузить скин'}`,
);
setUploadStatus('error');
} finally {
setLoading(false);
}
};
@ -124,124 +173,213 @@ export default function Profile() {
gap: 2,
}}
>
<Paper
elevation={0}
sx={{
p: 0,
borderRadius: 2,
overflow: 'hidden',
mb: 4,
bgcolor: 'transparent',
}}
>
{/* Используем переработанный компонент SkinViewer */}
<SkinViewer
width={300}
height={400}
skinUrl={skin}
capeUrl={cape}
walkingSpeed={walkingSpeed}
autoRotate={true}
/>
</Paper>
<Box sx={{ width: '100%', maxWidth: '500px', mt: 2 }}>
<Typography variant="h6" gutterBottom>
Установить скин
</Typography>
<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)}
{loading ? (
<CircularProgress />
) : (
<>
<Paper
elevation={0}
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)',
},
p: 0,
borderRadius: 2,
overflow: 'hidden',
mb: 4,
bgcolor: 'transparent',
}}
>
<MenuItem value="">По умолчанию</MenuItem>
<MenuItem value="slim">Тонкая (Alex)</MenuItem>
<MenuItem value="classic">Классическая (Steve)</MenuItem>
</Select>
</FormControl>
{/* Используем переработанный компонент SkinViewer */}
<SkinViewer
width={300}
height={400}
skinUrl={skin}
capeUrl={cape}
walkingSpeed={walkingSpeed}
autoRotate={true}
/>
</Paper>
{uploadStatus === 'error' && (
<Alert severity="error" sx={{ mb: 2 }}>
{statusMessage}
</Alert>
)}
<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>
{uploadStatus === 'success' && (
<Alert severity="success" sx={{ mb: 2 }}>
{statusMessage}
</Alert>
)}
<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>
<Button
sx={{ color: 'white' }}
variant="contained"
color="primary"
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>
{uploadStatus === 'error' && (
<Alert severity="error" sx={{ mb: 2 }}>
{statusMessage}
</Alert>
)}
{uploadStatus === 'success' && (
<Alert severity="success" sx={{ mb: 2 }}>
{statusMessage}
</Alert>
)}
<Button
sx={{ color: 'white' }}
variant="contained"
color="primary"
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 }}>
{capes.map((cape) => (
<Tooltip arrow title={cape.cape_description}>
<Card
key={cape.cape_id}
sx={{
bgcolor: 'rgba(255, 255, 255, 0.05)',
width: 200, // фиксированная ширина карточки
overflow: 'hidden',
}}
>
<CardMedia
component="img"
image={cape.image_url}
alt={cape.cape_name}
sx={{
display: 'block',
width: '100%',
transform:
'scale(2.9) translateX(66px) translateY(32px)',
imageRendering: 'pixelated',
}}
/>
<CardContent
sx={{ bgcolor: 'rgba(255, 255, 255, 0.05)', pt: '9vh' }}
>
<Typography sx={{ color: 'white' }}>
{cape.cape_name}
</Typography>
</CardContent>
{cape.is_active ? (
<CardActions
sx={{ display: 'flex', justifyContent: 'center' }}
>
<Button
variant="contained"
sx={{ bgcolor: 'red' }}
onClick={() => handleDeactivateCape(cape.cape_id)}
>
Снять
</Button>
</CardActions>
) : (
<CardActions
sx={{ display: 'flex', justifyContent: 'center' }}
>
<Button
variant="contained"
sx={{ bgcolor: 'green' }}
onClick={() => handleActivateCape(cape.cape_id)}
>
Надеть
</Button>
</CardActions>
)}
</Card>
</Tooltip>
))}
</Box>
</Box>
</Box>
</>
)}
</Box>
);
}