From ca8ac8e880a1d9b3cdbf4a53a730d6ca91c68e06 Mon Sep 17 00:00:00 2001 From: aurinex Date: Sat, 13 Dec 2025 22:17:17 +0500 Subject: [PATCH] mnoga che sdelal --- src/renderer/App.tsx | 2 +- src/renderer/components/CoinsDisplay.tsx | 7 + src/renderer/components/HeadAvatar.tsx | 45 ++-- src/renderer/components/Login/AuthForm.tsx | 29 ++ src/renderer/components/PageHeader.tsx | 51 ++-- src/renderer/components/TopBar.tsx | 292 +++++++++++++++------ src/renderer/pages/Marketplace.tsx | 14 +- src/renderer/pages/Profile.tsx | 40 --- src/renderer/utils/serverTranslator.ts | 12 +- 9 files changed, 302 insertions(+), 190 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index c2c041f..efd009c 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -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', diff --git a/src/renderer/components/CoinsDisplay.tsx b/src/renderer/components/CoinsDisplay.tsx index 1840d6a..16866ab 100644 --- a/src/renderer/components/CoinsDisplay.tsx +++ b/src/renderer/components/CoinsDisplay.tsx @@ -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; } 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(externalValue || 0); const [isLoading, setIsLoading] = useState(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} diff --git a/src/renderer/components/HeadAvatar.tsx b/src/renderer/components/HeadAvatar.tsx index 0395ff4..0fb4159 100644 --- a/src/renderer/components/HeadAvatar.tsx +++ b/src/renderer/components/HeadAvatar.tsx @@ -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 = ({ skinUrl, size = 24, + style, + ...canvasProps }) => { const canvasRef = useRef(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 = ({ 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 = ({ return ( ); diff --git a/src/renderer/components/Login/AuthForm.tsx b/src/renderer/components/Login/AuthForm.tsx index 8d2316f..9482baa 100644 --- a/src/renderer/components/Login/AuthForm.tsx +++ b/src/renderer/components/Login/AuthForm.tsx @@ -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 ( { }}> Войти + + 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, + }, + }} + > + Зарегистрироваться + + ); }; diff --git a/src/renderer/components/PageHeader.tsx b/src/renderer/components/PageHeader.tsx index c561fd2..8e7df04 100644 --- a/src/renderer/components/PageHeader.tsx +++ b/src/renderer/components/PageHeader.tsx @@ -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() { {headerConfig.subtitle} diff --git a/src/renderer/components/TopBar.tsx b/src/renderer/components/TopBar.tsx index f6f20d7..f7f940f 100644 --- a/src/renderer/components/TopBar.tsx +++ b/src/renderer/components/TopBar.tsx @@ -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(0); const [value, setValue] = useState(1); const [activePage, setActivePage] = useState('versions'); const tabsWrapperRef = useRef(null); + const [skinUrl, setSkinUrl] = useState(''); + const [avatarAnchorEl, setAvatarAnchorEl] = + useState(null); + const [menuOpen, setMenuOpen] = useState(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) => { + 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 ( )} - {!isLaunchPage && !isRegistrationPage && !isLoginPage && ( + {isAuthed && !isLaunchPage && ( - { - 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', - }} - /> {!isLoginPage && !isRegistrationPage && username && ( - + onClick={handleAvatarClick} + /> + )} {/* Кнопка регистрации, если на странице логина */} {!isLoginPage && !isRegistrationPage && username && ( @@ -393,37 +401,6 @@ export default function TopBar({ onRegister, username }: TopBarProps) { showTooltip={true} /> )} - {isLoginPage && ( - - )} {/* Кнопки управления окном */} + + {/* ===== 1 строка: аватар + ник + валюта ===== */} + + + + + + {username || 'Игрок'} + + + + + + + + + { + handleAvatarMenuClose(); + navigate('/profile'); + }} + sx={{ + fontFamily: 'Benzin-Bold', + fontSize: '1.5vw', + gap: '0.5vw', + py: '0.7vw', + '&:hover': { + bgcolor: 'rgba(255,77,77,0.15)', + }, + }} + > + Профиль + + + {/* ===== 2 строка: ежедневные задания ===== */} + { + handleAvatarMenuClose(); + navigate('/dailyquests'); + }} + sx={{ + fontFamily: 'Benzin-Bold', + fontSize: '1.5vw', + gap: '0.5vw', + py: '0.7vw', + '&:hover': { + bgcolor: 'rgba(255,77,77,0.15)', + }, + }} + > + Ежедневные задания + + + {/* ===== 3 строка: ежедневная награда ===== */} + { + handleAvatarMenuClose(); + navigate('/daily'); + }} + sx={{ + fontFamily: 'Benzin-Bold', + fontSize: '1.5vw', + gap: '0.5vw', + py: '0.7vw', + '&:hover': { + bgcolor: 'rgba(255,77,77,0.15)', + }, + }} + > + Ежедневная награда + + + + {!isLoginPage && !isRegistrationPage && username && ( + + )} + + {/* ↓↓↓ дальше ты сам добавишь пункты ↓↓↓ */} + ); } diff --git a/src/renderer/pages/Marketplace.tsx b/src/renderer/pages/Marketplace.tsx index fe36804..95525f5 100644 --- a/src/renderer/pages/Marketplace.tsx +++ b/src/renderer/pages/Marketplace.tsx @@ -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; diff --git a/src/renderer/pages/Profile.tsx b/src/renderer/pages/Profile.tsx index 8f240e3..547913a 100644 --- a/src/renderer/pages/Profile.tsx +++ b/src/renderer/pages/Profile.tsx @@ -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() { - - )} diff --git a/src/renderer/utils/serverTranslator.ts b/src/renderer/utils/serverTranslator.ts index e58498a..b2b2423 100644 --- a/src/renderer/utils/serverTranslator.ts +++ b/src/renderer/utils/serverTranslator.ts @@ -1,21 +1,15 @@ // src/renderer/utils/serverTranslator.ts import { Server } from '../api'; -type ServerLike = Pick | { 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; } -}; +}