incercare de NUI-Offline Viewer ca sa nu mai intru in joc pt a vedea NUI-uri.
This commit is contained in:
2026-03-30 01:40:16 +03:00
parent 55e2a09e20
commit 319050b353
623 changed files with 68280 additions and 0 deletions

View File

@@ -0,0 +1,617 @@
<!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>