1
0
mirror of https://github.com/citizenfx/cfx-server-data.git synced 2025-01-11 08:25:08 +08:00
cfx-server-data/resources/[system]/runcode/web/index.html
2019-12-10 09:54:29 +01:00

486 lines
12 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>fivem runcode</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulmaswatch/0.7.2/cyborg/bulmaswatch.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
<style type="text/css">
body {
font-family: "Segoe UI", sans-serif;
}
.navbar {
z-index: inherit;
}
html.in-nui {
overflow: hidden;
background: transparent;
margin-top: 5vh;
margin-left: 7.5vw;
margin-right: 7.5vw;
margin-bottom: 5vh;
height: calc(100% - 10vh);
position: relative;
}
html.in-nui body > div.bg {
background-color: rgba(0, 0, 0, 1);
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
z-index: -999;
box-shadow: 0 22px 70px 4px rgba(0, 0, 0, 0.56);
}
span.nui-edition {
display: none;
}
html.in-nui span.nui-edition {
display: inline;
}
#close {
display: none;
}
html.in-nui #close {
display: block;
}
#result {
margin-top: 0.5em;
}
.navbar {
border-top: none;
border-left: none;
border-right: none;
}
</style>
</head>
<body>
<div class="bg">
</div>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item" href="/runcode">
<strong>runcode</strong> <span class="nui-edition">&nbsp;in-game</span>
</a>
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarMain">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarMain" class="navbar-menu">
<div class="navbar-end">
<div class="navbar-item">
<div class="field" id="cl-field">
<div class="control has-icons-left">
<div class="select">
<select id="cl-select">
</select>
</div>
<div class="icon is-small is-left">
<i class="fas fa-user"></i>
</div>
</div>
</div>
</div>
<div class="navbar-item">
<div class="field has-addons" id="lang-toggle">
<p class="control">
<button class="button" id="lua-button">
<span class="icon is-small">
<i class="fas fa-moon"></i>
</span>
<span>Lua</span>
</button>
</p>
<p class="control">
<button class="button" id="js-button">
<span class="icon is-small">
<i class="fab fa-js"></i>
</span>
<span>JS</span>
</button>
</p>
<!-- TODO pending add-on resource that'll contain webpack'd compiler
<p class="control">
<button class="button" id="ts-button">
<span class="icon is-small">
<i class="fas fa-code"></i>
</span>
<span>TS</span>
</button>
</p>
-->
</div>
</div>
<div class="navbar-item">
<div class="field has-addons" id="cl-sv-toggle">
<p class="control">
<button class="button" id="cl-button">
<span class="icon is-small">
<i class="fas fa-user-friends"></i>
</span>
<span>Client</span>
</button>
</p>
<p class="control">
<button class="button" id="sv-button">
<span class="icon is-small">
<i class="fas fa-server"></i>
</span>
<span>Server</span>
</button>
</p>
</div>
</div>
<div class="navbar-item" id="close">
<button class="button is-danger">Close</button>
</div>
</div>
</div>
</div>
</nav>
<section class="section">
<div class="container">
<div id="code-container" style="width:100%;height:60vh;border:1px solid grey"></div><br>
<div class="field" id="passwordField">
<p class="control has-icons-left">
<input class="input" type="password" id="password" placeholder="RCon Password">
<span class="icon is-small is-left">
<i class="fas fa-lock"></i>
</span>
</p>
</div>
<button class="button is-primary" id="run">Run</button>
<div id="result">
</div>
</div>
</section>
<!--
to use a local deployment, uncomment; do note currently the server isn't optimized to serve >1MB files
<script src="monaco-editor/vs/loader.js"></script>
-->
<script src="https://unpkg.com/monaco-editor@0.18.1/min/vs/loader.js"></script>
<script>
function fetchClients() {
fetch('/runcode/clients').then(res => res.json()).then(res => {
const el = document.querySelector('#cl-select');
const clients = res.clients;
const realClients = [['All', '-1'], ...clients];
const createdClients = new Set([...el.querySelectorAll('option').entries()].map(([i, el]) => el.value));
const existentClients = new Set(realClients.map(([ name, id ]) => id));
const toRemove = [...createdClients].filter(a => !existentClients.has(a));
for (const [name, id] of realClients) {
const ex = el.querySelector(`option[value="${id}"]`);
if (!ex) {
const l = document.createElement('option');
l.setAttribute('value', id);
l.appendChild(document.createTextNode(name));
el.appendChild(l);
}
}
for (const id of toRemove) {
const l = el.querySelector(`option[value="${id}"]`);
if (l) {
el.removeChild(l);
}
}
});
}
let useClient = false;
let editServerCb = null;
[['#cl-button', true], ['#sv-button', false]].forEach(([ selector, isClient ]) => {
const eh = () => {
if (isClient) {
document.querySelector('#cl-select').disabled = false;
useClient = true;
} else {
document.querySelector('#cl-select').disabled = true;
useClient = false;
}
document.querySelectorAll('#cl-sv-toggle button').forEach(el => {
el.classList.remove('is-selected', 'is-info');
});
const tgt = document.querySelector(selector);
tgt.classList.add('is-selected', 'is-info');
if (editServerCb) {
editServerCb();
}
};
// default to not-client
if (!isClient) {
eh();
}
document.querySelector(selector).addEventListener('click', ev => {
eh();
ev.preventDefault();
});
});
let lang = 'lua';
let editLangCb = null;
let initCb = null;
function getLangCode(lang) {
switch (lang) {
case 'js':
return 'javascript';
case 'ts':
return 'typescript';
}
return lang;
}
[['#lua-button', 'lua'], ['#js-button', 'js']/*, ['#ts-button', 'ts']*/].forEach(([ selector, langOpt ]) => {
const eh = () => {
lang = langOpt;
document.querySelectorAll('#lang-toggle button').forEach(el => {
el.classList.remove('is-selected', 'is-info');
});
const tgt = document.querySelector(selector);
tgt.classList.add('is-selected', 'is-info');
if (editLangCb) {
editLangCb();
}
};
// default to not-client
if (langOpt === 'lua') {
eh();
}
document.querySelector(selector).addEventListener('click', ev => {
eh();
ev.preventDefault();
});
});
setInterval(() => fetchClients(), 1000);
const inNui = (!!window.invokeNative);
let openData = {};
if (inNui) {
document.querySelector('#passwordField').style.display = 'none';
document.querySelector('html').classList.add('in-nui');
fetch(`http://${window.parent.GetParentResourceName()}/getOpenData`, {
method: 'POST',
body: '{}'
}).then(a => a.json())
.then(a => {
openData = a;
if (!openData.options.canServer) {
document.querySelector('#cl-sv-toggle').style.display = 'none';
const trigger = document.createEvent('HTMLEvents');
trigger.initEvent('click', true, true);
document.querySelector('#cl-button').dispatchEvent(trigger);
} else if (!openData.options.canClient && !openData.options.canSelf) {
document.querySelector('#cl-sv-toggle').style.display = 'none';
document.querySelector('#cl-field').style.display = 'none';
const trigger = document.createEvent('HTMLEvents');
trigger.initEvent('click', true, true);
document.querySelector('#sv-button').dispatchEvent(trigger);
}
if (!openData.options.canClient && openData.options.canSelf) {
document.querySelector('#cl-field').style.display = 'none';
}
if (openData.options.saveData) {
const cb = () => {
if (initCb) {
initCb({
lastLang: openData.options.saveData.lastLang,
lastSnippet: openData.options.saveData.lastSnippet
});
} else {
setTimeout(cb, 50);
}
};
setTimeout(cb, 50);
}
fetch(`https://${window.parent.GetParentResourceName()}/doOk`, {
method: 'POST',
body: '{}'
});
});
document.querySelector('#close button').addEventListener('click', ev => {
fetch(`https://${window.parent.GetParentResourceName()}/doClose`, {
method: 'POST',
body: '{}'
});
ev.preventDefault();
});
}
const defFiles = ['index.d.ts'];
const defFilesServer = [...defFiles, 'natives_server.d.ts'];
const defFilesClient = [...defFiles, 'natives_universal.d.ts'];
const prefix = 'https://unpkg.com/@citizenfx/{}/';
const prefixClient = prefix.replace('{}', 'client');
const prefixServer = prefix.replace('{}', 'server');
require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.18.1/min/vs' }});
require(['vs/editor/editor.main'], function() {
const editor = monaco.editor.create(document.getElementById('code-container'), {
value: 'return 42',
language: 'lua'
});
monaco.editor.setTheme('vs-dark');
let finalizers = [];
const updateScript = (client, lang) => {
finalizers.forEach(a => a());
finalizers = [];
if (lang === 'js' || lang === 'ts') {
const defaults = (lang === 'js') ? monaco.languages.typescript.javascriptDefaults :
monaco.languages.typescript.typescriptDefaults;
defaults.setCompilerOptions({
noLib: true,
allowNonTsExtensions: true
});
for (const file of (client ? defFilesClient : defFilesServer)) {
const prefix = (client ? prefixClient : prefixServer);
fetch(`${prefix}${file}`)
.then(a => a.text())
.then(a => {
const l = defaults.addExtraLib(a, file);
finalizers.push(() => l.dispose());
});
}
}
}
editLangCb = () => {
monaco.editor.setModelLanguage(editor.getModel(), getLangCode(lang));
updateScript(useClient, lang);
};
editServerCb = () => {
updateScript(useClient, lang);
};
initCb = (data) => {
if (data.lastLang) {
const trigger = document.createEvent('HTMLEvents');
trigger.initEvent('click', true, true);
document.querySelector(`#${data.lastLang}-button`).dispatchEvent(trigger);
}
if (data.lastSnippet) {
editor.getModel().setValue(data.lastSnippet);
}
};
document.querySelector('#run').addEventListener('click', e => {
const text = editor.getValue();
fetch((!inNui) ? '/runcode/' : `https://${openData.res}/runCodeInBand`, {
method: 'post',
body: JSON.stringify({
password: document.querySelector('#password').value,
client: (useClient) ? document.querySelector('#cl-select').value : '',
code: text,
lang: lang
})
}).then(res => res.json()).then(res => {
if (inNui) {
res = JSON.parse(res); // double packing for sad msgpack-to-json
}
const resultElement = document.querySelector('#result');
if (res.error) {
resultElement.classList.remove('notification', 'is-success');
resultElement.classList.add('notification', 'is-danger');
} else {
resultElement.classList.remove('notification', 'is-danger');
resultElement.classList.add('notification', 'is-success');
}
resultElement.innerHTML = res.error || res.result;
if (res.from) {
resultElement.innerHTML += ' (from ' + res.from + ')';
}
});
e.preventDefault();
});
});
</script>
</body>
</html>