486 lines
		
	
	
		
			No EOL
		
	
	
		
			12 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			486 lines
		
	
	
		
			No EOL
		
	
	
		
			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"> 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> |