player/src/LoadingScreen.jsx
МИН 8f0524cbb3 feat: порт 3D-стрелки-указателя в плеер (фича-парность) + dev JWT-панель
- game.fx.pointer + расширенный game.fx.beam: BeamManager (текстуры/curved/
  градиент/quest-marker), ScriptSandboxWorker (_normFxPoint от DataCloneError),
  GameRuntime (fx.createPointer/pointerTarget/pointerUpdate/beamUpdate/
  beamVisible), BabylonScene._activatePointers. 1-в-1 со студией.
- Dev JWT-панель на экране «Нужен JWT» (только localhost): кнопка → инпут →
  localStorage.player_jwt + reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:46:24 +03:00

234 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// LoadingScreen — брендовый сплеш плеера.
// Скопирован 1-в-1 с Android LoadingView.kt:
// - синий gradient brandTop #2841C8 → brandBottom #4B6EF0
// - лого A02 130px по центру
// - "Рублокс" 56sp белый под лого
// - 8-точечный spinner с активной точкой
// - подпись (text + анимированные точки)
// - декоративные пузыри с "дышащим" alpha
//
// CSS-анимации, без JS-фрейма каждый кадр.
import React, { useState } from 'react';
const TITLE = 'Рублокс';
/**
* Dev-only панель вставки JWT. Показывается на экране «Нужен JWT» (только
* localhost). Кнопка → инпут → сохраняет в localStorage['player_jwt'] и
* перезагружает страницу. На проде этот экран не наступает (там redirect).
*/
function DevJwtPanel() {
const [open, setOpen] = useState(false);
const [val, setVal] = useState('');
const apply = () => {
const t = (val || '').trim();
if (!t) return;
try {
localStorage.setItem('player_jwt', t);
// совместимость с другими местами чтения токена
localStorage.setItem('Authorization', t);
} catch (e) { /* ignore */ }
window.location.reload();
};
if (!open) {
return (
<button className="rb-devbtn" onClick={() => setOpen(true)} title="Вставить JWT (dev)">
🔑 Вставить JWT
</button>
);
}
return (
<div className="rb-devpanel">
<textarea
className="rb-devinput"
placeholder="Вставь сюда player_jwt…"
value={val}
onChange={(e) => setVal(e.target.value)}
autoFocus
spellCheck={false}
onKeyDown={(e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) apply(); }}
/>
<div className="rb-devrow">
<button className="rb-devapply" onClick={apply} disabled={!val.trim()}>
Войти
</button>
<button className="rb-devcancel" onClick={() => setOpen(false)}>
Отмена
</button>
</div>
<div className="rb-devhint">Ctrl+Enter войти. Токен сохранится в localStorage.</div>
</div>
);
}
export default function LoadingScreen({ text = 'Подключение', subText = null, devJwt = false }) {
return (
<div className="rb-splash">
<style>{splashCss}</style>
{/* Декоративные пузыри — те же позиции что в Android LoadingView */}
{bubbles.map((b, i) => (
<span
key={i}
className="rb-bubble"
style={{
left: `${b.x * 100}%`,
top: `${b.y * 100}%`,
width: b.size,
height: b.size,
animationDelay: `${b.phase}s`,
}}
/>
))}
<img src="/A02.png" alt="" width={130} height={130} className="rb-logo" />
<div className="rb-title">{TITLE}</div>
{/* 8-точечный spinner */}
<div className="rb-spinner" aria-hidden="true">
{Array.from({ length: 8 }).map((_, i) => (
<span key={i} className="rb-dot" style={{ '--i': i }} />
))}
</div>
<div className="rb-status">
{text}<span className="rb-dots" aria-hidden="true"></span>
</div>
{subText && <div className="rb-substatus">{subText}</div>}
{/* Dev-only: вставка JWT прямо с экрана (вместо ручного localStorage). */}
{devJwt && <DevJwtPanel />}
</div>
);
}
const bubbles = [
{ x: 0.05, y: 0.93, size: 48, phase: 0.0 },
{ x: 0.12, y: 0.88, size: 28, phase: 1.2 },
{ x: 0.20, y: 0.95, size: 20, phase: 2.4 },
{ x: 0.85, y: 0.85, size: 36, phase: 0.7 },
{ x: 0.92, y: 0.92, size: 44, phase: 1.9 },
{ x: 0.50, y: 0.97, size: 16, phase: 3.1 },
];
const splashCss = `
.rb-splash {
position: fixed; inset: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: linear-gradient(160deg, #2841C8 0%, #4B6EF0 100%);
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
overflow: hidden;
}
.rb-logo {
filter: drop-shadow(0 6px 14px rgba(0,0,0,0.35));
margin-bottom: 28px;
}
.rb-title {
font-size: 56px;
font-weight: 700;
letter-spacing: 1px;
margin-bottom: 40px;
text-shadow: 0 2px 8px rgba(0,0,0,0.25);
}
.rb-spinner {
position: relative;
width: 64px; height: 64px;
margin-bottom: 16px;
}
.rb-dot {
position: absolute;
top: 50%; left: 50%;
width: 6px; height: 6px; border-radius: 50%;
background: rgba(255,255,255,0.45);
transform:
translate(-50%, -50%)
rotate(calc(var(--i) * 45deg))
translateY(-22px);
animation: rbDotPulse 1s linear infinite;
animation-delay: calc(var(--i) * 0.125s);
}
@keyframes rbDotPulse {
0%, 70%, 100% { background: rgba(255,255,255,0.35); }
10% { background: rgba(255,255,255,1.0); }
}
.rb-status {
font-size: 16px; opacity: 0.85;
letter-spacing: 0.3px;
}
.rb-dots {
display: inline-block; width: 24px; text-align: left;
animation: rbDotsBlink 1.4s steps(4, end) infinite;
}
@keyframes rbDotsBlink {
0% { clip-path: inset(0 100% 0 0); }
100% { clip-path: inset(0 0 0 0); }
}
.rb-substatus {
margin-top: 14px;
font-size: 13px; opacity: 0.65;
max-width: 360px; padding: 0 20px;
text-align: center; line-height: 1.45;
}
.rb-bubble {
position: absolute;
border-radius: 50%;
background: rgba(255,255,255,0.16);
animation: rbBubble 3.5s ease-in-out infinite;
transform: translate(-50%, -50%);
pointer-events: none;
}
/* === Dev JWT panel === */
.rb-devbtn {
margin-top: 22px;
background: rgba(255,255,255,0.14);
border: 1px solid rgba(255,255,255,0.3);
color: #fff;
font-size: 13px; font-weight: 600;
padding: 8px 16px; border-radius: 10px;
cursor: pointer;
transition: background 0.15s;
}
.rb-devbtn:hover { background: rgba(255,255,255,0.24); }
.rb-devpanel {
margin-top: 20px;
width: 380px; max-width: calc(100vw - 40px);
display: flex; flex-direction: column; gap: 8px;
}
.rb-devinput {
width: 100%; height: 70px; resize: vertical;
background: rgba(0,0,0,0.28);
border: 1px solid rgba(255,255,255,0.3);
border-radius: 10px;
color: #fff; font-size: 12px;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
padding: 10px; box-sizing: border-box;
outline: none;
}
.rb-devinput:focus { border-color: rgba(255,255,255,0.6); }
.rb-devrow { display: flex; gap: 8px; }
.rb-devapply {
flex: 1;
background: #ffffff; color: #2841C8;
border: none; border-radius: 10px;
font-size: 14px; font-weight: 700;
padding: 9px 0; cursor: pointer;
}
.rb-devapply:disabled { opacity: 0.4; cursor: default; }
.rb-devcancel {
background: transparent; color: rgba(255,255,255,0.8);
border: 1px solid rgba(255,255,255,0.3); border-radius: 10px;
font-size: 13px; padding: 9px 16px; cursor: pointer;
}
.rb-devhint {
font-size: 11px; opacity: 0.6; text-align: center;
}
@keyframes rbBubble {
0%, 100% { opacity: 0.35; }
50% { opacity: 0.70; }
}
`;