This commit is contained in:
2026-07-07 21:08:52 +02:00
commit 4c20cfc716
2613 changed files with 318021 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
let announcements = [];
const secondsBetweenAnnouncement = 20;
let currentAnnouncementIndex = 0;
function hideAnnouncements() {
$("#nexus-announcements-div").remove();
}
function setAnnouncement(index) {
const announcement = announcements[index];
if(!announcement) {
hideAnnouncements();
return;
}
const animationsTime = 1000;
$("#nexus-announcements-div").fadeOut(animationsTime);
setTimeout(() => {
if(!announcement) return;
const scriptColor = $(".script-name").css("color");
if(announcement.url) {
const div = $(`
<a id="nexus-announcements-div" href="${announcement.url}" target="_blank" onclick="window.invokeNative('openUrl', '${announcement.url}');" class="my-3 p-1 rounded fw-bold d-flex justify-content-between align-items-center"
style="background: linear-gradient(to right, #181818, ${scriptColor}); color: white;">
<span class="font-monospace" style="color: rgba(255, 255, 255, 0.1)">${announcement.id}</span>
<p class="mb-0">${announcement.message}</p>
<button type="button" class="btn-close float-end"></button>
</a>
`);
div.click(function() {
$.post(`https://${resName}/nexusAnnouncementClicked`, JSON.stringify({ announcementId: announcement.id }));
});
$("#nexus-announcements-div").replaceWith(div).fadeIn(animationsTime);
} else {
$("#nexus-announcements-div").replaceWith(`
<div id="nexus-announcements-div" class="my-3 p-1 rounded fw-bold d-flex justify-content-between align-items-center"
style="background: linear-gradient(to right, #181818, ${scriptColor}); color: white;">
<span class="font-monospace" style="color: rgba(255, 255, 255, 0.1)">${announcement.id}</span>
<p class="mb-0">${announcement.message}</p>
<button type="button" class="btn-close float-end"></button>
</div>
`).fadeIn(animationsTime);
}
$("#nexus-announcements-div").find(".btn-close").click((event) => {
event.stopPropagation();
markAnnouncementAsHidden(announcement.id);
});
$.post(`https://${resName}/nexusAnnouncementSeen`, JSON.stringify({ announcementId: announcement.id }));
}, animationsTime);
}
function processAnnouncements() {
currentAnnouncementIndex = (currentAnnouncementIndex + 1) % announcements.length;
setAnnouncement(currentAnnouncementIndex);
}
async function markAnnouncementAsHidden(announcementId) {
await $.post(`https://${resName}/nexusMarkAnnouncementAsHidden`, JSON.stringify({ announcementId }));
refreshAnnouncements();
}
let announcementsIntervalId;
async function refreshAnnouncements() {
hideAnnouncements();
clearInterval(announcementsIntervalId);
announcementsIntervalId = setInterval(processAnnouncements, secondsBetweenAnnouncement * 1000);
announcements = await $.post(`https://${resName}/nexusGetAnnouncements`);
if(!announcements || announcements.length == 0) return;
currentAnnouncementIndex = Math.floor(Math.random() * announcements.length);
const div = $(`
<div id="nexus-announcements-div"></div>
`);
processAnnouncements()
$("#main-bar").prepend(div);
}

View File

