import { useEffect, useState } from 'react'; import { Box, Typography, Grid, Button, Paper, FormControl, Select, MenuItem, InputLabel } from '@mui/material'; import { FullScreenLoader } from '../components/FullScreenLoader'; import { translateServer } from '../utils/serverTranslator'; import { fetchInventoryItems, withdrawInventoryItem, type InventoryRawItem, type InventoryItemsResponse, } from '../api'; import CustomTooltip from '../components/Notifications/CustomTooltip'; import { getPlayerServer } from '../utils/playerOnlineCheck'; const KNOWN_SERVER_IPS = [ 'minecraft.hub.popa-popa.ru', 'minecraft.survival.popa-popa.ru', 'minecraft.minigames.popa-popa.ru', ]; const STORAGE_KEY = 'inventory_layout'; function stripMinecraftColors(text?: string | null): string { if (!text) return ''; return text.replace(/§[0-9A-FK-ORa-fk-or]/g, ''); } const CARD_BG = 'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.14), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.12), transparent 55%), rgba(10,10,20,0.86)'; const CardFacePaperSx = { borderRadius: '1.2vw', background: CARD_BG, border: '1px solid rgba(255,255,255,0.08)', boxShadow: '0 1.2vw 3.2vw rgba(0,0,0,0.55)', color: 'white', } as const; function readInventoryLayout(): any { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : {}; } catch { return {}; } } function writeInventoryLayout(next: any) { localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); } function getLayout(username: string, serverIp: string): Record { const inv = readInventoryLayout(); return inv?.[username]?.[serverIp] ?? {}; } function setLayout(username: string, serverIp: string, layout: Record) { const inv = readInventoryLayout(); inv[username] ??= {}; inv[username][serverIp] = layout; writeInventoryLayout(inv); } function buildSlots( items: InventoryRawItem[], layout: Record, size = 28, ): (InventoryRawItem | null)[] { const slots: (InventoryRawItem | null)[] = Array.from({ length: size }, () => null); const byId = new Map(items.map((it) => [it.id, it])); const used = new Set(); // 1) ставим туда, куда сохранено for (const [id, idx] of Object.entries(layout)) { const i = Number(idx); const it = byId.get(id); if (!it) continue; if (Number.isFinite(i) && i >= 0 && i < size && !slots[i]) { slots[i] = it; used.add(id); } } // 2) остальные — в первые пустые for (const it of items) { if (used.has(it.id)) continue; const empty = slots.findIndex((x) => x === null); if (empty === -1) break; slots[empty] = it; used.add(it.id); } return slots; } export default function Inventory() { const [username, setUsername] = useState(''); const [loading, setLoading] = useState(true); const [availableServers, setAvailableServers] = useState([]); const [selectedServerIp, setSelectedServerIp] = useState(''); const [items, setItems] = useState([]); const [page, setPage] = useState(1); const limit = 28; const [total, setTotal] = useState(0); const [pages, setPages] = useState(1); const [withdrawingIds, setWithdrawingIds] = useState([]); const [hoveredId, setHoveredId] = useState(null); const [isOnline, setIsOnline] = useState(false); const [playerServer, setPlayerServer] = useState(null); const [checkingOnline, setCheckingOnline] = useState(false); const [draggedItemId, setDraggedItemId] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); const [dragPos, setDragPos] = useState<{ x: number; y: number } | null>(null); type SlotItem = InventoryRawItem | null; const [slots, setSlots] = useState(() => Array.from({ length: 28 }, () => null)); const [draggedFromIndex, setDraggedFromIndex] = useState(null); useEffect(() => { const handleMove = (e: MouseEvent) => { if (!draggedItemId) return; setDragPos({ x: e.clientX, y: e.clientY }); }; const handleUp = () => { if ( draggedItemId && draggedFromIndex !== null && dragOverIndex !== null && draggedFromIndex !== dragOverIndex ) { setSlots((prev) => { const next = [...prev]; const moving = next[draggedFromIndex]; if (!moving) return prev; // ✅ swap или move в пустоту const target = next[dragOverIndex]; next[dragOverIndex] = moving; next[draggedFromIndex] = target ?? null; // ✅ сохраняем layout (позиции предметов) const layout: Record = {}; next.forEach((it, idx) => { if (it) layout[it.id] = idx; }); if (username && selectedServerIp) { setLayout(username, selectedServerIp, layout); } return next; }); } setDraggedItemId(null); setDraggedFromIndex(null); setDragOverIndex(null); setDragPos(null); }; window.addEventListener('mousemove', handleMove); window.addEventListener('mouseup', handleUp); return () => { window.removeEventListener('mousemove', handleMove); window.removeEventListener('mouseup', handleUp); }; }, [draggedItemId, dragOverIndex]); useEffect(() => { const savedConfig = localStorage.getItem('launcher_config'); if (!savedConfig) { setLoading(false); return; } const config = JSON.parse(savedConfig); if (config?.username) setUsername(config.username); setLoading(false); }, []); const detectServersWithItems = async (u: string) => { const checks = await Promise.allSettled( KNOWN_SERVER_IPS.map(async (ip) => { const res = await fetchInventoryItems(u, ip, 1, 1); return { ip, has: (res.items || []).length > 0 || (res.total ?? 0) > 0 }; }), ); return checks .filter( (r): r is PromiseFulfilledResult<{ ip: string; has: boolean }> => r.status === 'fulfilled', ) .filter((r) => r.value.has) .map((r) => r.value.ip); }; const loadInventory = async (u: string, ip: string, p: number) => { const res: InventoryItemsResponse = await fetchInventoryItems(u, ip, p, limit); const list = res.items || []; setItems(res.items || []); setTotal(res.total ?? 0); setPages(Math.max(1, Math.ceil((res.total ?? 0) / (res.limit ?? limit)))); const layout = getLayout(u, ip); setSlots(buildSlots(list, layout, 28)); }; useEffect(() => { if (!username) return; let cancelled = false; (async () => { try { setLoading(true); const servers = await detectServersWithItems(username); if (cancelled) return; setAvailableServers(servers); const defaultIp = servers[0] || ''; setSelectedServerIp(defaultIp); setPage(1); if (defaultIp) { await loadInventory(username, defaultIp, 1); } else { setItems([]); setTotal(0); setPages(1); } } catch (e) { console.error(e); setAvailableServers([]); setSelectedServerIp(''); setItems([]); setTotal(0); setPages(1); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [username]); useEffect(() => { if (!username || !selectedServerIp) return; let cancelled = false; (async () => { try { setLoading(true); setPage(1); await loadInventory(username, selectedServerIp, 1); } catch (e) { console.error(e); setItems([]); setTotal(0); setPages(1); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [selectedServerIp]); const withWithdrawing = async (id: string, fn: () => Promise) => { setWithdrawingIds((prev) => [...prev, id]); try { await fn(); } finally { setWithdrawingIds((prev) => prev.filter((x) => x !== id)); } }; const handleWithdraw = async (item: InventoryRawItem) => { if (!username || !selectedServerIp) return; // сервер в UI может не совпадать — оставим защиту if (selectedServerIp !== item.server_ip) { alert('Ошибка! Вы не на том сервере для выдачи этого предмета.'); return; } await withWithdrawing(item.id, async () => { try { await withdrawInventoryItem({ username, item_id: item.id, server_ip: selectedServerIp, }); setItems((prevItems) => prevItems.filter((prevItem) => prevItem.id !== item.id)); setSlots((prev) => { const next = prev.map((x) => (x?.id === item.id ? null : x)); const layout: Record = {}; next.forEach((it, idx) => { if (it) layout[it.id] = idx; }); setLayout(username, selectedServerIp, layout); return next; }); } catch (e) { console.error('Ошибка при выводе предмета:', e); } }); }; const checkPlayerStatus = async () => { if (!username) return; setCheckingOnline(true); try { const res = await getPlayerServer(username); setIsOnline(!!res?.online); setPlayerServer(res?.server?.ip ?? null); } catch (e) { console.error('Ошибка проверки онлайна:', e); setIsOnline(false); setPlayerServer(null); } finally { setCheckingOnline(false); } }; useEffect(() => { if (!username) return; checkPlayerStatus(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [username]); const headerServerName = selectedServerIp ? translateServer(`Server ${selectedServerIp}`) : ''; if (!username) { return ( Не найдено имя игрока. Авторизуйтесь в лаунчере. ); } const canPrev = page > 1; const canNext = page < pages; const handlePrev = async () => { if (!canPrev || !username || !selectedServerIp) return; const nextPage = page - 1; setPage(nextPage); setLoading(true); try { await loadInventory(username, selectedServerIp, nextPage); } finally { setLoading(false); } }; const handleNext = async () => { if (!canNext || !username || !selectedServerIp) return; const nextPage = page + 1; setPage(nextPage); setLoading(true); try { await loadInventory(username, selectedServerIp, nextPage); } finally { setLoading(false); } }; return ( {/* ШАПКА + ПАГИНАЦИЯ */} {!!selectedServerIp && ( Страница {page} / {pages} • Всего: {total} )} {availableServers.length > 0 && ( Сервер )} Инвентарь {headerServerName ? `— ${headerServerName}` : ''} {/* GRID */} {loading ? ( ) : ( {Array.from({ length: 28 }).map((_, index) => { const item = slots[index]; // ПУСТАЯ ЯЧЕЙКА if (!item) { return ( { if (draggedItemId) setDragOverIndex(index); }} sx={{ outline: draggedItemId && dragOverIndex === index ? '2px dashed rgba(255,255,255,0.4)' : 'none', }} > Пусто ); } const displayNameRaw = item.item_data?.meta?.display_name ?? item.item_data?.material ?? 'Предмет'; const displayName = stripMinecraftColors(displayNameRaw); const amount = (item as any)?.amount ?? (item as any)?.item_data?.amount ?? (item as any)?.item_data?.meta?.amount ?? 1; const isHovered = hoveredId === item.id; const isWithdrawing = withdrawingIds.includes(item.id); // ✅ проверка: игрок реально онлайн на нужном сервере const isOnRightServer = isOnline && playerServer === item.server_ip; const canWithdraw = isOnRightServer && !loading && !checkingOnline && !isWithdrawing; const texture = item.item_data?.material ? `https://cdn.minecraft.popa-popa.ru/textures/${item.item_data.material.toLowerCase()}.png` : ''; return ( { if (draggedItemId) setDragOverIndex(index); }} sx={{ outline: draggedItemId && dragOverIndex === index ? '2px dashed rgba(255,255,255,0.4)' : 'none', }} > setHoveredId(item.id)} onMouseLeave={() => setHoveredId(null)} onMouseDown={(e) => { const target = e.target as HTMLElement; if (target.closest('button')) return; e.preventDefault(); setDraggedItemId(item.id); setDraggedFromIndex(index); setDragPos({ x: e.clientX, y: e.clientY }); }} > {/* FRONT */} {/* BACK */} {displayName} Кол-во: {amount} {!isOnRightServer ? ( ) : ( )} ); })} )} {draggedItemId && dragPos && (() => { const draggedItem = items.find(i => i.id === draggedItemId); if (!draggedItem) return null; const texture = `https://cdn.minecraft.popa-popa.ru/textures/${draggedItem.item_data.material.toLowerCase()}.png`; return ( ); })()} ); }