mnoga che sdelal

This commit is contained in:
aurinex
2025-12-13 22:17:17 +05:00
parent abb45c3838
commit ca8ac8e880
9 changed files with 302 additions and 190 deletions

View File

@ -134,7 +134,7 @@ const AppLayout = () => {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: location.pathname === '/profile' || location.pathname.startsWith('/launch')
justifyContent: location.pathname === '/profile' || location.pathname.startsWith('/launch') || location.pathname === '/login' || '/registration'
? 'center'
: 'flex-start',
overflowX: 'hidden',

View File

@ -3,6 +3,7 @@ import { Box, Typography } from '@mui/material';
import CustomTooltip from './Notifications/CustomTooltip';
import { useEffect, useState } from 'react';
import { fetchCoins } from '../api';
import type { SxProps, Theme } from '@mui/material/styles';
interface CoinsDisplayProps {
// Основные пропсы
@ -23,6 +24,8 @@ interface CoinsDisplayProps {
// Стилизация
backgroundColor?: string;
textColor?: string;
sx?: SxProps<Theme>;
}
export default function CoinsDisplay({
@ -44,6 +47,8 @@ export default function CoinsDisplay({
// Стилизация
backgroundColor = 'rgba(0, 0, 0, 0.2)',
textColor = 'white',
sx,
}: CoinsDisplayProps) {
const [coins, setCoins] = useState<number>(externalValue || 0);
const [isLoading, setIsLoading] = useState<boolean>(false);
@ -142,6 +147,8 @@ export default function CoinsDisplay({
cursor: showTooltip ? 'help' : 'default',
opacity: isLoading ? 0.7 : 1,
transition: 'opacity 0.2s ease',
...sx,
}}
onClick={username ? handleRefresh : undefined}
title={username ? 'Нажмите для обновления' : undefined}

View File

@ -1,28 +1,33 @@
// src/renderer/components/HeadAvatar.tsx
import { useEffect, useRef } from 'react';
import React, { useEffect, useRef } from 'react';
interface HeadAvatarProps {
skinUrl?: string;
size?: number; // финальный размер головы, px
size?: number;
style?: React.CSSProperties;
}
const DEFAULT_SKIN =
'https://static.planetminecraft.com/files/resource_media/skin/original-steve-15053860.png';
export const HeadAvatar: React.FC<HeadAvatarProps> = ({
skinUrl,
size = 24,
style,
...canvasProps
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (!skinUrl || !canvasRef.current) return;
const finalSkinUrl = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN;
const canvas = canvasRef.current;
if (!canvas) return;
const img = new Image();
img.crossOrigin = 'anonymous'; // на всякий случай, если CDN
img.src = skinUrl;
img.crossOrigin = 'anonymous';
img.src = finalSkinUrl;
img.onload = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
@ -30,26 +35,12 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
canvas.height = size;
ctx.clearRect(0, 0, size, size);
// Координаты головы в стандартном скине 64x64:
// База головы: (8, 8, 8, 8)
// Слой шляпы/маски: (40, 8, 8, 8)
// Рисуем основную голову
ctx.imageSmoothingEnabled = false;
ctx.drawImage(
img,
8, // sx
8, // sy
8, // sWidth
8, // sHeight
0, // dx
0, // dy
size, // dWidth
size, // dHeight
);
// Рисуем слой шляпы поверх (если есть)
// База головы: (8, 8, 8, 8)
ctx.drawImage(img, 8, 8, 8, 8, 0, 0, size, size);
// Слой шляпы: (40, 8, 8, 8)
ctx.drawImage(img, 40, 8, 8, 8, 0, 0, size, size);
};
@ -61,11 +52,13 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
return (
<canvas
ref={canvasRef}
{...canvasProps}
style={{
width: size,
height: size,
borderRadius: 4,
imageRendering: 'pixelated',
...style, // 👈 даём переопределять снаружи
}}
/>
);

View File

@ -4,6 +4,7 @@ import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
import GradientTextField from '../GradientTextField';
import GradientVisibilityToggleIcon from '../../assets/Icons/GradientVisibilityToggleIcon'
import { useNavigate } from 'react-router-dom'
interface AuthFormProps {
config: {
@ -16,6 +17,7 @@ interface AuthFormProps {
const AuthForm = ({ config, handleInputChange, onLogin }: AuthFormProps) => {
const [showPassword, setShowPassword] = useState(false);
const navigate = useNavigate();
return (
<Box
sx={{
@ -92,6 +94,33 @@ const AuthForm = ({ config, handleInputChange, onLogin }: AuthFormProps) => {
}}>
Войти
</Button>
<Box
sx={{
display: 'flex',
justifyContent: 'flex-end',
}}
>
<Typography
onClick={() => navigate('/registration')}
sx={{
fontFamily: 'Benzin-Bold',
fontSize: '1vw',
textTransform: 'uppercase',
letterSpacing: '0.08em',
cursor: 'pointer',
backgroundImage:
'linear-gradient(71deg, #F27121 0%, #E940CD 60%, #8A2387 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
textShadow: '0 0 15px rgba(0,0,0,0.9)',
'&:hover': {
opacity: 1,
},
}}
>
Зарегистрироваться
</Typography>
</Box>
</Box>
);
};

View File

@ -1,6 +1,6 @@
import { Box, Typography } from '@mui/material';
import { useLocation } from 'react-router-dom';
import { useMemo } from 'react';
import { useMemo, useEffect, useState } from 'react';
interface HeaderConfig {
title: string;
@ -10,6 +10,19 @@ interface HeaderConfig {
export default function PageHeader() {
const location = useLocation();
const [isAuthed, setIsAuthed] = useState(false);
const isLaunchPage = location.pathname.startsWith('/launch');
useEffect(() => {
const saved = localStorage.getItem('launcher_config');
try {
const cfg = saved ? JSON.parse(saved) : null;
setIsAuthed(Boolean(cfg?.accessToken)); // или cfg?.uuid/username
} catch {
setIsAuthed(false);
}
}, [location.pathname]);
const headerConfig: HeaderConfig | null = useMemo(() => {
const path = location.pathname;
@ -34,7 +47,7 @@ export default function PageHeader() {
if (path === '/') {
return {
title: 'Выбор версию клиента',
title: 'Выбор версии клиента',
subtitle: 'Выберите установленную версию или добавьте новую сборку',
};
}
@ -49,39 +62,31 @@ export default function PageHeader() {
if (path.startsWith('/dailyquests')) {
return {
title: 'Ежедневные задания',
subtitle: 'Выполняйте ежедневные задания разной сложности и получайте награды!',
};
}
if (path.startsWith('/profile')) {
return {
title: 'Профиль пользователя',
subtitle: 'Профиль пользователя для личных настроек',
subtitle:
'Выполняйте ежедневные задания разной сложности и получайте награды!',
};
}
if (path.startsWith('/shop')) {
return {
title: 'Внутриигровой магазин',
subtitle: 'Тратьте свою уникалькую, виртуальную валюту - Попы!',
subtitle: 'Тратьте свою уникальную виртуальную валюту Попы!',
};
}
if (path.startsWith('/marketplace')) {
return {
title: 'Маркетплейс',
subtitle: 'Покупайте или продавайте - торговая площадка между игроками',
subtitle: 'Покупайте или продавайте торговая площадка между игроками',
};
}
// Дефолт на всякий случай
return {
title: 'test',
subtitle: 'test',
};
// Дефолт
return { title: 'test', subtitle: 'test' };
}, [location.pathname]);
if (!headerConfig || headerConfig.hidden) {
// ✅ один общий guard — тут и “hidden”, и “не авторизован”, и launch
if (!headerConfig || headerConfig.hidden || !isAuthed || isLaunchPage) {
return null;
}
@ -115,11 +120,11 @@ export default function PageHeader() {
</Typography>
<Typography
variant="body2"
sx={{
mt: 0.5,
color: 'rgba(255,255,255,1)',
}}
variant="body2"
sx={{
mt: 0.5,
color: 'rgba(255,255,255,1)',
}}
>
{headerConfig.subtitle}
</Typography>

View File

@ -1,4 +1,4 @@
import { Box, Button, Tab, Tabs, Typography } from '@mui/material';
import { Box, Button, Tab, Tabs, Typography, Menu, MenuItem, Divider } from '@mui/material';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import { useLocation, useNavigate } from 'react-router-dom';
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
@ -7,6 +7,11 @@ import { Tooltip } from '@mui/material';
import { fetchCoins } from '../api';
import CustomTooltip from './Notifications/CustomTooltip';
import CoinsDisplay from './CoinsDisplay';
import { HeadAvatar } from './HeadAvatar';
import { fetchPlayer } from './../api'
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
import PersonIcon from '@mui/icons-material/Person';
declare global {
interface Window {
electron: {
@ -29,14 +34,38 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
// Получаем текущий путь
const location = useLocation();
const isLoginPage = location.pathname === '/login';
const [isAuthed, setIsAuthed] = useState(false);
const isLaunchPage = location.pathname.startsWith('/launch');
const isVersionsExplorerPage = location.pathname.startsWith('/');
const isRegistrationPage = location.pathname === '/registration';
const navigate = useNavigate();
const [coins, setCoins] = useState<number>(0);
const [value, setValue] = useState(1);
const [activePage, setActivePage] = useState('versions');
const tabsWrapperRef = useRef<HTMLDivElement | null>(null);
const [skinUrl, setSkinUrl] = useState<string>('');
const [avatarAnchorEl, setAvatarAnchorEl] =
useState<null | HTMLElement>(null);
const [menuOpen, setMenuOpen] = useState<boolean>(false);
useEffect(() => {
const saved = localStorage.getItem('launcher_config');
try {
const cfg = saved ? JSON.parse(saved) : null;
setIsAuthed(Boolean(cfg?.accessToken)); // или cfg?.uuid/username — как у тебя принято
} catch {
setIsAuthed(false);
}
}, [location.pathname]); // можно и без dependency, но так надёжнее при logout/login
const avatarMenuOpen = Boolean(avatarAnchorEl);
const handleAvatarClick = (event: React.MouseEvent<HTMLElement>) => {
setAvatarAnchorEl(event.currentTarget);
};
const handleAvatarMenuClose = () => {
setAvatarAnchorEl(null);
};
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
@ -147,6 +176,19 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
navigate('/login');
};
useEffect(() => {
const savedConfig = localStorage.getItem('launcher_config');
if (!savedConfig) return;
const config = JSON.parse(savedConfig);
const uuid = config.uuid;
if (!uuid) return;
fetchPlayer(uuid)
.then((player) => setSkinUrl(player.skin_url))
.catch((e) => console.error('Не удалось получить скин:', e));
}, []);
return (
<Box
sx={{
@ -197,7 +239,7 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
<ArrowBackRoundedIcon />
</Button>
)}
{!isLaunchPage && !isRegistrationPage && !isLoginPage && (
{isAuthed && !isLaunchPage && (
<Box
ref={tabsWrapperRef}
onWheel={handleTabsWheel}
@ -264,25 +306,6 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
transition: 'all 0.3s ease',
}}
/>
<Tab
label="Профиль"
disableRipple={true}
onClick={() => {
setActivePage('profile');
}}
sx={{
color: 'white',
fontFamily: 'Benzin-Bold',
fontSize: '0.7em',
'&.Mui-selected': {
color: 'rgba(255, 77, 77, 1)',
},
'&:hover': {
color: 'rgb(177, 52, 52)',
},
transition: 'all 0.3s ease',
}}
/>
<Tab
label="Магазин"
disableRipple={true}
@ -357,32 +380,17 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
}}
>
{!isLoginPage && !isRegistrationPage && username && (
<Button
variant="outlined"
color="primary"
onClick={() => logout()}
sx={{
width: '8em',
height: '3em',
borderRadius: '2.5vw',
fontFamily: 'Benzin-Bold',
fontSize: '0.9em',
background:
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
color: 'white',
border: 'none',
transition: 'transform 0.3s ease',
'&:hover': {
background:
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
transform: 'scale(1.01)',
boxShadow: '0 4px 15px rgba(242, 113, 33, 0.4)',
},
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
<Box sx={{ display: 'flex', alignItems: 'center', gap: '1vw' }}>
<HeadAvatar
skinUrl={skinUrl}
size={44}
style={{
borderRadius: '3vw',
cursor: 'pointer',
}}
>
Выйти
</Button>
onClick={handleAvatarClick}
/>
</Box>
)}
{/* Кнопка регистрации, если на странице логина */}
{!isLoginPage && !isRegistrationPage && username && (
@ -393,37 +401,6 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
showTooltip={true}
/>
)}
{isLoginPage && (
<Button
variant="contained"
onClick={() => navigate('/registration')}
sx={{
width: '13em',
height: '3em',
borderRadius: '2.5vw',
fontFamily: 'Benzin-Bold',
fontSize: '0.9em',
background:
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
color: 'white',
border: 'none',
transition: 'all 0.3s ease',
'&:hover': {
background:
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
transform: 'scale(1.01)',
boxShadow: '0 4px 15px rgba(242, 113, 33, 0.4)',
},
'&:active': {
color: 'transparent',
},
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
userSelect: 'none',
}}
>
<Typography sx={{ fontSize: '1em', color: 'white' }}>Регистрация</Typography>
</Button>
)}
{/* Кнопки управления окном */}
<Button
@ -465,6 +442,165 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
<CloseRoundedIcon sx={{ color: 'white' }} />
</Button>
</Box>
<Menu
anchorEl={avatarAnchorEl}
open={avatarMenuOpen}
onClose={handleAvatarMenuClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
PaperProps={{
sx: {
mt: '0.5vw',
borderRadius: '1vw',
minWidth: '16vw',
bgcolor: 'rgba(0,0,0,0.92)',
color: 'white',
boxShadow: '0 0 20px rgba(0,0,0,0.6)',
border: '1px solid rgba(255,77,77,0.35)',
},
}}
>
{/* ===== 1 строка: аватар + ник + валюта ===== */}
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'auto 1fr',
gap: '1.5vw',
alignItems: 'center',
px: '2vw',
py: '0.8vw',
}}
>
<HeadAvatar
skinUrl={skinUrl}
size={40}
style={{ borderRadius: '3vw' }}
/>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography
sx={{
fontFamily: 'Benzin-Bold',
fontSize: '2vw',
}}
>
{username || 'Игрок'}
</Typography>
<CoinsDisplay
username={username}
size="medium"
autoUpdate={true}
showTooltip={true}
sx={{
border: 'none',
padding: '0vw',
}}
backgroundColor={'rgba(0, 0, 0, 0)'}
/>
</Box>
</Box>
<Divider sx={{ my: '0.4vw', borderColor: 'rgba(255,255,255,0.08)' }} />
<MenuItem
onClick={() => {
handleAvatarMenuClose();
navigate('/profile');
}}
sx={{
fontFamily: 'Benzin-Bold',
fontSize: '1.5vw',
gap: '0.5vw',
py: '0.7vw',
'&:hover': {
bgcolor: 'rgba(255,77,77,0.15)',
},
}}
>
<PersonIcon sx={{ fontSize: '2vw' }}/> Профиль
</MenuItem>
{/* ===== 2 строка: ежедневные задания ===== */}
<MenuItem
onClick={() => {
handleAvatarMenuClose();
navigate('/dailyquests');
}}
sx={{
fontFamily: 'Benzin-Bold',
fontSize: '1.5vw',
gap: '0.5vw',
py: '0.7vw',
'&:hover': {
bgcolor: 'rgba(255,77,77,0.15)',
},
}}
>
<CalendarMonthIcon sx={{ fontSize: '2vw' }} /> Ежедневные задания
</MenuItem>
{/* ===== 3 строка: ежедневная награда ===== */}
<MenuItem
onClick={() => {
handleAvatarMenuClose();
navigate('/daily');
}}
sx={{
fontFamily: 'Benzin-Bold',
fontSize: '1.5vw',
gap: '0.5vw',
py: '0.7vw',
'&:hover': {
bgcolor: 'rgba(255,77,77,0.15)',
},
}}
>
<EmojiEventsIcon sx={{ fontSize: '2vw' }} /> Ежедневная награда
</MenuItem>
<Divider sx={{ my: '0.4vw', borderColor: 'rgba(255,255,255,0.08)' }} />
{!isLoginPage && !isRegistrationPage && username && (
<Button
variant="outlined"
color="primary"
onClick={() => {
logout();
setMenuOpen(false);
}}
sx={{
width: '8vw',
height: '3vw',
borderRadius: '2.5vw',
fontFamily: 'Benzin-Bold',
fontSize: '1.2vw',
background:
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
color: 'white',
border: 'none',
transition: 'transform 0.3s ease',
'&:hover': {
background:
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
transform: 'scale(1.01)',
boxShadow: '0 4px 15px rgba(242, 113, 33, 0.4)',
},
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
m: '0 0 0 18vw'
}}
>
Выйти
</Button>
)}
{/* ↓↓↓ дальше ты сам добавишь пункты ↓↓↓ */}
</Menu>
</Box>
);
}

View File

@ -20,6 +20,7 @@ import CustomNotification from '../components/Notifications/CustomNotification';
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
import * as React from 'react';
import { playBuySound } from '../utils/sounds';
import { translateServer } from '../utils/serverTranslator'
interface TabPanelProps {
children?: React.ReactNode;
@ -66,19 +67,6 @@ export default function Marketplace() {
horizontal: 'center',
});
const translateServer = (server: Server) => {
switch (server.name) {
case 'Server minecraft.hub.popa-popa.ru':
return 'Хаб';
case 'Server survival.hub.popa-popa.ru':
return 'Выживание';
case 'Server minecraft.minigames.popa-popa.ru':
return 'Миниигры';
default:
return server.name;
}
};
// Функция для проверки онлайн-статуса игрока и определения сервера
const checkPlayerStatus = async () => {
if (!username) return;

View File

@ -62,14 +62,6 @@ export default function Profile() {
horizontal: 'right',
});
const navigateDaily = () => {
navigate('/daily');
};
const navigateDailyQuests = () => {
navigate('/dailyquests');
};
useEffect(() => {
const savedConfig = localStorage.getItem('launcher_config');
if (savedConfig) {
@ -501,38 +493,6 @@ export default function Profile() {
</Box>
</Box>
<OnlinePlayersPanel currentUsername={username} />
<Button
variant="contained"
fullWidth
onClick={navigateDaily}
sx={{
mt: 1,
transition: 'transform 0.3s ease',
background:
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
fontFamily: 'Benzin-Bold',
borderRadius: '2.5vw',
'&:hover': { transform: 'scale(1.03)' },
}}
>
Ежедневные награды
</Button>
<Button
variant="contained"
fullWidth
onClick={navigateDailyQuests}
sx={{
mt: 1,
transition: 'transform 0.3s ease',
background:
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
fontFamily: 'Benzin-Bold',
borderRadius: '2.5vw',
'&:hover': { transform: 'scale(1.03)' },
}}
>
Ежедневные квесты
</Button>
</Box>
</>
)}

View File

@ -1,21 +1,15 @@
// src/renderer/utils/serverTranslator.ts
import { Server } from '../api';
type ServerLike = Pick<Server, 'name'> | { name: string };
export const translateServer = (
server: ServerLike | null | undefined,
): string => {
if (!server?.name) return '';
export function translateServer(server: Server): string {
switch (server.name) {
case 'Server minecraft.hub.popa-popa.ru':
return 'Хаб';
case 'Server survival.hub.popa-popa.ru':
case 'Server minecraft.survival.popa-popa.ru':
return 'Выживание';
case 'Server minecraft.minigames.popa-popa.ru':
return 'Миниигры';
default:
return server.name;
}
};
}