@@ -0,0 +1,348 @@
const ITEM_TYPES = {
item: {icon: "bi bi-box", color: "#b8e994"},
weapon: {icon: "bi bi-radioactive", color: "#d35400"},
account: {icon: "bi bi-wallet2", color: "#f1c40f"},
}
let previousObject = null;
let isMetadataDisabled = false;
let isDialogOpen = false;
function addDivForMetadataField(parentDiv, fieldId, fieldValue, fieldType="string") {
const div = $(`
<div class="field-div d-flex justify-content-center align-items-center gap-3 mb-4">
<button type="button" class="btn-close"></button>
<div class="input-group">
<input type="text" class="form-control font-monospace w-25 field-id" placeholder="Field ID" required>
<select class="form-select field-type-select">
<option value="string">String</option>
<option value="number">Number</option>
<option value="boolean">Bool</option>
</select>
<input type="text" class="form-control field-value-input w-25" data-field-type="string" placeholder="A tasty apple" style="display:none;">
<input type="number" step="0.01" class="form-control field-value-input w-25" data-field-type="number" placeholder="1074" style="display:none;">
<select class="form-select field-value-input w-25" data-field-type="boolean" style="display:none;">
<option value="true" selected>True</option>
<option value="false">False</option>
</select>
</div>
</div>
`);
if (fieldId) div.find(".field-id").val(fieldId);
if (fieldValue !== undefined && fieldValue !== null) {
fieldType = typeof fieldValue;
div.find(`.field-value-input[data-field-type='${fieldType}']`).val(fieldValue.toString());
}
if (fieldType) {
div.find(".field-type-select").val(fieldType);
div.find(`.field-value-input[data-field-type='${fieldType}']`).prop("required", true).show();
}
div.find(".field-type-select").change(function() {
const fieldType = $(this).val();
div.find(".field-value-input").prop("required", false).hide();
div.find(`.field-value-input[data-field-type='${fieldType}']`).prop("required", true).show();
});
div.find(".btn-close").click(function() { div.remove(); });
parentDiv.append(div);
}
async function metadataDialog(oldMetadata={}) {
let resolve = null;
const promise = new Promise((res) => { resolve = res; });
const div = $(`
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" role="dialog" style="z-index: 1070;">
<div class="modal-dialog modal-dialog-centered" role="document">
<form class="modal-content needs-validation" novalidate>
<div class="modal-header">
<h5 class="modal-title">${getLocalizedText("menu:items_dialog:title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="metadata-fields-list">
</div>
</div>
<div class="modal-footer">
<div class="d-flex col">
<button type="button" class="btn btn-info add-metadata-field-btn float-start">Add field</button>
</div>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${getLocalizedText("menu:close")}</button>
<button type="submit" class="btn btn-success">${getLocalizedText("menu:input_text:confirm")}</button>
</div>
</form>
</div>
`);
div.find(".add-metadata-field-btn").click(function() {
const parentDiv = div.find(".metadata-fields-list");
addDivForMetadataField(parentDiv);
});
for(const [fieldId, fieldValue] of Object.entries(oldMetadata)) {
addDivForMetadataField(div.find(".metadata-fields-list"), fieldId, fieldValue);
}
// delete the div when the modal is closed
div.on("hidden.bs.modal", function() {
div.remove();
});
div.modal("show");
div.find("form").submit(function(event) {
if(isThereAnyErrorInForm(event)) return;
const metadata = {};
let foundAny = false;
div.find(".field-div").each(function() {
const fieldId = $(this).find(".field-id").val();
const fieldType = $(this).find(".field-type-select").val();
let fieldValue = $(this).find(`.field-value-input[data-field-type='${fieldType}']`).val();
if(fieldType == "number") fieldValue = parseFloat(fieldValue);
else if(fieldType == "boolean") fieldValue = fieldValue == "true";
metadata[fieldId] = fieldValue;
foundAny = true;
});
div.modal("hide");
if(!foundAny) { resolve(null); return; };
resolve(metadata);
});
return promise;
}
function appendObjectToItemsList(object, itemsListDiv) {
let objectDiv = $(`
<li class="list-group-item list-group-item-action clickable object-li" data-object-name="${object.name}">
<i class="${ITEM_TYPES[object.type].icon}" title="${object.type}" data-bs-toggle="tooltip" data-bs-placement="top"></i>
<span class="ms-2 object-label-span" title="${object.name}" data-bs-toggle="tooltip" data-bs-placement="right">${object.label}</span>
</li>
`);
// Keeps old metadata
if(object.name === previousObject?.name && !isMetadataDisabled) {
object.metadata = previousObject.metadata;
}
if(object.type == "item" && !isMetadataDisabled) {
const editMetadataBtn = $(`
<button type="button" class="edit-metadata-btn btn p-0 border-0 float-end" data-bs-toggle="tooltip" data-bs-placement="left" title="Metadata">
<i class="bi bi-pencil-square"></i>
</button>
`);
objectDiv.append(editMetadataBtn);
}
objectDiv.hover(function() {
objectDiv.find("[data-bs-toggle='tooltip']").tooltip();
}, function() {
objectDiv.find("[data-bs-toggle='tooltip']").tooltip("hide");
});
objectDiv.find(".edit-metadata-btn").click(async function(e) {
e.stopPropagation();
let object = objectDiv.data("object");
let newMetadata = await metadataDialog(object.metadata);
if (!newMetadata) return;
object.metadata = newMetadata;
objectDiv.data("object", object);
});
if(object.isManualInput) {
objectDiv.find(".object-label-span").addClass("font-monospace fw-light").attr("title", "Manual input")
objectDiv.addClass("manual-input-div");
objectDiv.hide();
}
objectDiv.css("color", ITEM_TYPES[object.type].color);
objectDiv.data("object", object);
itemsListDiv.append(objectDiv);
return objectDiv;
}
async function chooseObject(oldObject, metadataDisabled) {
if(isDialogOpen) return;
isDialogOpen = true;
previousObject = oldObject;
isMetadataDisabled = metadataDisabled;
let resolve = null;
const promise = new Promise((res) => { resolve = res; });
const div = $(`
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" role="dialog" style="z-index: 1070;">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${getLocalizedText("menu:items_dialog:title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body pt-0">
<div class="input-group">
<span class="input-group-text">${getLocalizedText("menu:items_dialog:search_item")}</span>
<input type="text" class="form-control item-search" placeholder="Name | ID | Type">
</div>
<ul class="list-group mt-2 items-list scrollbar" style="overflow: auto; max-height: 70vh">
</ul>
</div>
</div>
</div>
`);
let objects = await $.post(`https://${resName}/getAllObjects`);
objects = objects
.filter(object => object && object.name && object.label && object.type && typeof object.label == "string")
.sort((a, b) => a.label.localeCompare(b.label));
objects.push(
{type: "item", label: "Manual input", name: "manual_input", isManualInput: true},
{type: "weapon", label: "Manual input", name: "manual_input", isManualInput: true},
{type: "account", label: "Manual input", name: "manual_input", isManualInput: true}
);
let itemsListDiv = div.find(".items-list");
// delete the div when the modal is closed
div.on("hidden.bs.modal", function() {
isDialogOpen = false;
div.remove();
resolve(null);
});
div.on("shown.bs.modal", function() {
div.find(".item-search").focus();
});
div.find(".item-search").val("").on("keyup", function() {
const rawVal = $(this).val();
const lowercaseVal = rawVal.toLowerCase();
const valToId = rawVal.replaceAll(" ", "_");
div.find(".items-list li").each(function() {
const object = $(this).data("object");
let canShow = lowercaseVal == "" || object.label.toLowerCase().indexOf(lowercaseVal) > -1 || object.name.toLowerCase().indexOf(lowercaseVal) > -1 || object.type.toLowerCase().indexOf(lowercaseVal) > -1;
$(this).toggle(canShow)
});
const manualInputDiv = div.find(".manual-input-div");
if (rawVal) {
let object = manualInputDiv.data("object");
object.label = valToId;
object.name = valToId;
manualInputDiv.data("object", object);
manualInputDiv.find(".object-label-span").text(valToId);
manualInputDiv.show();
} else {
manualInputDiv.hide();
}
});
div.modal("show");
objects.forEach(thisObj => {
const objectDiv = appendObjectToItemsList(thisObj, itemsListDiv);
objectDiv.click(function() {
let object = $(this).data("object");
div.modal("hide");
resolve(object);
});
});
return promise;
}
function setChoosenObject(chooseObjectDiv, object) {
const objectInput = chooseObjectDiv.find(".choose-object-label");
objectInput.tooltip("dispose");
if(!object) {
objectInput.data("object", null);
objectInput.val("");
return;
}
objectInput.data("object", object);
objectInput.val(object.label);
if(object.metadata) {
objectInput.tooltip({title: `${object.type}: ${object.name}<br><span class="font-monospace">${JSON.stringify(object.metadata)}<span>`, placement: "top", html: true});
} else {
objectInput.tooltip({title: `${object.type}: ${object.name}`, placement: "top"});
}
}
function getChoosenObject(chooseObjectDiv) {
return chooseObjectDiv.find(".choose-object-label").data("object");
}
function loadChooseObjectDivs(elements, oldObject) {
if(!elements) return;
elements.each(function() {
const chooseObjectDiv = $(this);
const chooseObjectBtn = chooseObjectDiv.find(".choose-object-btn");
const objectInput = chooseObjectDiv.find(".choose-object-label");
objectInput.prop("disabled", true); // User can't edit it anyway
const metadataDisabled = chooseObjectDiv.attr("data-metadata-disabled") != undefined;
chooseObjectBtn.click(async function() {
let object = await chooseObject(objectInput.data("object"), metadataDisabled);
setChoosenObject(chooseObjectDiv, object);
});
if(oldObject) setChoosenObject(chooseObjectDiv, oldObject);
});
}
function createDivForObjectChoose(object) {
const div = $(`
<div class="choose-object-div input-group col" data-metadata-disabled>
<input type="text" class="form-control choose-object-label" placeholder="Item ID">
<button type="button" class="btn btn-secondary choose-object-btn" data-bs-toggle="tooltip" data-bs-placement="top" title="${getLocalizedText("menu:dialog:choose_item")}"><i class="bi bi-list-ul"></i></button>
</div>
`);
loadChooseObjectDivs(div);
setChoosenObject(div.find(".choose-object-label"), object);
return div;
}
// Loads the choose object divs in .html (not in js)
loadChooseObjectDivs( $(".choose-object-div") );

