From 24423173a60ef6c64399a63fce57992715b640e3 Mon Sep 17 00:00:00 2001 From: aurinex Date: Wed, 17 Dec 2025 00:12:23 +0500 Subject: [PATCH] minor redesign inventory + add functions --- src/renderer/pages/Inventory.tsx | 572 ++++++++++++++++++++++++++----- 1 file changed, 478 insertions(+), 94 deletions(-) diff --git a/src/renderer/pages/Inventory.tsx b/src/renderer/pages/Inventory.tsx index 5018b25..f047cb4 100644 --- a/src/renderer/pages/Inventory.tsx +++ b/src/renderer/pages/Inventory.tsx @@ -1,16 +1,5 @@ import { useEffect, useState } from 'react'; -import { - Box, - Typography, - Grid, - Button, - FormControl, - Select, - MenuItem, - InputLabel, - Tooltip, - Paper, -} from '@mui/material'; +import { Box, Typography, Grid, Button, Paper } from '@mui/material'; import { FullScreenLoader } from '../components/FullScreenLoader'; import { translateServer } from '../utils/serverTranslator'; @@ -20,6 +9,8 @@ import { 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', @@ -32,22 +23,75 @@ function stripMinecraftColors(text?: string | null): string { return text.replace(/§[0-9A-FK-ORa-fk-or]/g, ''); } -const Glass = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); +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 readLauncherConfig(): any { + try { + const raw = localStorage.getItem('launcher_config'); + return raw ? JSON.parse(raw) : {}; + } catch { + return {}; + } +} + +function writeLauncherConfig(next: any) { + localStorage.setItem('launcher_config', JSON.stringify(next)); +} + +function getLayout(username: string, serverIp: string): Record { + const cfg = readLauncherConfig(); + return cfg?.inventory_layout?.[username]?.[serverIp] ?? {}; +} + +function setLayout(username: string, serverIp: string, layout: Record) { + const cfg = readLauncherConfig(); + cfg.inventory_layout ??= {}; + cfg.inventory_layout[username] ??= {}; + cfg.inventory_layout[username][serverIp] = layout; + writeLauncherConfig(cfg); +} + +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(''); @@ -60,7 +104,72 @@ export default function Inventory() { const [total, setTotal] = useState(0); const [pages, setPages] = useState(1); const [withdrawingIds, setWithdrawingIds] = useState([]); - const isServerSelectVisible = availableServers.length > 1; + 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'); @@ -82,19 +191,24 @@ export default function Inventory() { }), ); - const servers = checks - .filter((r): r is PromiseFulfilledResult<{ ip: string; has: boolean }> => r.status === 'fulfilled') + return checks + .filter( + (r): r is PromiseFulfilledResult<{ ip: string; has: boolean }> => r.status === 'fulfilled', + ) .filter((r) => r.value.has) .map((r) => r.value.ip); - - return servers; }; 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(() => { @@ -164,11 +278,21 @@ export default function Inventory() { }; }, [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("Ошибка! Вы не на том сервере для выдачи этого предмета."); + alert('Ошибка! Вы не на том сервере для выдачи этого предмета.'); return; } @@ -181,24 +305,48 @@ export default function Inventory() { }); 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); + console.error('Ошибка при выводе предмета:', e); } }); }; - const withWithdrawing = async (id: string, fn: () => Promise) => { - setWithdrawingIds((prev) => [...prev, id]); + const checkPlayerStatus = async () => { + if (!username) return; + + setCheckingOnline(true); try { - await fn(); + const res = await getPlayerServer(username); + setIsOnline(!!res?.online); + setPlayerServer(res?.server?.ip ?? null); + } catch (e) { + console.error('Ошибка проверки онлайна:', e); + setIsOnline(false); + setPlayerServer(null); } finally { - setWithdrawingIds((prev) => prev.filter((x) => x !== id)); + setCheckingOnline(false); } }; - const headerServerName = selectedServerIp - ? translateServer(`Server ${selectedServerIp}`) - : ''; + useEffect(() => { + if (!username) return; + checkPlayerStatus(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [username]); + + const headerServerName = selectedServerIp ? translateServer(`Server ${selectedServerIp}`) : ''; if (!username) { return ( @@ -253,42 +401,51 @@ export default function Inventory() { > {loading && } - - {/* Пагинация */} + {/* ШАПКА + ПАГИНАЦИЯ */} + {!!selectedServerIp && ( - + - Страница {page} / {pages} • Всего: {total} + Страница {page} / {pages} • Всего: {total} - + )} Инвентарь {headerServerName ? `— ${headerServerName}` : ''} + + {/* GRID */} {Array.from({ length: 28 }).map((_, index) => { - const item = items[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 }); + }} + > item && handleWithdraw(item)} > - {item ? ( - + + + + {/* BACK */} + + - + + + Кол-во: {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 ( + + + + + + ); + })()} ); }