Files
Kotzu 319050b353 NUI-SIM
incercare de NUI-Offline Viewer ca sa nu mai intru in joc pt a vedea NUI-uri.
2026-03-30 01:40:16 +03:00

618 lines
23 KiB
HTML
Raw Permalink 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.
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Red Valley — NUI Simulator</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-primary: #0a0b0d;
--bg-secondary: #111318;
--bg-tertiary: #181a1f;
--bg-card: #1a1c22;
--accent: #ff1a35;
--accent-glow: rgba(255, 26, 53, 0.15);
--text-primary: #ffffff;
--text-secondary: #8b8d94;
--text-muted: #555760;
--border: #222430;
--border-hover: #333540;
--success: #22c55e;
--warning: #f59e0b;
}
body {
font-family: 'Inter', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
height: 100vh;
overflow: hidden;
}
.app { display: flex; height: 100vh; }
/* ====== SIDEBAR ====== */
.sidebar {
width: 300px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar-header {
padding: 16px 16px 12px;
border-bottom: 1px solid var(--border);
}
.sidebar-header h1 {
font-size: 15px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.sidebar-header h1 .badge {
font-size: 10px; font-weight: 600;
background: var(--accent); padding: 2px 7px;
border-radius: 4px; letter-spacing: 0.5px;
}
.sidebar-header .subtitle {
font-size: 11px; color: var(--text-muted); margin-top: 3px;
display: flex; align-items: center; gap: 6px;
}
.sidebar-header .count {
background: var(--bg-tertiary); padding: 1px 6px;
border-radius: 3px; font-weight: 600; color: var(--accent);
}
.search-box {
padding: 10px 16px;
border-bottom: 1px solid var(--border);
}
.search-box input {
width: 100%; height: 34px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-size: 12px; font-family: 'Inter', sans-serif;
padding: 0 12px 0 34px;
outline: none; transition: border-color 0.2s;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23555760' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 10px center;
}
.search-box input:focus { border-color: var(--accent); }
.search-box input::placeholder { color: var(--text-muted); }
.filter-bar {
display: flex; gap: 4px;
padding: 8px 16px;
border-bottom: 1px solid var(--border);
overflow-x: auto;
}
.filter-pill {
font-size: 10px; font-weight: 500;
padding: 4px 10px; border-radius: 4px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
color: var(--text-muted);
cursor: pointer; transition: all 0.15s;
white-space: nowrap;
}
.filter-pill:hover { color: var(--text-primary); border-color: var(--border-hover); }
.filter-pill.active { background: var(--accent-glow); border-color: var(--accent); color: var(--accent); }
.nui-list {
flex: 1; overflow-y: auto; padding: 6px;
}
.nui-list::-webkit-scrollbar { width: 4px; }
.nui-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.nui-item {
display: flex; align-items: center; gap: 8px;
padding: 8px 10px; border-radius: 6px;
cursor: pointer; transition: all 0.12s;
margin-bottom: 1px; border: 1px solid transparent;
}
.nui-item:hover { background: var(--bg-tertiary); }
.nui-item.active {
background: var(--accent-glow);
border-color: rgba(255, 26, 53, 0.3);
}
.nui-item .icon {
width: 30px; height: 30px; border-radius: 5px;
display: flex; align-items: center; justify-content: center;
font-size: 13px; flex-shrink: 0;
background: var(--bg-tertiary); border: 1px solid var(--border);
}
.nui-item.active .icon { background: var(--accent); border-color: var(--accent); }
.nui-item .info { flex: 1; min-width: 0; }
.nui-item .info .name {
font-size: 12px; font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.nui-item .info .path {
font-size: 10px; color: var(--text-muted);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.nui-item .tag {
font-size: 9px; font-weight: 600;
padding: 2px 5px; border-radius: 3px;
flex-shrink: 0; text-transform: uppercase;
letter-spacing: 0.3px;
}
.nui-item.missing .name { color: var(--text-muted); text-decoration: line-through; }
/* ====== MAIN ====== */
.main {
flex: 1; display: flex; flex-direction: column; min-width: 0;
}
.toolbar {
height: 44px; background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
display: flex; align-items: center;
padding: 0 12px; gap: 6px;
}
.toolbar .btn {
height: 28px; padding: 0 10px;
border: 1px solid var(--border);
border-radius: 5px;
background: var(--bg-tertiary);
color: var(--text-secondary);
font-size: 11px; font-weight: 500;
font-family: 'Inter', sans-serif;
cursor: pointer; display: flex;
align-items: center; gap: 4px;
transition: all 0.12s;
}
.toolbar .btn:hover {
background: var(--bg-card); color: var(--text-primary);
border-color: var(--border-hover);
}
.toolbar .sep {
width: 1px; height: 20px;
background: var(--border); margin: 0 4px;
}
.toolbar .spacer { flex: 1; }
.toolbar .resolution select {
height: 28px; background: var(--bg-tertiary);
border: 1px solid var(--border); border-radius: 5px;
color: var(--text-primary); font-size: 11px;
font-family: 'Inter', sans-serif;
padding: 0 6px; outline: none; cursor: pointer;
}
.toolbar .resolution label {
font-size: 10px; color: var(--text-muted); margin-right: 4px;
}
/* ====== PREVIEW ====== */
.preview-container {
flex: 1; display: flex;
align-items: center; justify-content: center;
background:
radial-gradient(circle at 20% 50%, rgba(255, 26, 53, 0.03) 0%, transparent 50%),
repeating-linear-gradient(0deg, transparent, transparent 19px, rgba(255,255,255,0.015) 19px, rgba(255,255,255,0.015) 20px),
repeating-linear-gradient(90deg, transparent, transparent 19px, rgba(255,255,255,0.015) 19px, rgba(255,255,255,0.015) 20px),
var(--bg-primary);
padding: 16px; position: relative;
}
.preview-frame {
width: 1920px; height: 1080px;
border: 1px solid var(--border);
border-radius: 6px; overflow: hidden;
background: #000; position: relative;
box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
transform-origin: center center;
}
.preview-frame iframe {
width: 100%; height: 100%; border: none;
}
.empty-state {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
height: 100%; gap: 10px;
}
.empty-state .icon-large {
width: 56px; height: 56px; border-radius: 14px;
background: var(--bg-tertiary);
display: flex; align-items: center; justify-content: center;
font-size: 24px; border: 1px solid var(--border);
}
.empty-state h3 { font-size: 15px; font-weight: 600; }
.empty-state p { font-size: 12px; color: var(--text-muted); text-align: center; max-width: 280px; }
/* ====== STATUS BAR ====== */
.status-bar {
height: 26px; background: var(--bg-secondary);
border-top: 1px solid var(--border);
display: flex; align-items: center;
padding: 0 12px; font-size: 10px;
color: var(--text-muted); gap: 14px;
}
.status-bar .dot {
width: 6px; height: 6px; border-radius: 50%; background: var(--success);
}
.status-bar .item { display: flex; align-items: center; gap: 4px; }
/* ====== MESSAGE PANEL ====== */
.msg-panel {
width: 260px; background: var(--bg-secondary);
border-left: 1px solid var(--border);
display: flex; flex-direction: column; flex-shrink: 0;
}
.msg-panel-header {
padding: 12px 14px 10px;
border-bottom: 1px solid var(--border);
}
.msg-panel-header h3 {
font-size: 12px; font-weight: 700;
display: flex; align-items: center; gap: 5px;
}
.msg-panel-header p { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
.msg-section {
padding: 10px 14px; border-bottom: 1px solid var(--border);
}
.msg-section h4 {
font-size: 10px; font-weight: 600; color: var(--text-muted);
text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 8px;
}
.msg-preset {
width: 100%; padding: 6px 10px; margin-bottom: 4px;
border: 1px solid var(--border); border-radius: 5px;
background: var(--bg-tertiary); color: var(--text-primary);
font-size: 11px; font-weight: 500;
font-family: 'Inter', sans-serif;
cursor: pointer; transition: all 0.12s;
text-align: left;
}
.msg-preset:hover {
background: var(--bg-card); border-color: var(--border-hover);
}
.msg-custom-area {
width: 100%; min-height: 60px; max-height: 100px;
background: var(--bg-tertiary); border: 1px solid var(--border);
border-radius: 5px; color: var(--text-primary);
font-size: 11px; font-family: 'JetBrains Mono', 'Consolas', monospace;
padding: 8px; outline: none; resize: vertical;
}
.msg-custom-area:focus { border-color: var(--accent); }
.msg-send-btn {
width: 100%; height: 28px; margin-top: 6px;
background: var(--accent); border: none;
border-radius: 5px; color: white;
font-size: 11px; font-weight: 600;
font-family: 'Inter', sans-serif;
cursor: pointer; transition: opacity 0.15s;
}
.msg-send-btn:hover { opacity: 0.85; }
.msg-log {
flex: 1; overflow-y: auto; padding: 10px 14px;
}
.msg-log::-webkit-scrollbar { width: 3px; }
.msg-log::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.log-entry {
font-size: 10px; padding: 3px 0;
border-bottom: 1px solid rgba(255,255,255,0.02);
line-height: 1.5; word-break: break-all;
}
.log-entry .time { color: var(--text-muted); }
.log-entry.log-out { color: #60a5fa; }
.log-entry.log-in { color: var(--success); }
.log-entry.log-err { color: var(--accent); }
</style>
</head>
<body>
<div class="app">
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">
<h1>🎮 NUI Simulator <span class="badge">RV</span></h1>
<div class="subtitle">
<span id="nuiCount" class="count">0</span> NUI resources discovered
</div>
</div>
<div class="search-box">
<input type="text" id="searchInput" placeholder="Search resources...">
</div>
<div class="filter-bar" id="filterBar"></div>
<div class="nui-list" id="nuiList"></div>
</div>
<!-- Main -->
<div class="main">
<div class="toolbar">
<button class="btn" id="btnReload" title="Reload NUI">↻ Reload</button>
<button class="btn" id="btnRescan" title="Rescan resources">🔄 Rescan</button>
<div class="sep"></div>
<button class="btn" id="btnShow">📤 Show</button>
<button class="btn" id="btnHide">📥 Hide</button>
<button class="btn" id="btnNewTab">🔗 New Tab</button>
<div class="spacer"></div>
<div class="resolution">
<label>Resolution:</label>
<select id="resolutionSelect">
<option value="1920x1080" selected>1920×1080</option>
<option value="2560x1440">2560×1440</option>
<option value="1280x720">1280×720</option>
<option value="1600x900">1600×900</option>
</select>
</div>
</div>
<div class="preview-container">
<div class="preview-frame" id="previewFrame">
<div class="empty-state" id="emptyState">
<div class="icon-large">🖥️</div>
<h3>No NUI Selected</h3>
<p>Select a resource from the sidebar to preview its NUI. The server auto-discovers all ui_page resources.</p>
</div>
</div>
</div>
<div class="status-bar">
<div class="item"><span class="dot"></span> Connected</div>
<div class="item" id="statusResource">No resource loaded</div>
<div class="item" id="statusResolution">1920×1080</div>
</div>
</div>
<!-- Message Panel -->
<div class="msg-panel">
<div class="msg-panel-header">
<h3>📨 NUI Messages</h3>
<p>Send postMessage to the iframe</p>
</div>
<div class="msg-section">
<h4>Quick Actions</h4>
<button class="msg-preset" onclick="sendPreset({action:'show'})">{ action: "show" }</button>
<button class="msg-preset" onclick="sendPreset({action:'hide'})">{ action: "hide" }</button>
<button class="msg-preset" onclick="sendPreset({type:'open'})">{ type: "open" }</button>
<button class="msg-preset" onclick="sendPreset({type:'close'})">{ type: "close" }</button>
<button class="msg-preset" onclick="sendPreset({show:true})">{ show: true }</button>
<button class="msg-preset" onclick="sendPreset({show:false})">{ show: false }</button>
</div>
<div class="msg-section">
<h4>Custom Message (JSON)</h4>
<textarea class="msg-custom-area" id="customMsg" placeholder='{"action": "show", "data": {...}}'></textarea>
<button class="msg-send-btn" onclick="sendCustom()">Send Message</button>
</div>
<div class="msg-log" id="msgLog">
<h4 style="font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:6px;">Message Log</h4>
</div>
</div>
</div>
<script>
let allNUIs = [];
let activeResource = null;
let activeFilter = 'all';
// ====== FETCH NUIs FROM SERVER ======
async function fetchNUIs() {
try {
const res = await fetch('/api/nuis');
allNUIs = await res.json();
document.getElementById('nuiCount').textContent = allNUIs.length;
buildFilters();
renderList();
} catch (e) {
log('err', 'Failed to fetch NUI list');
}
}
// ====== FILTERS ======
function buildFilters() {
const tags = [...new Set(allNUIs.map(n => n.tag))];
const bar = document.getElementById('filterBar');
bar.innerHTML = `<span class="filter-pill active" data-f="all" onclick="setFilter('all')">All</span>` +
tags.map(t => `<span class="filter-pill" data-f="${t}" onclick="setFilter('${t}')">${t}</span>`).join('');
}
function setFilter(f) {
activeFilter = f;
document.querySelectorAll('.filter-pill').forEach(p =>
p.classList.toggle('active', p.dataset.f === f)
);
renderList();
}
// ====== RENDER LIST ======
function renderList() {
const search = document.getElementById('searchInput').value.toLowerCase();
const list = document.getElementById('nuiList');
const filtered = allNUIs.filter(n => {
const matchSearch = n.name.toLowerCase().includes(search) || n.path.toLowerCase().includes(search);
const matchFilter = activeFilter === 'all' || n.tag === activeFilter;
return matchSearch && matchFilter;
});
list.innerHTML = filtered.map(n => `
<div class="nui-item ${activeResource === n.name ? 'active' : ''} ${!n.exists ? 'missing' : ''}"
data-name="${n.name}" data-path="${n.uiFullPath}" onclick="loadNUI('${n.name}', '${n.uiFullPath}')">
<div class="icon">${n.icon}</div>
<div class="info">
<div class="name">${n.name}</div>
<div class="path">${n.path}</div>
</div>
<span class="tag" style="background:${n.color}20;color:${n.color}">${n.tag}</span>
</div>
`).join('');
}
// ====== LOAD NUI ======
function loadNUI(name, uiPath) {
activeResource = name;
const frame = document.getElementById('previewFrame');
const empty = document.getElementById('emptyState');
if (empty) empty.remove();
const old = frame.querySelector('iframe');
if (old) old.remove();
const iframe = document.createElement('iframe');
iframe.src = '/resources/' + uiPath;
iframe.id = 'nuiIframe';
frame.appendChild(iframe);
document.getElementById('statusResource').textContent = `📦 ${name}`;
renderList();
log('out', `Loaded: ${name}`);
}
// ====== MESSAGING ======
function sendMsg(data) {
const iframe = document.getElementById('nuiIframe');
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(data, '*');
log('out', `${JSON.stringify(data)}`);
} else {
log('err', 'No iframe loaded');
}
}
function sendPreset(data) { sendMsg(data); }
function sendCustom() {
const raw = document.getElementById('customMsg').value.trim();
try {
const data = JSON.parse(raw);
sendMsg(data);
} catch (e) {
log('err', `Invalid JSON: ${e.message}`);
}
}
// ====== LOG ======
function log(type, msg) {
const el = document.getElementById('msgLog');
const t = new Date().toLocaleTimeString('ro-RO', { hour:'2-digit', minute:'2-digit', second:'2-digit' });
const entry = document.createElement('div');
entry.className = `log-entry log-${type}`;
entry.innerHTML = `<span class="time">[${t}]</span> ${msg}`;
el.appendChild(entry);
el.scrollTop = el.scrollHeight;
}
// Listen for messages FROM iframe
window.addEventListener('message', e => {
if (e.data && typeof e.data === 'object') {
log('in', `${JSON.stringify(e.data)}`);
}
});
// ====== RESOLUTION ======
function updateResolution() {
const [w, h] = document.getElementById('resolutionSelect').value.split('x').map(Number);
const frame = document.getElementById('previewFrame');
const container = document.querySelector('.preview-container');
frame.style.width = w + 'px';
frame.style.height = h + 'px';
const maxW = container.clientWidth - 32;
const maxH = container.clientHeight - 32;
const scale = Math.min(maxW / w, maxH / h, 1);
frame.style.transform = `scale(${scale})`;
document.getElementById('statusResolution').textContent = `${w}×${h} (${Math.round(scale * 100)}%)`;
}
// ====== TOOLBAR EVENTS ======
document.getElementById('btnReload').onclick = () => {
const iframe = document.getElementById('nuiIframe');
if (iframe) { iframe.src = iframe.src; log('out', 'Reloaded'); }
};
document.getElementById('btnRescan').onclick = async () => {
log('out', 'Rescanning resources...');
const res = await fetch('/api/rescan');
allNUIs = await res.json();
document.getElementById('nuiCount').textContent = allNUIs.length;
buildFilters();
renderList();
log('out', `Found ${allNUIs.length} NUIs`);
};
document.getElementById('btnShow').onclick = () => {
sendMsg({ action: 'show' });
sendMsg({ type: 'open' });
sendMsg({ show: true });
};
document.getElementById('btnHide').onclick = () => {
sendMsg({ action: 'hide' });
sendMsg({ type: 'close' });
sendMsg({ show: false });
};
document.getElementById('btnNewTab').onclick = () => {
if (activeResource) {
const n = allNUIs.find(x => x.name === activeResource);
if (n) window.open('/resources/' + n.uiFullPath, '_blank');
}
};
document.getElementById('searchInput').addEventListener('input', () => renderList());
document.getElementById('resolutionSelect').addEventListener('change', updateResolution);
window.addEventListener('resize', updateResolution);
// ====== INIT ======
fetchNUIs();
setTimeout(updateResolution, 100);
</script>
</body>
</html>