View File

@@ -0,0 +1,66 @@
async function controlsDialog() {
const div = $(`
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" role="dialog" style="z-index: 1070;">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${ getLocalizedText("menu:controls_dialog:title") }</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center" >
<p class="info-text text-warning">${ getLocalizedText("menu:controls_dialog:info_1") }</p>
<p class="info-text-3 text-danger" style="display:none">${ getLocalizedText("menu:controls_dialog:info_3") }</p>
<div class="scrollbar" style="max-height:60vh; overflow-y:auto; overflow-x:hidden;">
<table class="table table-hover" style="display:none">
<thead>
<tr>
<th class="col-3">ID</th>
<th class="col-4">Default</th>
<th class="col">Name</th>
</tr>
</thead>
<tbody class="controls-list text-break">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
`);
// delete the div when the modal is closed
div.on("hidden.bs.modal", function() {
div.remove();
});
div.modal("show");
let controlsList = div.find(".controls-list");
const controls = await $.post(`https://${resName}/detectPressedControls`);
controlsList.empty();
return new Promise((resolve, reject) => {
controls.forEach(control => {
let row = $(`<tr class="clickable" value=${control.id}><td>${control.id}</td><td>${control.default}</td><td>${control.name}</td></tr>`);
row.click(() => {
div.modal("hide");
resolve(control.id);
});
controlsList.append(row);
});
div.find("table").show();
div.find(".info-text").text(getLocalizedText("menu:controls_dialog:info_2"));
div.find(".info-text-3").show();
});
}
$(document).on("click", "input[data-dialog-type='control']", async function() {
const result = await controlsDialog();
if (!result) return;
$(this).val(result);
});

View File

@@ -0,0 +1,32 @@
function toggleCursor(enabled) {
if (enabled) {
$.post(`https://${resName}/enableCursor`, JSON.stringify({}));
} else {
$.post(`https://${resName}/disableCursor`, JSON.stringify({}));
}
}
function loadDialog(dialogName) {
var script = document.createElement('script');
script.setAttribute('src',`../utils/dialogs/${dialogName}/${dialogName}.js`);
document.head.appendChild(script);
}
// Messages received by client
window.addEventListener('message', (event) => {
let data = event.data;
let action = data.action;
switch(action) {
case "loadDialog": {
var script = document.createElement('script');
script.setAttribute('src',`../utils/dialogs/${data.dialogName}/${data.dialogName}.js`);
document.head.appendChild(script);
break;
}
}
})
$.post(`https://${resName}/nuiReady`, JSON.stringify({}));

View File

