2 Commits

8 changed files with 230 additions and 152 deletions

View File

@ -12,19 +12,52 @@
url('../../assets/fonts/benzin-bold.svg#benzin-bold') format('svg'); /* Chrome < 4, Legacy iOS */ url('../../assets/fonts/benzin-bold.svg#benzin-bold') format('svg'); /* Chrome < 4, Legacy iOS */
} }
:root { /* SETTINGS NO-BLUR */
--ui-scale: 1; .glass {
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
} }
#root { .glass-ui {
transform: scale(var(--ui-scale)); backdrop-filter: blur(10px);
transform-origin: top left; -webkit-backdrop-filter: blur(10px);
/* компенсация, чтобы после scale не появлялись пустые области/скроллы */
width: calc(100% / var(--ui-scale));
height: calc(100% / var(--ui-scale));
} }
.glass--soft { backdrop-filter: blur(6px); }
.glass--hard { backdrop-filter: blur(20px); }
body.no-blur .glass,
body.no-blur .glass-ui {
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
/* SETTINGS NO-BLUR */
/* SETTINGS REDUCE-MOTION */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}
body.reduce-motion *,
body.reduce-motion *::before,
body.reduce-motion *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
/* опционально: убрать ховер-скейлы (если ты их часто используешь) */
body.reduce-motion .no-motion-hover,
body.reduce-motion .no-motion-hover:hover {
transform: none !important;
}
/* SETTINGS REDUCE-MOTION */
body { body {
position: relative; position: relative;
color: white; color: white;

View File

@ -108,6 +108,46 @@ const App = () => {
}; };
const AppLayout = () => { const AppLayout = () => {
useEffect(() => {
const applySettings = () => {
try {
const raw = localStorage.getItem('launcher_settings');
if (!raw) return;
const settings = JSON.parse(raw);
document.body.classList.toggle(
'reduce-motion',
Boolean(settings.reduceMotion),
);
document.body.classList.toggle(
'no-blur',
settings.blurEffects === false,
);
const ui = document.getElementById('app-ui');
if (ui && typeof settings.uiScale === 'number') {
const scale = settings.uiScale / 100;
ui.style.transform = `scale(${scale})`;
ui.style.transformOrigin = 'top left';
ui.style.width = `${100 / scale}%`;
ui.style.height = `${100 / scale}%`;
}
} catch (e) {
console.error('Failed to apply UI settings', e);
}
};
// применяем при загрузке
applySettings();
// применяем после сохранения настроек
window.addEventListener('settings-updated', applySettings);
return () => window.removeEventListener('settings-updated', applySettings);
}, []);
// Просто используйте window.open без useNavigate // Просто используйте window.open без useNavigate
const handleRegister = () => { const handleRegister = () => {
window.open('https://account.ely.by/register', '_blank'); window.open('https://account.ely.by/register', '_blank');
@ -127,104 +167,129 @@ const AppLayout = () => {
}, []); }, []);
return ( return (
<Box <Box sx={{ width: '100vw', height: '100vh', position: 'relative', overflow: 'hidden' }}>
sx={{ {/* ФОН — НЕ масштабируется */}
height: '100vh', <Box sx={{ position: 'fixed', inset: 0, zIndex: 0, pointerEvents: 'none', }}>
width: '100vw',
position: 'relative',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: location.pathname === '/profile' || location.pathname.startsWith('/launch') || location.pathname === '/login' || location.pathname === '/registration'
? 'center'
: 'flex-start',
overflowX: 'hidden',
}}
>
<MinecraftBackground /> <MinecraftBackground />
<TopBar onRegister={handleRegister} username={username || ''} /> </Box>
<PageHeader />
<Notifier />
<Routes> {/* UI — масштабируется */}
<Route <Box
path="/login" id="app-scroll"
element={<Login onLoginSuccess={setUsername} />} sx={{
/> position: 'relative',
<Route path="/registration" element={<Registration />} /> zIndex: 1,
<Route height: '100%',
path="/" width: '100%',
element={ overflowY: 'hidden',
<AuthCheck> overflowX: 'hidden',
<VersionsExplorer /> }}
</AuthCheck> >
} <Box
/> id="app-ui"
<Route sx={{
path="/launch/:versionId" position: 'relative',
element={ zIndex: 1,
<AuthCheck> height: '100vh',
<LaunchPage /> width: '100vw',
</AuthCheck> display: 'flex',
} flexDirection: 'column',
/> alignItems: 'center',
<Route justifyContent:
path="/profile" path === '/profile' ||
element={ path.startsWith('/launch') ||
<AuthCheck> path === '/login' ||
<Profile /> path === '/registration'
</AuthCheck> ? 'center'
} : 'flex-start',
/> overflowX: 'hidden',
<Route }}
path="/daily" >
element={ <TopBar onRegister={handleRegister} username={username || ''} />
<AuthCheck> <PageHeader />
<DailyReward /> <Notifier />
</AuthCheck>
} <Routes>
/> <Route
<Route path="/login"
path="/dailyquests" element={<Login onLoginSuccess={setUsername} />}
element={ />
<AuthCheck> <Route path="/registration" element={<Registration />} />
<DailyQuests /> <Route
</AuthCheck> path="/"
} element={
/> <AuthCheck>
<Route <VersionsExplorer />
path="/settings" </AuthCheck>
element={ }
<AuthCheck> />
<Settings /> <Route
</AuthCheck> path="/launch/:versionId"
} element={
/> <AuthCheck>
<Route <LaunchPage />
path="/shop" </AuthCheck>
element={ }
<AuthCheck> />
<Shop /> <Route
</AuthCheck> path="/profile"
} element={
/> <AuthCheck>
<Route <Profile />
path="/marketplace" </AuthCheck>
element={ }
<AuthCheck> />
<Marketplace /> <Route
</AuthCheck> path="/daily"
} element={
/> <AuthCheck>
<Route <DailyReward />
path="/news" </AuthCheck>
element={ }
<AuthCheck> />
<News /> <Route
</AuthCheck> path="/dailyquests"
} element={
/> <AuthCheck>
</Routes> <DailyQuests />
</AuthCheck>
}
/>
<Route
path="/settings"
element={
<AuthCheck>
<Settings />
</AuthCheck>
}
/>
<Route
path="/shop"
element={
<AuthCheck>
<Shop />
</AuthCheck>
}
/>
<Route
path="/marketplace"
element={
<AuthCheck>
<Marketplace />
</AuthCheck>
}
/>
<Route
path="/news"
element={
<AuthCheck>
<News />
</AuthCheck>
}
/>
</Routes>
</Box>
</Box>
</Box> </Box>
); );
}; };

