/** * RbxlImportModal — модалка импорта .rbxl Roblox-карт в Rublox. * * Доступна ТОЛЬКО МИНу (user_id === 1) — это тест-фича. * * Поток: * 1. Юзер дропает или выбирает .rbxl файл. * 2. Кликает «Анализировать» → POST /import/rbxl/analyze. * 3. Видит отчёт: число объектов, скриптов, ассетов, предупреждения. * 4. Вводит название игры → кликает «Создать игру». * 5. POST /import/rbxl/create → редирект на /edit/. */ import React, { useState, useRef } from 'react'; import { analyzeRbxl, createRbxlProject } from '../api/rbxlImporterApi.js'; const ALLOWED_USER_ID = 1; // МИН const MAX_SIZE = 50 * 1024 * 1024; // 50 MB export default function RbxlImportModal({ open, onClose, currentUserId, onCreated }) { const [file, setFile] = useState(null); const [dragOver, setDragOver] = useState(false); const [analyzing, setAnalyzing] = useState(false); const [creating, setCreating] = useState(false); const [report, setReport] = useState(null); const [previewHash, setPreviewHash] = useState(null); const [title, setTitle] = useState(''); const [error, setError] = useState(null); // Режим скриптов: 'disabled' (импортнуть выключенными — для чтения), // 'enabled' (попытаться запустить — может вешать карту), 'skip' (удалить). const [scriptsMode, setScriptsMode] = useState('disabled'); const fileInputRef = useRef(null); if (!open) return null; if (currentUserId !== ALLOWED_USER_ID) { return (
e.stopPropagation()}>

Импорт из Roblox

Эта тест-функция доступна только администратору.

); } const reset = () => { setFile(null); setReport(null); setPreviewHash(null); setTitle(''); setError(null); setAnalyzing(false); setCreating(false); setScriptsMode('disabled'); }; const handleClose = () => { reset(); onClose?.(); }; const handleFile = (f) => { if (!f) return; if (!f.name.toLowerCase().endsWith('.rbxl')) { setError('Нужен файл .rbxl (Roblox Binary Level)'); return; } if (f.size > MAX_SIZE) { setError(`Файл больше ${MAX_SIZE / 1024 / 1024} MB`); return; } setFile(f); setError(null); setReport(null); setPreviewHash(null); }; const handleAnalyze = async () => { if (!file) return; setAnalyzing(true); setError(null); try { const result = await analyzeRbxl(file); setPreviewHash(result.preview_hash); setReport(result.report); // дефолтный title — имя файла без .rbxl const defTitle = (file.name || '').replace(/\.rbxl$/i, ''); setTitle(defTitle); } catch (e) { setError(e.message || String(e)); } finally { setAnalyzing(false); } }; const handleCreate = async () => { if (!previewHash) return; setCreating(true); setError(null); try { const result = await createRbxlProject(previewHash, title, { scriptsMode }); onCreated?.(result); handleClose(); // редирект на редактор if (result.redirect) window.location.href = result.redirect; } catch (e) { setError(e.message || String(e)); } finally { setCreating(false); } }; return (
e.stopPropagation()}>

Импорт игры из Roblox

Загрузи Roblox-карту в формате .rbxl. Геометрия и Lua-скрипты будут сконвертированы в проект Rublox.

{!file && (
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={(e) => { e.preventDefault(); setDragOver(false); handleFile(e.dataTransfer.files?.[0]); }} onClick={() => fileInputRef.current?.click()} style={{ ...dropZoneStyle, borderColor: dragOver ? '#4a9eff' : '#444', background: dragOver ? '#1a2a3a' : '#1a1a1a', }} >
📦
Перетащи .rbxl сюда
или кликни чтобы выбрать файл (макс {MAX_SIZE / 1024 / 1024} MB)
handleFile(e.target.files?.[0])} />
)} {file && !report && (
{file.name} ({(file.size / 1024).toFixed(1)} KB)
)} {report && (

Отчёт

Файл:{report.filename}
Размер:{(report.size_bytes / 1024).toFixed(1)} KB
Объектов:{report.instance_count}
Классов:{report.class_count}
Создано Part'ов:{report.primitives_created}
GLB-моделей:{report.glb_models_created}
Lua-скриптов:{report.scripts_total}
Ассетов для скачки:{report.assets_to_download}
{report.top_classes?.length > 0 && (
Что внутри (топ-25 классов) {report.top_classes.slice(0, 25).map((c, i) => ( ))}
{c.class}{c.count}
)} {report.warnings?.length > 0 && (
Предупреждения ({report.warnings.length})
    {report.warnings.slice(0, 30).map((w, i) =>
  • {w}
  • )}
)}
⚠️ Загружая, ты подтверждаешь право использовать содержимое этой карты. Не загружай чужие карты без разрешения автора.
{report.scripts_total > 0 && (
Что делать со скриптами ({report.scripts_total} шт.)?
)}
setTitle(e.target.value)} style={inputStyle} placeholder="Например: Моя обби-карта" />
)} {error && (
{error}
)}
); } const overlayStyle = { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', zIndex: 9000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20, }; const dialogStyle = { background: '#1f1f1f', color: '#eee', borderRadius: 10, padding: 20, maxWidth: 640, width: '100%', maxHeight: '90vh', overflowY: 'auto', boxShadow: '0 10px 40px rgba(0,0,0,0.5)', }; const dropZoneStyle = { border: '2px dashed #444', borderRadius: 8, padding: 40, textAlign: 'center', cursor: 'pointer', transition: 'all 0.2s', }; const panelStyle = { background: '#262626', borderRadius: 8, padding: 16, marginTop: 12, }; const btnStyle = { background: '#4a8', color: '#fff', border: 'none', padding: '10px 20px', borderRadius: 6, fontSize: 14, cursor: 'pointer', }; const closeBtnStyle = { background: 'transparent', border: 'none', color: '#888', fontSize: 20, cursor: 'pointer', padding: 4, }; const tableStyle = { width: '100%', fontSize: 13, marginTop: 8, }; const inputStyle = { width: '100%', padding: 8, background: '#1a1a1a', color: '#eee', border: '1px solid #444', borderRadius: 4, fontSize: 14, };