@@ -0,0 +1,154 @@
let allResources = [];
let externalScriptsNames = {};
async function getAllResources() {
const resources = await $.post(`https://${resName}/getAllResources`);
return resources;
}
async function getIntegrationsResources() {
const resources = await $.post(`https://${resName}/getIntegrationsResources`);
return resources;
}
async function resourcesDialog() {
let resolve, reject;
let promise = new Promise((res, rej) => { resolve = res; reject = rej; });
const div = $(`
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" role="dialog" style="z-index: 1070;">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${ getLocalizedText("menu:dialogs:resources") }</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<div class="scrollbar" style="max-height:60vh; overflow-y:auto; overflow-x:hidden;">
<div class="resources-list">
</div>
</div>
</div>
</div>
</div>
</div>
`);
// delete the div when the modal is closed
div.on("hidden.bs.modal", function() {
div.remove();
resolve();
});
div.modal("show");
let resourcesList = div.find(".resources-list");
const resources = await getAllResources();
resourcesList.empty();
resources.forEach(resource => {
const resourceDiv = createDivForResource(resource);
resourceDiv.click(() => {
div.modal("hide");
resolve(resource.name);
});
resourcesList.append(resourceDiv);
});
return promise;
}
function getLegendForState(state) {
switch(state) {
case "started": return "bg-success";
case "stopped": return "bg-danger";
default: return "";
}
}
function createDivForResource(resource) {
const div = $(`
<span class="resource-span d-flex align-items-center lh-large" role="button">
<span class="legend-circle-lg ${getLegendForState(resource.state)}"></span>
<span>${resource.name} ${resource.version || ""}</span>
${ resource.author ?
`<span class="ms-2 badge bg-info">
${resource.author}
</span>`
: "" }
</span>
`);
// Tooltip
div.find(".legend-circle-lg").tooltip({
title: resource.state,
placement: "left",
trigger: "hover",
});
return div;
}
function createDivForIntegration(resource) {
const resName = resource.name;
const div = $(`<button type="button" class="btn d-flex align-items-center py-1" data-integration-name="${resName}"></button>`);
const resourceDiv = createDivForResource(resource);
resourceDiv.appendTo(div);
const newResName = externalScriptsNames[resName];
if(newResName && newResName !== resName) {
let newRes = allResources.find(r => r.name === newResName);
if(!newRes) {
newRes = {
name: newResName,
version: undefined,
state: "stopped",
};
}
$(`<i class="bi bi-arrow-right mx-3"></i>`).appendTo(div);
createDivForResource(newRes).appendTo(div);
resourceDiv.css("opacity", 0.3);
}
div.click(async function() {
const newResource = await resourcesDialog() || resName;
externalScriptsNames[resName] = newResource;
loadIntegrationsSettings(externalScriptsNames);
});
return div;
}
async function loadIntegrationsSettings(newIntegrationsScripts) {
externalScriptsNames = newIntegrationsScripts;
const resources = await getIntegrationsResources();
if(resources.length === 0) return;
allResources = await getAllResources();
const integrationsListDiv = $(".external-scripts-names-dialog");
integrationsListDiv.empty();
resources.forEach(resource => {
const div = createDivForIntegration(resource);
div.appendTo(integrationsListDiv);
});
}
function getIntegrationSettings() {
for (const [key, value] of Object.entries(externalScriptsNames)) {
if(!value) {
externalScriptsNames[key] = key;
}
}
return externalScriptsNames;
}

View File

@@ -0,0 +1,77 @@
async function itemsDialog() {
let div = $(`
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" role="dialog" style="z-index: 1070;">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${getLocalizedText("menu:items_dialog:title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="input-group">
<span class="input-group-text">${getLocalizedText("menu:items_dialog:search_item")}</span>
<input type="text" class="form-control item-search">
</div>
<ul class="list-group mt-2 items-list scrollbar" style="overflow: auto; max-height: 70vh">
</ul>
</div>
</div>
</div>
</div>
`)
// delete the div when the modal is closed
div.on("hidden.bs.modal", function() {
div.remove();
});
div.modal("show");
div.find(".item-search").val("").on("keyup", function() {
let text = $(this).val().toLowerCase();
div.find(".items-list li").filter(function() {
$(this).toggle($(this).text().toLowerCase().indexOf(text) > -1)
});
})
const items = await $.post(`https://${resName}/getAllItems`)
let itemsListDiv = div.find(".items-list");
itemsListDiv.empty();
if (!Array.isArray(items)) {
console.error('Invalid items data received:', items);
return new Promise((resolve) => {
div.modal("hide");
resolve(null);
});
}
for(const item of items) {
if (!item || typeof item !== 'object') continue;
let itemDiv = $(`
<li class="list-group-item list-group-item-action clickable" data-item-name="${item.name || 'unknown'}">${item.label || 'Unknown'}</li>
`);
itemsListDiv.append(itemDiv);
}
return new Promise((resolve) => {
div.find(".list-group-item").click(function() {
let itemName = $(this).data("itemName");
div.modal("hide");
resolve(itemName);
});
div.find(".item-search").keydown(function(e) {
if (e.keyCode != 13) return;
let searchContent = $(this).val();
div.modal("hide");
resolve(searchContent);
});
});
}

View File

