Files
popa-launcher/src/renderer/components/PlayerInventory.tsx
2025-07-21 17:07:49 +05:00

373 lines
12 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.

// src/renderer/components/PlayerInventory.tsx
import { useEffect, useState } from 'react';
import {
Box,
Typography,
Grid,
Card,
CardMedia,
CardContent,
Button,
CircularProgress,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Alert,
} from '@mui/material';
import {
RequestPlayerInventory,
getPlayerInventory,
sellItem,
PlayerInventoryItem,
} from '../api';
interface PlayerInventoryProps {
username: string;
serverIp: string;
onSellSuccess?: () => void; // Callback для обновления маркетплейса после продажи
}
export default function PlayerInventory({
username,
serverIp,
onSellSuccess,
}: PlayerInventoryProps) {
const [loading, setLoading] = useState<boolean>(false);
const [inventoryItems, setInventoryItems] = useState<PlayerInventoryItem[]>(
[],
);
const [error, setError] = useState<string | null>(null);
const [sellDialogOpen, setSellDialogOpen] = useState<boolean>(false);
const [selectedItem, setSelectedItem] = useState<PlayerInventoryItem | null>(
null,
);
const [price, setPrice] = useState<number>(0);
const [amount, setAmount] = useState<number>(1);
const [sellLoading, setSellLoading] = useState<boolean>(false);
const [sellError, setSellError] = useState<string | null>(null);
// Функция для запроса инвентаря игрока
const fetchPlayerInventory = async () => {
try {
setLoading(true);
setError(null);
// Сначала делаем запрос на получение идентификатора запроса инвентаря
const inventoryRequest = await RequestPlayerInventory(serverIp, username);
const requestId = inventoryRequest.request_id;
// Затем начинаем опрашивать API для получения результата
let inventoryData = null;
let attempts = 0;
const maxAttempts = 10; // Максимальное количество попыток
while (!inventoryData && attempts < maxAttempts) {
attempts++;
try {
// Пауза перед следующим запросом
await new Promise((resolve) => setTimeout(resolve, 1000));
// Запрашиваем состояние инвентаря
const response = await getPlayerInventory(requestId);
// Если инвентарь загружен, сохраняем его
if (response.status === 'completed') {
inventoryData = response.result.inventory_data;
break;
}
} catch (e) {
console.log('Ожидание завершения запроса инвентаря...');
}
}
if (inventoryData) {
setInventoryItems(inventoryData);
} else {
setError('Не удалось получить инвентарь. Попробуйте еще раз.');
}
} catch (e) {
console.error('Ошибка при получении инвентаря:', e);
setError('Произошла ошибка при загрузке инвентаря.');
} finally {
setLoading(false);
}
};
// Открываем диалог для продажи предмета
const handleOpenSellDialog = (item: PlayerInventoryItem) => {
setSelectedItem(item);
setAmount(1);
setPrice(0);
setSellError(null);
setSellDialogOpen(true);
};
// Закрываем диалог
const handleCloseSellDialog = () => {
setSellDialogOpen(false);
setSelectedItem(null);
};
// Выставляем предмет на продажу
const handleSellItem = async () => {
if (!selectedItem) return;
try {
setSellLoading(true);
setSellError(null);
// Проверяем валидность введенных данных
if (price <= 0) {
setSellError('Цена должна быть больше 0');
return;
}
if (amount <= 0 || amount > selectedItem.amount) {
setSellError(`Количество должно быть от 1 до ${selectedItem.amount}`);
return;
}
// Отправляем запрос на продажу
const result = await sellItem(
username,
selectedItem.slot,
amount,
price,
serverIp,
);
// Проверяем статус операции
if (result.status === 'pending') {
// Закрываем диалог и обновляем инвентарь
handleCloseSellDialog();
// Показываем уведомление о том, что операция обрабатывается
// setNotification({ // Assuming setNotification is available in the context
// open: true,
// message: 'Предмет выставляется на продажу. Это может занять некоторое время.',
// type: 'info'
// });
// Через 5 секунд обновляем инвентарь
setTimeout(() => {
fetchPlayerInventory();
// Вызываем callback для обновления маркетплейса
if (onSellSuccess) {
onSellSuccess();
}
}, 5000);
}
} catch (e) {
console.error('Ошибка при продаже предмета:', e);
setSellError('Произошла ошибка при продаже предмета.');
} finally {
setSellLoading(false);
}
};
// Загружаем инвентарь при монтировании компонента
useEffect(() => {
fetchPlayerInventory();
}, [username, serverIp]);
// Получаем отображаемое имя предмета
const getItemDisplayName = (material: string) => {
return material
.replace(/_/g, ' ')
.toLowerCase()
.replace(/\b\w/g, (l) => l.toUpperCase());
};
return (
<Box sx={{ mt: '1vw' }}>
<Box
sx={{
display: 'flex',
gap: '1vw',
alignItems: 'center',
mb: '2vw',
}}
>
<Typography variant="h5" color="white">
Ваш инвентарь
</Typography>
<Button
variant="outlined"
onClick={fetchPlayerInventory}
disabled={loading}
sx={{
borderRadius: '20px',
p: '10px 25px',
color: 'white',
borderColor: 'rgba(255, 77, 77, 1)',
'&:hover': {
backgroundColor: 'rgba(255, 77, 77, 1)',
borderColor: 'rgba(255, 77, 77, 1)',
},
fontFamily: 'Benzin-Bold',
fontSize: '1vw',
}}
>
Обновить
</Button>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', my: 4 }}>
<CircularProgress />
</Box>
) : (
<>
{inventoryItems.length === 0 ? (
<Typography
variant="body1"
color="white"
sx={{ textAlign: 'center', my: 4 }}
>
Ваш инвентарь пуст или не удалось загрузить предметы.
</Typography>
) : (
<Grid container spacing={2}>
{inventoryItems.map((item) =>
item.material !== 'AIR' && item.amount > 0 ? (
<Grid item xs={6} sm={4} md={3} lg={2} key={item.slot}>
<Card
sx={{
bgcolor: 'rgba(255, 255, 255, 0.05)',
cursor: 'pointer',
transition: 'transform 0.2s',
'&:hover': { transform: 'scale(1.03)' },
borderRadius: '1vw',
}}
onClick={() => handleOpenSellDialog(item)}
>
<CardMedia
component="img"
sx={{
minWidth: '10vw',
minHeight: '10vw',
maxHeight: '10vw',
objectFit: 'contain',
bgcolor: 'white',
p: '1vw',
imageRendering: 'pixelated',
}}
image={`/minecraft/${item.material.toLowerCase()}.png`}
alt={item.material}
/>
<CardContent sx={{ p: 1 }}>
<Box sx={{ display: 'flex', gap: '1vw', justifyContent: 'space-between' }}>
<Typography variant="body2" color="white" noWrap>
{getItemDisplayName(item.material)}
</Typography>
<Typography variant="body2" color="white">
{item.amount > 1 ? `x${item.amount}` : ''}
</Typography>
</Box>
{Object.keys(item.enchants || {}).length > 0 && (
<Typography
variant="caption"
color="secondary"
sx={{ display: 'block' }}
>
Зачарования: {Object.keys(item.enchants).length}
</Typography>
)}
</CardContent>
</Card>
</Grid>
) : null,
)}
</Grid>
)}
</>
)}
{/* Диалог для продажи предмета */}
<Dialog open={sellDialogOpen} onClose={handleCloseSellDialog}>
<DialogTitle>Продать предмет</DialogTitle>
<DialogContent>
{selectedItem && (
<Box sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<CardMedia
component="img"
sx={{
width: 50,
height: 50,
objectFit: 'contain',
mr: 2,
}}
image={`/items/${selectedItem.material.toLowerCase()}.png`}
alt={selectedItem.material}
/>
<Typography variant="h6">
{getItemDisplayName(selectedItem.material)}
</Typography>
</Box>
<Typography variant="body2" gutterBottom>
Всего доступно: {selectedItem.amount}
</Typography>
<TextField
label="Количество"
type="number"
fullWidth
margin="dense"
value={amount}
onChange={(e) =>
setAmount(
Math.min(
parseInt(e.target.value) || 0,
selectedItem.amount,
),
)
}
inputProps={{ min: 1, max: selectedItem.amount }}
/>
<TextField
label="Цена (за всё)"
type="number"
fullWidth
margin="dense"
value={price}
onChange={(e) => setPrice(parseInt(e.target.value) || 0)}
inputProps={{ min: 1 }}
/>
{sellError && (
<Alert severity="error" sx={{ mt: 2 }}>
{sellError}
</Alert>
)}
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseSellDialog}>Отмена</Button>
<Button
onClick={handleSellItem}
variant="contained"
color="primary"
disabled={sellLoading}
>
{sellLoading ? <CircularProgress size={24} /> : 'Продать'}
</Button>
</DialogActions>
</Dialog>
</Box>
);
}