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 ? (
+
+
+
+
+
+ ) : (
+
- ) : (
- Пусто
- )}
+ sx={{
+ fontSize: '0.8vw',
+ borderRadius: '2vw',
+ fontFamily: 'Benzin-Bold',
+ border: '1px solid rgba(255,255,255,0.15)',
+ color: 'white',
+ }}
+ >
+ {isWithdrawing ? 'Выдача...' : 'Выдать'}
+
+ )}
+
-
+
);
})}
-
+ {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 (
+
+
+
+
+
+ );
+ })()}
);
}