@@ -0,0 +1,173 @@
function jobsDialog(oldJobs) {
return new Promise(async (resolve, reject) => {
let div = $(`
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" role="dialog" style="z-index: 1070; background: rgba(25, 25, 25, 0.7);">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${getLocalizedText("menu:jobs_dialog:title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="d-flex align-items-center justify-content-center fs-2">
<input class="form-check-input me-2 my-auto all-jobs-allowed-checkbox" type="checkbox">
<label class="form-label fw-bold mb-0">${getLocalizedText("menu:all_jobs_allowed")}</label>
</div>
<hr class="my-3">
<div class="input-group">
<span class="input-group-text">${getLocalizedText("menu:jobs_dialog:search_job")}</span>
<input type="text" class="form-control input-job-search">
</div>
<div class="mt-3 jobs-list" style="max-height: 50vh; overflow-y: auto">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${getLocalizedText("menu:close")}</button>
<button type="button" class="btn btn-success input-jobs-confirm-btn">${getLocalizedText("menu:confirm")}</button>
</div>
</div>
</div>
</div>
`);
// delete the div when the modal is closed
div.on("hidden.bs.modal", function() {
div.remove();
});
div.modal("show");
div.find(".input-job-search").val("");
const jobs = await $.post(`https://${resName}/getAllJobs`, JSON.stringify({}));
let jobListDiv = div.find(".jobs-list")
jobListDiv.empty();
for(const[jobName, jobData] of Object.entries(jobs)) {
let jobDiv = $(`
<div class="form-check fs-3 mb-2 ms-1">
<input class="form-check-input" data-type="job" type="checkbox" data-job-name="${jobName}">
<label class="form-check-label fw-bold mb-0">${jobData.label}</label>
<div class="job-grades ms-3">
</div>
</div>
`);
for(let [grade, gradeData] of Object.entries(jobData.grades)) {
// QB-Core
if(gradeData.grade == undefined) {
gradeData.grade = grade;
gradeData.label = gradeData.name;
}
let rankDiv = $(`
<div class="rank-div">
<input class="form-check-input" data-type="job-grade" type="checkbox" data-job-name="${jobName}" data-grade=${gradeData.grade}>
<label class="form-check-label">
${gradeData.grade} - ${gradeData.label}
</label>
</div>
`);
jobDiv.find(".job-grades").append(rankDiv);
}
jobListDiv.append(jobDiv);
}
// Disables and uncheck grades checkbox if the entire job is selected
jobListDiv.find(`[data-type="job"]`).change(function() {
let isChecked = $(this).prop("checked");
if(isChecked) {
$(this).parent().find(".job-grades").find("input").prop("checked", false).prop("disabled", true);
} else {
$(this).parent().find(".job-grades").find("input").prop("disabled", false);
}
})
if(oldJobs) {
for(let [jobName, allowedGrades] of Object.entries(oldJobs)) {
if(allowedGrades === true) {
jobListDiv.find(`[data-type="job"][data-job-name="${jobName}"]`).prop("checked", true).change();
} else {
for(let [grade, _] of Object.entries(allowedGrades)) {
jobListDiv.find(`[data-type="job-grade"][data-job-name="${jobName}"][data-grade="${grade}"]`).prop("checked", true);
}
}
}
}
// Unbinds the button and rebinds it to callback the selected jobs
div.find(".input-jobs-confirm-btn").unbind().click(function() {
const allJobsAllowed = div.find(".all-jobs-allowed-checkbox").prop("checked");
if(allJobsAllowed) {
resolve(false);
} else {
let selectedJobs = {};
jobListDiv.find("input:checked").each(function() {
let checkBoxType = $(this).data("type");
let jobName = $(this).data("jobName");
switch(checkBoxType) {
case "job":
selectedJobs[jobName] = true;
break;
case "job-grade":
let grade = $(this).data("grade");
if(!selectedJobs[jobName]) {
selectedJobs[jobName] = {};
}
selectedJobs[jobName][grade] = true;
break;
default:
console.log("Unknown checkbox type: " + checkBoxType + " in jobs dialog");
break;
}
});
resolve(selectedJobs);
}
div.modal("hide");
});
div.find(".input-job-search").on("keyup", function() {
let text = $(this).val().toLowerCase();
div.find(".jobs-list .form-check").filter(function() {
$(this).toggle($(this).text().toLowerCase().indexOf(text) > -1)
});
});
div.find(".all-jobs-allowed-checkbox").change(function() {
let isChecked = $(this).prop("checked");
if(isChecked) {
div.find(".jobs-list input").prop("checked", true).prop("disabled", true);
} else {
div.find(".jobs-list input").prop("checked", false).prop("disabled", false);
}
});
// If oldJobs is false (so all jobs are allowed), check the checkbox
if(oldJobs === false || oldJobs === null || oldJobs === undefined) {
div.find(".all-jobs-allowed-checkbox").prop("checked", true).change();
}
});
}

View File

@@ -0,0 +1,19 @@
.swal-overlay {
background-color: rgba(11, 11, 11, 0.6);
}
.swal-modal {
background-color: #212529 !important;
color: #fff;
border: 1px solid #555;
}
.swal-icon--success:before,
.swal-icon--success:after,
.swal-icon--success__hide-corners {
background: none !important;
}
.swal-title, .swal-text {
color: white !important;
}

View File

@@ -0,0 +1,96 @@
/*
██ ███ ██ ████████ ██████ ██████
██ ████ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ ██████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ████ ██ ██ ██ ██████
*/
function registerIntroView(introName) {
$.post(`https://${resName}/registerIntroView`, JSON.stringify({introName}));
}
async function hasIntroBeenViewed(introName) {
return await $.post(`https://${resName}/hasIntroBeenViewed`, JSON.stringify({introName}));
}
/*
███ ██ ██████ ████████ ██ ███████ ██ ██████ █████ ████████ ██ ██████ ███ ██ ███████
████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██
██ ██ ██ ██ ██ ██ ██ █████ ██ ██ ███████ ██ ██ ██ ██ ██ ██ ██ ███████
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ████ ██████ ██ ██ ██ ██ ██████ ██ ██ ██ ██ ██████ ██ ████ ███████
*/
const cssLink = document.createElement("link");
cssLink.href = "../utils/dialogs/misc/misc.css";
cssLink.rel = "stylesheet";
cssLink.type = "text/css";
document.head.appendChild(cssLink);
async function showServerResponse(response) {
if(response == null) return;
if(response === true) {
swal("SUCCESS", "", "success");
} else {
swal("ERROR", response || "", "error");
}
}
async function confirmDeletion(message) {
const wantsToDelete = await swal({
title: getLocalizedText("menu:are_you_sure"),
text: message,
icon: "warning",
buttons: true,
dangerMode: true,
});
return wantsToDelete;
}
/*
███████ ██████ ██████ ███ ███ ███████
██ ██ ██ ██ ██ ████ ████ ██
█████ ██ ██ ██████ ██ ████ ██ ███████
██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██████ ██ ██ ██ ██ ███████
*/
function isThereAnyErrorInForm(event) {
event.preventDefault();
event.stopPropagation();
if (!event.target.checkValidity()) {
const invalidInputs = $(event.target).find(':invalid');
if (invalidInputs.length > 0) {
const firstInvalidInput = invalidInputs.first();
firstInvalidInput.focus();
}
$(event.target).addClass('was-validated')
swal("ERROR", `There are ${invalidInputs.length} invalid fields somewhere!`, "error");
return true;
} else {
// The timeout is needed because otherwise the was-validated class is added afterwards by the theme .js
setTimeout(() => {
$(event.target).removeClass("was-validated");
}, 0);
return false;
}
}
function setTomSelectValue(id, value) {
// If id starts with #, remove it
if(id[0] == "#") id = id.substring(1);
let select = document.getElementById(id);
let control = select.tomselect;
if(!control) return;
control.setValue(value);
}

View File

