add OnlinePlayersPanel in Profile

This commit is contained in:
2025-12-06 21:08:26 +05:00
parent 6a7169e2ae
commit 5efeb9a5c1
7 changed files with 379 additions and 5 deletions

View File

@ -0,0 +1,281 @@
// src/renderer/components/OnlinePlayersPanel.tsx
import { useEffect, useState, useMemo } from 'react';
import {
Box,
Typography,
Paper,
Chip,
TextField,
MenuItem,
Select,
FormControl,
InputLabel,
} from '@mui/material';
import {
fetchActiveServers,
fetchOnlinePlayers,
fetchPlayer,
Server,
} from '../api';
import { FullScreenLoader } from './FullScreenLoader';
import { HeadAvatar } from './HeadAvatar';
import { translateServer } from '../utils/serverTranslator';
type OnlinePlayerFlat = {
username: string;
uuid: string;
serverId: string;
serverName: string;
onlineSince: string;
};
interface OnlinePlayersPanelProps {
currentUsername: string;
}
export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
currentUsername,
}) => {
const [servers, setServers] = useState<Server[]>([]);
const [onlinePlayers, setOnlinePlayers] = useState<OnlinePlayerFlat[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [serverFilter, setServerFilter] = useState<string>('all');
const [search, setSearch] = useState('');
const [skinMap, setSkinMap] = useState<Record<string, string>>({});
useEffect(() => {
const load = async () => {
try {
setLoading(true);
setError(null);
const activeServers = await fetchActiveServers();
setServers(activeServers);
const results = await Promise.all(
activeServers.map((s) => fetchOnlinePlayers(s.id)),
);
const flat: OnlinePlayerFlat[] = [];
results.forEach((res) => {
res.online_players.forEach((p) => {
flat.push({
username: p.username,
uuid: p.uuid,
serverId: res.server.id,
serverName: res.server.name,
onlineSince: p.online_since,
});
});
});
setOnlinePlayers(flat);
} catch (e: any) {
setError(e?.message || 'Не удалось загрузить онлайн игроков');
} finally {
setLoading(false);
}
};
load();
}, []);
// Догружаем скины по uuid
useEffect(() => {
const loadSkins = async () => {
// Берём всех видимых игроков (чтобы не грузить для тысяч, если их много)
const uuids = Array.from(new Set(onlinePlayers.map((p) => p.uuid)));
const toLoad = uuids.filter((uuid) => !skinMap[uuid]);
if (!toLoad.length) return;
try {
// Просто по очереди, чтобы не DDOS'ить API
for (const uuid of toLoad) {
try {
const player = await fetchPlayer(uuid);
if (player.skin_url) {
setSkinMap((prev) => ({
...prev,
[uuid]: player.skin_url,
}));
}
} catch (e) {
console.warn('Не удалось получить скин для', uuid, e);
}
}
} catch (e) {
console.error('Ошибка при загрузке скинов:', e);
}
};
loadSkins();
}, [onlinePlayers]);
const filteredPlayers = useMemo(() => {
return (
onlinePlayers
.filter((p) =>
serverFilter === 'all' ? true : p.serverId === serverFilter,
)
.filter((p) =>
search.trim()
? p.username.toLowerCase().includes(search.toLowerCase())
: true,
)
// свой ник наверх
.sort((a, b) => {
if (a.username === currentUsername && b.username !== currentUsername)
return -1;
if (b.username === currentUsername && a.username !== currentUsername)
return 1;
return a.username.localeCompare(b.username);
})
);
}, [onlinePlayers, serverFilter, search, currentUsername]);
if (loading) {
return <FullScreenLoader message="Загружаем игроков онлайн..." />;
}
if (error) {
return (
<Typography color="error" sx={{ mt: 2 }}>
{error}
</Typography>
);
}
if (!onlinePlayers.length) {
return (
<Typography sx={{ mt: 2, opacity: 0.8 }}>
Сейчас на серверах никого нет.
</Typography>
);
}
const totalOnline = onlinePlayers.length;
return (
<Paper
sx={{
mt: 3,
p: 2,
borderRadius: '1vw',
background: 'rgba(255,255,255,0.03)',
border: '1px solid rgba(255,255,255,0.06)',
color: 'white',
}}
elevation={0}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
gap: 2,
}}
>
<Box>
<Typography
sx={{
fontFamily: 'Benzin-Bold',
fontSize: '1.2vw',
mb: 0.5,
}}
>
Игроки онлайн
</Typography>
<Typography sx={{ fontSize: '0.9vw', opacity: 0.7 }}>
Сейчас на серверах: {totalOnline}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
<FormControl size="small" sx={{ minWidth: 160 }}>
<InputLabel sx={{ color: 'white' }}>Сервер</InputLabel>
<Select
label="Сервер"
value={serverFilter}
onChange={(e) => setServerFilter(e.target.value)}
sx={{ color: 'white' }}
>
<MenuItem value="all" sx={{ color: 'black' }}>
Все сервера
</MenuItem>
{servers.map((s) => (
<MenuItem key={s.id} value={s.id} sx={{ color: 'black' }}>
{s.name}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
size="small"
label="Поиск по нику"
value={search}
onChange={(e) => setSearch(e.target.value)}
sx={{ color: 'white' }}
/>
</Box>
</Box>
<Box
sx={{
maxHeight: '35vh',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 1,
}}
>
{filteredPlayers.map((p) => (
<Box
key={p.uuid}
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
py: 0.6,
px: 1.5,
borderRadius: '999px',
background: 'rgba(0,0,0,0.35)',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<HeadAvatar skinUrl={skinMap[p.uuid]} size={24} />
<Typography sx={{ fontFamily: 'Benzin-Bold' }}>
{p.username}
</Typography>
{p.username === currentUsername && (
<Chip
label="Вы"
size="small"
sx={{
height: '1.4rem',
fontSize: '0.7rem',
bgcolor: 'rgb(255,77,77)',
color: 'white',
}}
/>
)}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Chip
label={translateServer({ name: p.serverName })}
size="small"
sx={{ bgcolor: 'rgba(255,255,255,0.08)', color: 'white' }}
/>
{/* Можно позже красиво форматировать onlineSince */}
</Box>
</Box>
))}
</Box>
</Paper>
);
};