View File

@ -39,13 +39,12 @@ export const FullScreenLoader = ({
{fullScreen && ( {fullScreen && (
<Fade in timeout={220} appear> <Fade in timeout={220} appear>
<Box <Box
className="glass-ui"
sx={{ sx={{
position: 'absolute', position: 'absolute',
inset: 0, inset: 0,
background: background:
'radial-gradient(circle at 15% 20%, rgba(242,113,33,0.15), transparent 60%), radial-gradient(circle at 85% 10%, rgba(233,64,205,0.12), transparent 55%), rgba(5,5,10,0.75)', 'radial-gradient(circle at 15% 20%, rgba(242,113,33,0.15), transparent 60%), radial-gradient(circle at 85% 10%, rgba(233,64,205,0.12), transparent 55%), rgba(5,5,10,0.75)',
backdropFilter: 'blur(14px)',
WebkitBackdropFilter: 'blur(14px)',
}} }}
/> />
</Fade> </Fade>
@ -61,7 +60,9 @@ export const FullScreenLoader = ({
alignItems: 'center', alignItems: 'center',
gap: 3, gap: 3,
// небольшой "подъём" при появлении // небольшой "подъём" при появлении
animation: 'popIn 260ms ease-out both', animation: document.body.classList.contains('reduce-motion')
? 'none'
: 'popIn 260ms ease-out both',
'@keyframes popIn': { '@keyframes popIn': {
from: { opacity: 0, transform: 'translateY(8px) scale(0.98)' }, from: { opacity: 0, transform: 'translateY(8px) scale(0.98)' },
to: { opacity: 1, transform: 'translateY(0) scale(1)' }, to: { opacity: 1, transform: 'translateY(0) scale(1)' },
@ -76,7 +77,9 @@ export const FullScreenLoader = ({
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
background: 'conic-gradient(#F27121, #E940CD, #8A2387, #F27121)', background: 'conic-gradient(#F27121, #E940CD, #8A2387, #F27121)',
animation: 'spin 1s linear infinite', animation: document.body.classList.contains('reduce-motion')
? 'none'
: 'spin 1s linear infinite',
WebkitMask: 'radial-gradient(circle, transparent 55%, black 56%)', WebkitMask: 'radial-gradient(circle, transparent 55%, black 56%)',
mask: 'radial-gradient(circle, transparent 55%, black 56%)', mask: 'radial-gradient(circle, transparent 55%, black 56%)',
'@keyframes spin': { '@keyframes spin': {

View File

@ -251,8 +251,18 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
window.removeEventListener('skin-updated', handler as EventListener); window.removeEventListener('skin-updated', handler as EventListener);
}, [loadSkin]); }, [loadSkin]);
useEffect(() => {
const handler = () => {
requestAnimationFrame(updateGradientVars);
};
window.addEventListener('settings-updated', handler as EventListener);
return () => window.removeEventListener('settings-updated', handler as EventListener);
}, [updateGradientVars]);
return ( return (
<Box <Box
className={isAuthPage ? undefined : 'glass-ui'}
sx={[ sx={[
{ {
display: 'flex', display: 'flex',

View File

@ -153,11 +153,11 @@ export const News = () => {
sx={{ sx={{
px: '7vw', px: '7vw',
pb: '4vh', pb: '4vh',
maxHeight: '100vh',
overflowY: 'auto', overflowY: 'auto',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: '2vh', gap: '2vh',
width: '85%',
}} }}
> >
{/* Админский редактор */} {/* Админский редактор */}
@ -353,7 +353,6 @@ export const News = () => {
border: '1px solid rgba(255, 255, 255, 0)', border: '1px solid rgba(255, 255, 255, 0)',
boxShadow: '0 18px 45px rgba(0, 0, 0, 0.7)', boxShadow: '0 18px 45px rgba(0, 0, 0, 0.7)',
backdropFilter: 'blur(14px)', backdropFilter: 'blur(14px)',
width: '80vw',
// transition: // transition:
// 'transform 0.25s ease, box-shadow 0.25s.ease, border-color 0.25s ease', // 'transform 0.25s ease, box-shadow 0.25s.ease, border-color 0.25s ease',
'&:hover': { '&:hover': {

View File

@ -54,7 +54,7 @@ const defaultSettings: SettingsState = {
walkingSpeed: 0.5, walkingSpeed: 0.5,
notifications: true, notifications: true,
notificationPosition: 'top-right', notificationPosition: 'bottom-center',
}; };
function safeParseSettings(raw: string | null): SettingsState | null { function safeParseSettings(raw: string | null): SettingsState | null {
@ -229,13 +229,10 @@ const Settings = () => {
}; };
useEffect(() => { useEffect(() => {
if (typeof document === 'undefined') return; // motion / blur классы — глобально на body
const scale = settings.uiScale / 100;
document.documentElement.style.setProperty('--ui-scale', String(scale));
document.body.classList.toggle('reduce-motion', settings.reduceMotion); document.body.classList.toggle('reduce-motion', settings.reduceMotion);
document.body.classList.toggle('no-blur', !settings.blurEffects); document.body.classList.toggle('no-blur', !settings.blurEffects);
}, [settings.uiScale, settings.reduceMotion, settings.blurEffects]); }, [settings.reduceMotion, settings.blurEffects]);
const SectionTitle = ({ children }: { children: string }) => ( const SectionTitle = ({ children }: { children: string }) => (
<Typography <Typography
@ -255,6 +252,7 @@ const Settings = () => {
const Glass = ({ children }: { children: React.ReactNode }) => ( const Glass = ({ children }: { children: React.ReactNode }) => (
<Paper <Paper
className="glass glass--soft"
elevation={0} elevation={0}
sx={{ sx={{
borderRadius: '1.2vw', borderRadius: '1.2vw',
@ -263,7 +261,6 @@ const Settings = () => {
'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)', '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)',
border: '1px solid rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.08)',
boxShadow: '0 1.2vw 3.2vw rgba(0,0,0,0.55)', boxShadow: '0 1.2vw 3.2vw rgba(0,0,0,0.55)',
backdropFilter: 'blur(14px)',
color: 'white', color: 'white',
}} }}
> >
@ -287,6 +284,10 @@ const Settings = () => {
}, },
} as const; } as const;
useEffect(() => {
document.body.classList.toggle('no-blur', !settings.blurEffects);
}, [settings.blurEffects]);
return ( return (
<Box <Box
sx={{ sx={{

View File

@ -289,6 +289,7 @@ export const VersionsExplorer = () => {
flexDirection: 'column', flexDirection: 'column',
gap: '2vh', gap: '2vh',
height: '100%', height: '100%',
width: '85%',
}} }}
> >
{loading ? ( {loading ? (

View File

@ -1,34 +0,0 @@
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
export type LauncherSettings = {
notificationPosition?: 'top-right' | 'top-center' | 'top-left' | 'bottom-right' | 'bottom-center' | 'bottom-left';
};
export function getLauncherSettings(): LauncherSettings {
try {
return JSON.parse(localStorage.getItem('launcher_settings') || '{}');
} catch {
return {};
}
}
export function getNotificationPosition(): NotificationPosition {
const { notificationPosition } = getLauncherSettings();
switch (notificationPosition) {
case 'top-right':
return { vertical: 'top', horizontal: 'right' };
case 'top-center':
return { vertical: 'top', horizontal: 'center' };
case 'top-left':
return { vertical: 'top', horizontal: 'left' };
case 'bottom-right':
return { vertical: 'bottom', horizontal: 'right' };
case 'bottom-center':
return { vertical: 'bottom', horizontal: 'center' };
case 'bottom-left':
return { vertical: 'bottom', horizontal: 'left' };
default:
return { vertical: 'bottom', horizontal: 'center' };
}
}