@@ -0,0 +1,36 @@
let isMissingMenuDialogActive = false;
// Messages received by client
window.addEventListener('message', (event) => {
let data = event.data;
let action = data.action;
if (action != 'showMissingMenuDialog') return;
if (isMissingMenuDialogActive) return; else isMissingMenuDialogActive = true;
let div = $(`
<div style="background: rgba(25, 25, 25, 0.7); width: 100%; height: 100%; position: absolute; display: flex; align-items: center; font-family: 'Roboto', sans-serif;">
<div class="container border border-danger rounded py-3" style="background-color: #222; color: white; width: 50%;">
<button type="button" class="btn-close btn-close-white float-end"></button>
<p class="text-center fs-1">${getLocalizedText("menu:missing_menu_default")}</p>
<p class="text-center fs-3">${getLocalizedText("menu:missing_menu_default:description")}</p>
<div class="d-flex justify-content-center mt-3">
<a class="btn btn-success fs-3" href="https://drive.google.com/file/d/1Ezz-d50NIKQZeZJ-RgyclvNG7qC4Nfu8/view?usp=sharing" target="_blank" onclick='window.invokeNative("openUrl", "https://drive.google.com/file/d/1Ezz-d50NIKQZeZJ-RgyclvNG7qC4Nfu8/view?usp=sharing")'>${getLocalizedText("menu:download")}</a>
</div>
</div>
</div>
`);
div.find('.btn-close').click(() => {
div.remove();
toggleCursor(false);
isMissingMenuDialogActive = false;
});
$("html").append(div);
toggleCursor(true);
})

View File

@@ -0,0 +1,80 @@
async function getAllModules() {
return await $.post(`https://${resName}/getAllModules`);
}
function createDivForModule(moduleType, options) {
const div = $(`
<div class="d-flex align-items-center gap-3">
<p class="text-center my-auto text-capitalize" style="width:auto">${moduleType}</p>
<select class="module-options form-select w-25" data-select></select>
</div>
`);
const select = div.find(".module-options");
select.data("moduleType", moduleType);
options.forEach(name => {
const option = $(`<option value="${name}">${name}</option>`);
option.appendTo(select);
});
return div;
}
async function loadModulesSettings(modulesSettings) {
const modulesDiv = $(".modules-dialog");
modulesDiv.empty();
const modules = await getAllModules();
let unselectedModules = [];
modules.forEach(module => {
const div = createDivForModule(module.type, module.options);
div.appendTo(modulesDiv);
if (modulesSettings[module.type] == "not_selected") {
unselectedModules.push(module);
}
});
// For each select, set the value to the value in modulesSettings
modulesDiv.find(".module-options").each((index, select) => {
const moduleType = $(select).data("moduleType");
const moduleName = modulesSettings[moduleType];
$(select).val(moduleName);
});
// For each not_selected module, add it to unselectedModules
if(unselectedModules.length <= 0) return;
let tabs = [];
unselectedModules.forEach(module => {
const tab = {
id: module.type,
title: module.type,
description: "Choose the one you prefer (can be changed later)",
options: module.otions.map(option => { return { label: undefined, value: option } } )
};
tabs.push(tab);
});
const results = await wizard(tabs)
}
function getModulesSettings() {
const modulesSettings = {};
$(".modules-dialog .module-options").each((index, select) => {
const moduleType = $(select).data("moduleType");
const moduleName = $(select).val();
modulesSettings[moduleType] = moduleName;
});
return modulesSettings;
}

View File

@@ -0,0 +1,112 @@
let isDialogActive = false;
// Messages received by client
window.addEventListener('message', (event) => {
let data = event.data;
let action = data.action;
if (action != 'showNotAllowedDialog') return;
if (isDialogActive) return;
const div = $(`
<div style="background: rgba(25, 25, 25, 0.7); width: 100%; height: 100%; position: absolute; display: flex; align-items: center;"">
<div class="container border border-danger rounded py-3" style="background-color: #222; color: white; width: 50%;">
<button type="button" class="btn-close btn-close-white float-end"></button>
<p class="text-center fs-1">${getLocalizedText("menu:not_allowed:you_are_not_allowed")}</p>
<p class="text-center fs-3">${getLocalizedText("menu:not_allowed:if_you_are_admin")}</p>
<p class="text-center fs-3 text-danger">Server restart required</p>
<p class="text-center fs-3 mt-5 mb-2"># Only use one of these. If one doesn't work, try another one</p>
<div class="container text-warning p-2">
<hr class="mb-4">
${data.playerIdentifiers["license"] ? `
<div class="d-flex align-items-center">
<p class="not-allowed-license font-monospace mb-0 flex-grow-1"></p>
<button class="btn btn-warning ms-2 copy-btn" data-target="not-allowed-license">
<i class="bi bi-clipboard-fill"></i>
</button>
</div>
<hr class="my-4">
` : ''}
${data.playerIdentifiers["license2"] ? `
<div class="d-flex align-items-center">
<p class="not-allowed-license2 font-monospace mb-0 flex-grow-1"></p>
<button class="btn btn-warning ms-2 copy-btn" data-target="not-allowed-license2">
<i class="bi bi-clipboard-fill"></i>
</button>
</div>
<hr class="my-4">
` : ''}
${data.playerIdentifiers["steam"] ? `
<div class="d-flex align-items-center">
<p class="not-allowed-steamid font-monospace mb-0 flex-grow-1"></p>
<button class="btn btn-warning ms-2 copy-btn" data-target="not-allowed-steamid">
<i class="bi bi-clipboard-fill"></i>
</button>
</div>
` : ''}
</div>
</div>
</div>
`);
div.find('.btn-close').click(() => {
div.remove();
toggleCursor(false);
isDialogActive = false;
});
div.find('.copy-btn').click(function() {
const target = $(this).data('target');
const text = div.find(`.${target}`).text().trim();
// Create temporary textarea element
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
// Select and copy text
textarea.select();
document.execCommand('copy');
// Remove temporary element
document.body.removeChild(textarea);
// Visual feedback
const btn = $(this);
const originalHtml = btn.html();
btn.html('<i class="bi bi-check-lg"></i>');
setTimeout(() => {
btn.html(originalHtml);
}, 1000);
});
if (data.playerIdentifiers["license"]) {
div.find(".not-allowed-license").text(`
add_ace identifier.license:${data.playerIdentifiers["license"]} ${data.acePermission} allow # Add permission to '${data.playerName}' Rockstar license
`);
}
if (data.playerIdentifiers["license2"]) {
div.find(".not-allowed-license2").text(`
add_ace identifier.license2:${data.playerIdentifiers["license2"]} ${data.acePermission} allow # Add permission to '${data.playerName}' Rockstar license2
`);
}
if (data.playerIdentifiers["steam"]) {
div.find(".not-allowed-steamid").text(`
add_ace identifier.steam:${data.playerIdentifiers["steam"]} ${data.acePermission} allow # Add permission to '${data.playerName}' Steam account
`);
}
$("html").append(div);
toggleCursor(true);
isDialogActive = true;
})

View File

@@ -0,0 +1,9 @@
async function placeEntity(model, entityType, placedEntities) {
$("html").hide();
const data = await $.post(`https://${resName}/placeEntity`, JSON.stringify({ model, entityType, placedEntities }));
$("html").show();
return data;
}

View File

@@ -0,0 +1,44 @@
section.progressbar-section {
width: 100%;
display: flex;
flex-direction: column;
justify-content: end;
align-items: center;
text-align: center;
}
h1.progressbar-text {
font-size: 24px;
color: #fff;
margin-bottom: 20px;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 1.0);
}
ul.loader {
width: 40%;
list-style: none;
display: flex;
align-items: center;
position: relative;
}
ul.loader li {
width: 0;
height: 2px;
background-color: rgba(255,255,255,0);
}
ul.loader li.background-bar {
width: 100%;
height: 2px;
position: absolute;
}
ul.loader li.empty {
box-shadow: none;
}
ul.loader li.filled {
width: 100%;
transition: width 0.5s linear;
}

View File

@@ -0,0 +1,57 @@
let progressBarTimeout;
function startTimedProgressBar(time, text, color="#47ff33") {
var cssLink = document.createElement("link");
cssLink.href = "../utils/dialogs/progressbar/progressbar.css";
cssLink.rel = "stylesheet";
cssLink.type = "text/css";
document.head.appendChild(cssLink);
const div = $(`
<section class="fixed-bottom mb-5 progressbar-section">
<h1 class="progressbar-text">Loading...</h1>
<ul class="loader p-0">
</ul>
</section>
`);
let duration = time;
div.find("h1").text(text);
div.find(".loader").append(`<li class="bar empty"></li>`);
div.find(".loader").append(`<li class="background-bar"></li>`);
div.find(".bar").css("transition-duration", `${duration}ms`);
div.find(".background-bar").css("background-color", `${color}20`); // Aggiunge l'alpha 0.2 al colore
setTimeout(() => {
div.find(".bar").removeClass("empty");
div.find(".bar").addClass("filled");
div.find(".filled").css("box-shadow", `inset 0 0 10px 2px ${color}, 0 0 25px 5px ${color}, 0 0 10px 2px rgba(0, 0, 0, 0.5)`); // Aggiungi questa riga qui
}, 100);
progressBarTimeout = setTimeout(() => {
div.remove();
}, duration + 100);
$("body").append(div);
}
function stopProgressBar() {
clearTimeout(progressBarTimeout);
$('.progressbar-section').remove();
}
// Messages received by client
window.addEventListener('message', (event) => {
let data = event.data;
let action = data.action;
if(action == "progressBar") {
startTimedProgressBar(data.time, data.text, data.hexColor)
} else if(action == "stopProgressBar") {
stopProgressBar();
}
})

View File

@@ -0,0 +1,59 @@
async function singleJobDialog() {
return new Promise((resolve, reject) => {
let div = $(`
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" role="dialog" style="z-index: 1070;">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${getLocalizedText("menu:jobs_dialog:title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="input-group">
<span class="input-group-text">${getLocalizedText("menu:jobs_dialog:search_job")}</span>
<input type="text" class="form-control single-job-search">
</div>
<ul class="list-group mt-2 single-jobs-list scrollbar" style="overflow: auto; max-height: 70vh">
</ul>
</div>
</div>
</div>
</div>
`)
// delete the div when the modal is closed
div.on("hidden.bs.modal", function() {
div.remove();
});
div.modal("show");
div.find(".single-job-search").val("").on("keyup", function() {
let text = $(this).val().toLowerCase();
div.find(".single-jobs-list li").filter(function() {
$(this).toggle($(this).text().toLowerCase().indexOf(text) > -1)
});
})
$.post(`https://${resName}/getAllJobs`, JSON.stringify({}), function (jobs) {
let jobsListDiv = div.find(".single-jobs-list");
jobsListDiv.empty();
for(const[jobName, jobData] of Object.entries(jobs)) {
let jobDiv = $(`
<li class="list-group-item list-group-item-action clickable" value=${jobName}>${jobData.label}</li>
`);
jobDiv.click(function() {
div.modal("hide");
resolve(jobName);
});
jobsListDiv.append(jobDiv);
}
});
})
}

View File

@@ -0,0 +1,12 @@
#bar-container {
position: fixed;
bottom: 10%;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 4px;
}
.segment {
flex: 0 0 auto;
}

View File

@@ -0,0 +1,76 @@
let skillCheckCSSLink = document.createElement("link");
skillCheckCSSLink.href = "../utils/dialogs/skillcheck/skillcheck.css";
skillCheckCSSLink.rel = "stylesheet";
skillCheckCSSLink.type = "text/css";
document.head.appendChild(skillCheckCSSLink);
window.addEventListener("message", e => {
if (e.data.action !== "cancelSkillcheck") return;
$(".skillcheck-container").remove();
$('html').off("keydown.skillcheck");
});
window.addEventListener("message", e => {
if (e.data.action !== "skillcheck") return;
const speed = e.data.speed * 233
$(".skillcheck-container").remove();
let numSegments = 25,
containerWidth = 400,
gap = 4,
segmentWidth = (containerWidth - (numSegments - 1) * gap) / numSegments,
segmentUnit = segmentWidth + gap,
correctBlocksCount = e.data.positiveBars,
baseColors = { correct: "rgba(3, 110, 187, 0.8)", incorrect: "rgba(141, 17, 48, 0.8)" },
highlightColors = { correct: "rgba(0, 200, 255, 0.96)", incorrect: "rgb(243, 23, 45)" },
pattern = new Array(numSegments).fill("incorrect"),
startIndex = Math.floor(Math.random() * (numSegments - correctBlocksCount + 1));
for (let i = startIndex; i < startIndex + correctBlocksCount; i++) pattern[i] = "correct";
let bar = $('<div id="bar-container" class="skillcheck-container"></div>').css({ width: containerWidth + "px", height: "70px" });
for (let i = 0; i < numSegments; i++)
$('<div class="segment"></div>')
.attr("data-type", pattern[i])
.css({ "background-color": baseColors[pattern[i]], width: segmentWidth + "px", height: "100%" })
.appendTo(bar);
$("body").append(bar);
let cursorSpeed = speed, pos = 0, last = performance.now(), dir = 1, active = true, maxPos = containerWidth,
pauseTime = null, pauseDuration = 0.1;
function animate(t) {
if (!active) return;
let dt = (t - last) / 1000;
last = t;
if (pauseTime !== null) {
if (t - pauseTime >= pauseDuration * 1000) { dir *= -1; pauseTime = null; }
else { pos = (dir > 0) ? maxPos : 0; }
} else {
pos += cursorSpeed * dt * dir;
if (pos >= maxPos) { pos = maxPos; pauseTime = t; }
else if (pos <= 0) { pos = 0; pauseTime = t; }
}
let currentIndex = Math.min(Math.floor(pos / segmentUnit), numSegments - 1);
bar.children(".segment").each((i, el) => {
let seg = $(el), type = seg.attr("data-type");
if (i === currentIndex)
seg.css({ transition: "transform 0.2s ease, background-color 0.2s ease", "background-color": highlightColors[type], transform: "scale(1.4)" });
else if (i === currentIndex - 1 || i === currentIndex + 1)
seg.css({ transition: "transform 0.3s ease, background-color 0.3s ease", "background-color": highlightColors[type], transform: "scale(1.2)" });
else if (i === currentIndex - 2 || i === currentIndex + 2)
seg.css({ transition: "transform 0.4s ease, background-color 0.4s ease", "background-color": highlightColors[type], transform: "scale(1.1)" });
else
seg.css({ transition: "transform 0.5s ease, background-color 0.5s ease", "background-color": baseColors[type], transform: "scale(1)" });
});
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
$('html').off("keydown.skillcheck").on("keydown.skillcheck", e => {
if (!active) return;
if ((e.key && e.key.toLowerCase() !== "e") && (e.which && e.which !== 69)) return;
active = false;
let currentIndex = Math.min(Math.floor(pos / segmentUnit), numSegments - 1),
seg = bar.children(".segment").eq(currentIndex),
type = seg.attr("data-type");
$.post(`https://${resName}/skillCheckFinish`, JSON.stringify({ success: type === "correct" }));
setTimeout(() => bar.remove(), 300);
});
});

View File

@@ -0,0 +1,67 @@
async function weaponsDialog() {
let div = $(`
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" role="dialog" style="z-index: 1070;">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${getLocalizedText("menu:weapons_dialog:title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="input-group">
<span class="input-group-text">${getLocalizedText("menu:weapons_dialog:search_weapon")}</span>
<input type="text" class="form-control weapon-search">
</div>
<ul class="list-group mt-2 weapons-list scrollbar" style="overflow: auto; max-height: 70vh">
</ul>
</div>
</div>
</div>
</div>
`)
// delete the div when the modal is closed
div.on("hidden.bs.modal", function() {
div.remove();
});
div.modal("show");
div.find(".weapon-search").val("").on("keyup", function() {
let text = $(this).val().toLowerCase();
div.find(".weapons-list li").filter(function() {
$(this).toggle($(this).text().toLowerCase().indexOf(text) > -1)
});
})
const weapons = await $.post(`https://${resName}/getAllWeapons`)
let weaponsListDiv = div.find(".weapons-list");
weaponsListDiv.empty();
for(const weaponData of weapons) {
let weaponDiv = $(`
<li class="list-group-item list-group-item-action clickable" data-weapon-name=${weaponData.name}>${weaponData.label}</li>
`);
weaponsListDiv.append(weaponDiv);
}
return new Promise((resolve) => {
div.find(".list-group-item").click(function() {
let weaponName = $(this).data("weaponName");
div.modal("hide");
resolve(weaponName);
});
div.find(".weapon-search").keydown(function(e) {
if (e.keyCode != 13) return;
let searchContent = $(this).val();
div.modal("hide");
resolve(searchContent);
});
});
}

View File

@@ -0,0 +1,41 @@
function addTabForWizard(wizardDiv, tab, active=false) {
const { id, title, description, options } = tab;
const li = $(`
<li class="nav-item">
<button class="nav-link" id="wizardTabOne" data-bs-toggle="pill" data-bs-target="#${id}" type="button" role="tab">
${title}
</button>
</li>
`);
if (active) {
li.find('button').addClass('active');
}
wizardDiv.find(".nav").append(li);
const tabContent = $(`
<div class="tab-pane fade" id="${id}" role="tabpanel">
<h4>${title}</h4>
<p>${description}</p>
</div>
`);
// TODO: ADD options
}
async function wizard(tabs=[]) {
const div = $(`
<div>
<ul class="nav nav-pills steps mb-7 wizard-div" role="tablist">
</ul>
<div class="tab-content">
</div>
</div>
`);
}