diff --git a/nui/index.html b/nui/index.html index 4f95ca6..f4da756 100644 --- a/nui/index.html +++ b/nui/index.html @@ -16,5 +16,18 @@
+ diff --git a/nui/main.lua b/nui/main.lua index 33fad24..c7b844a 100644 --- a/nui/main.lua +++ b/nui/main.lua @@ -1,11 +1,9 @@ -local debug = GetConvar("jim_DisableDebug", "false") == "true" and false or true Config = Config or { System = {} } CreateThread(function() local fileLoader = assert(load(LoadResourceFile('jim_bridge', ('starter.lua')), ('@@jim_bridge/starter.lua'))) fileLoader() end) -print("yay") local headerShown = true local sendData = nil @@ -32,13 +30,11 @@ local colours = { -- Functions function openNuiMenu(data, modifiers) - print("Trying to open menu") if not data or not next(data) then return end for _, v in pairs(data) do v["icon"] = v["arrow"] and "fas fa-angle-right" or v["icon"] or nil v["colorScheme"] = v["colourScheme"] and colours[v["colourScheme"]] or (v["colorScheme"] and colours[v["colorScheme"]] or colours["green.7"]) if v["onSelect"] then - if debug then print("^5Debug^7: ^6onSelect^2 found^7 -^2 sending to ^7'^6params^7.^6isAction^3()^7'") end v.params = { isAction = true, event = v["onSelect"] } end end @@ -53,7 +49,7 @@ local function closeMenu() sendData = nil sendModifiers = nil headerShown = false - SetNuiFocus(false) + SetNuiFocus(false, false) SendNUIMessage({ action = 'CLOSE_MENU' }) end @@ -66,7 +62,7 @@ end -- Events -RegisterNetEvent("jim_bridge:client:openMenu", function(data) print("test") openNuiMenu(data) end) +RegisterNetEvent("jim_bridge:client:openMenu", function(data) openNuiMenu(data) end) RegisterNetEvent("jim_bridge:client:closeMenu", function() closeMenu() end) @@ -75,7 +71,7 @@ RegisterNetEvent("jim_bridge:client:closeMenu", function() closeMenu() end) RegisterNUICallback('clickedButton', function(option) if headerShown then headerShown = false end PlaySound(-1, "CLICK_BACK", "WEB_NAVIGATION_SOUNDS_PHONE", 0, 0, 1) - SetNuiFocus(false) + SetNuiFocus(false, false) if sendData then local data = sendData[tonumber(option)] sendData = nil @@ -102,7 +98,7 @@ RegisterNUICallback('closeMenu', function() headerShown = false sendModifiers = nil sendData = nil - SetNuiFocus(false) + SetNuiFocus(false, false) end) -- Command and Keymapping @@ -112,4 +108,31 @@ RegisterKeyMapping('playerFocus', 'Give Menu Focus', 'keyboard', 'LMENU') -- Exports exports('openMenu', function(data) openNuiMenu(data) end) exports('closeMenu', function() closeMenu() end) -exports('showHeader', function(data) showHeader(data) end) \ No newline at end of file +exports('showHeader', function(data) showHeader(data) end) + + +-- Input Dialog +-- function inputDialog(title, config) +-- local p = promise.new() +-- local cbId = math.random(111111, 999999) +-- +-- RegisterNUICallback("inputResult", function(data, cb) +-- if data.cbId == cbId then +-- cb({}) +-- SetNuiFocus(false, false) +-- p:resolve(data.result) +-- end +-- end) +-- +-- SendNUIMessage({ +-- action = "SHOW_INPUT", +-- title = title, +-- data = config, +-- cbId = cbId +-- }) +-- +-- SetNuiFocus(true, true) +-- return Citizen.Await(p) +-- end +-- +-- exports('inputDialog', inputDialog) \ No newline at end of file diff --git a/nui/script.js b/nui/script.js index e212180..b15110f 100644 --- a/nui/script.js +++ b/nui/script.js @@ -2,14 +2,15 @@ let buttonParams = []; let menuItems = []; const openMenu = (data = null) => { - let html = ` -
- + let html = "
"; + + // Add search as a title-like fixed header + html += ` +
+
`; - html += "
"; - data.forEach((item, index) => { if (!item.hidden) { let header = item.header || item.title; @@ -25,8 +26,9 @@ const openMenu = (data = null) => { }); html += "
"; - $("#buttons").html(html); + $("#container").html(html); + $('.button').click(function() { const target = $(this) if (!target.hasClass('title') && !target.hasClass('disabled')) { @@ -34,10 +36,11 @@ const openMenu = (data = null) => { } }); - $("#search-input").on("keyup", function() { - let value = $(this).val().toLowerCase(); - $("#buttons .button, #buttons .title").filter(function() { - $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1) + $("#search-input").on("input", function() { + const value = $(this).val().toLowerCase(); + $("#buttons .button, #buttons .title").filter(function(index) { + if (index === 0) return; // Skip the search bar itself (first title) + $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1); }); }); }; @@ -54,13 +57,14 @@ const getButtonRender = (header, message = null, id, isMenuHeader, isDisabled, i
${header ? `${header}` : " "}
${message ? `
${message}
` : ""} - ${progress ? ` -
-
-
` - : ""}
+ + ${progress ? ` +
+
+
` + : ""} `; }; @@ -89,16 +93,31 @@ $("#search-input").on('input', function() { filterButtons($(this).val()); }); +document.onkeyup = function (event) { + const charCode = event.key; + if (charCode == "Escape") { + cancelMenu(); + } +}; + +let inputCallbackId = null; + window.addEventListener("message", (event) => { const data = event.data; const buttons = data.data; const action = data.action; + debugLog("Opening Input Popup", data.data); // Log the input config specifically + switch (action) { case "OPEN_MENU": case "SHOW_HEADER": return openMenu(buttons); case "CLOSE_MENU": return closeMenu(); + case "SHOW_INPUT": + inputCallbackId = data.cbId; + debugLog("Opening Input Popup", data.data); // Log the input config specifically + return openInputPopup(data.data); default: return; } @@ -110,3 +129,214 @@ document.onkeyup = function (event) { cancelMenu(); } }; + +const openInputPopup = (config) => { + if (!config || !Array.isArray(config)) { + console.error("Invalid input config: missing or malformed 'fields'", config); + return; + } + const container = document.getElementById("input-popup"); + const form = document.getElementById("input-form"); + form.innerHTML = ""; // clear form + + config.forEach(field => { + let input; + + switch (field.type) { + case "text": + case "number": + input = document.createElement("input"); + input.type = field.type; + break; + case "radio": + input = document.createElement("input"); + input.type = "radio"; + break; + case "select": + input = document.createElement("select"); + field.options.forEach(opt => { + const option = document.createElement("option"); + option.value = opt; + option.textContent = opt; + input.appendChild(option); + }); + break; + case "slider": + input = document.createElement("input"); + input.type = "range"; + input.min = field.min; + input.max = field.max; + input.step = field.step || 1; + break; + case "color": + input = document.createElement("input"); + input.type = "color"; + break; + } + + if (!input) return; + + input.name = field.name; + input.className = "input-field"; + input.placeholder = field.label || field.placeholder || ""; + if (field.required) input.required = true; + + const wrapper = document.createElement("div"); + wrapper.className = "input-wrapper"; + if (field.type === "radio") { + if (field.label) { + const titleLabel = document.createElement("label"); + titleLabel.className = "input-label"; + titleLabel.textContent = field.label; + wrapper.appendChild(titleLabel); + } + + if (Array.isArray(field.options)) { + field.options.forEach(opt => { + const radioWrapper = document.createElement("label"); + radioWrapper.className = "radio-wrapper"; + + const radio = document.createElement("input"); + radio.type = "radio"; + radio.name = field.name; + radio.value = opt.value; + radio.className = "radio-input"; + + // Set default checked radio + if (field.default === opt.value) { + radio.checked = true; + } + + const label = document.createElement("span"); + label.className = "radio-label"; + label.textContent = opt.label || opt.value; + + radioWrapper.appendChild(radio); + radioWrapper.appendChild(label); + wrapper.appendChild(radioWrapper); + }); + } + + form.appendChild(wrapper); + return; + } + + if (field.type === "color") { + if (field.label) { + const titleLabel = document.createElement("label"); + titleLabel.className = "input-label"; + titleLabel.textContent = field.label; + wrapper.appendChild(titleLabel); + } + const colorPreview = document.createElement("span"); + colorPreview.className = "color-preview"; + colorPreview.textContent = input.value; + + input.addEventListener("input", () => { + const hex = input.value; + const rgb = hexToRgb(hex); + colorPreview.textContent = `${hex.toUpperCase()} (${rgb})`; + }); + wrapper.classList.add("color-picker"); + + wrapper.appendChild(input); + wrapper.appendChild(colorPreview); + form.appendChild(wrapper); + return; + } + + if (field.type === "slider") { + if (field.label) { + const titleLabel = document.createElement("label"); + titleLabel.className = "input-label"; + titleLabel.textContent = field.label; + wrapper.appendChild(titleLabel); + } + input.value = field.default || field.min; + const sliderValue = document.createElement("div"); + sliderValue.className = "slider-values"; + sliderValue.innerHTML = ` + ${field.min} + ${input.value} + ${field.max} + `; + + input.addEventListener("input", () => { + sliderValue.querySelector(".slider-current").textContent = input.value; + }); + + wrapper.appendChild(sliderValue); + wrapper.appendChild(input); + form.appendChild(wrapper); + return; + } + + // Common label + const label = document.createElement("label"); + label.className = "input-label"; + label.textContent = field.label || field.name || "Input"; + wrapper.appendChild(label); + + // Input + input.name = field.name; + input.className = "input-field"; + if (field.required) input.required = true; + wrapper.appendChild(input); + + form.appendChild(wrapper); + + form.appendChild(wrapper); + }); + + form.onsubmit = function (e) { + e.preventDefault(); + const data = {}; + new FormData(form).forEach((val, key) => { + // checkboxes return "on" when checked + if (form[key].type === "checkbox") { + data[key] = form[key].checked; + } else if (form[key].type === "color") { + const hex = val; + const rgb = hexToRgb(hex); + data[key] = { + hex: hex.toUpperCase(), + rgb: rgb + }; + } else { + data[key] = val; + } + }); + returnInputData(data); + }; + + document.querySelector(".input-cancel").onclick = () => { + returnInputData(null); + }; + + container.classList.remove("hidden"); +}; + +const closeInputPopup = () => { + document.getElementById("input-popup").classList.add("hidden"); +}; + +const returnInputData = (result) => { + $.post(`https://${GetParentResourceName()}/inputResult`, JSON.stringify({ + cbId: inputCallbackId, + result + })); + closeInputPopup(); +}; + +const debugLog = (label, data) => { + //console.log(`^4[DEBUG] ${label}`); + //console.log(JSON.stringify(data, null, 2)); +}; + +function hexToRgb(hex) { + const bigint = parseInt(hex.slice(1), 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + return `RGB(${r}, ${g}, ${b})`; +} \ No newline at end of file diff --git a/nui/style.css b/nui/style.css index 37ad642..07824c3 100644 --- a/nui/style.css +++ b/nui/style.css @@ -1,9 +1,23 @@ @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;500&display=swap"); :root { - --clr-yes: 8px solid red; --font-family: "Poppins", sans-serif !important; --width: 65%; + + + --text-colour: white; + --text-colour-hover: black; + --background-button: rgba(23, 23, 23, 85%); + --background-button-hover: rgba(200, 200, 200, 85%); + --background-title: rgba(23, 23, 23, 100%); + + /* + --text-colour: black; + --text-colour-hover: white; + --background-button: rgba(200, 200, 200, 85%); + --background-button-hover: rgba(0, 0, 0, 85%); + --background-title: rgba(200, 200, 200); + */ } * { @@ -48,29 +62,47 @@ html, body { background: transparent !important; } body::-webkit-scrollbar { display: none; } .button { - width: auto; - max-width: 55%; - height: 10%; - background-color: rgba(23, 23, 23, 85%); - color: grey; + max-width: 56%; + height: 60%; + background-color: var(--background-button); + color: var(--text-colour); margin: auto; position: relative; top: 10%; overflow: hidden; - padding: 0.35rem; + padding: 0.45rem; display: flex; flex-direction: column; cursor: pointer; - border-left: 8px solid rgba(23, 23, 23, 1%); - transition-property: color, background-color, border-left; - transition-duration: 0.1s, 0.3s; + z-index: 1; + transition-property: color; + transition-duration: 0.1s, 0.2s; transition-timing-function: linear, ease-in; } +.button::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 0%; + height: 100%; + background-color: var(--background-button-hover); + z-index: 0; + transition: width 0.2s ease; +} + +.button:hover::before { + width: 100%; +} + +.button > * { + position: relative; + z-index: 1; +} + .button:hover { - border-left: var(--clr-yes); - background: rgba(0, 0, 0); - color: white; + color: var(--text-colour-hover); } .icon > img { @@ -113,15 +145,14 @@ body::-webkit-scrollbar { display: none; } width: auto; max-width: 56%; height: 60%; - background: rgba(23, 23, 23); - border-left: 8px solid rgba(23, 23, 23, 1%); - color: white; + background: var(--background-title); + color: var(--text-colour); margin: auto; margin-left: -0.5; position: relative; top: 10%; overflow: hidden; - padding: 0.35rem; + padding: 0.45rem; flex-direction: column; border-left: 0px; } @@ -150,34 +181,270 @@ div > .header { position: relative; justify-content: left; overflow: wrap; - color: white; - font-size: 1.2vh; + font-size: 1.3vh; font-weight: 400; overflow: hidden; } /* Search input */ -.search-container { - margin-top: -10px; - margin-bottom: 10px; - position: relative; - left:55%; - z-index: 1; +.title.search-container { + background: var(--background-button); + color: var(--text-colour); + border-top-left-radius: 8px; + border-top-right-radius: 8px; + z-index: 3; } -.search-container input[type="text"] { - color: white; - font-family: var(--font-family); - padding: 0.35rem; - width: 100%; - font-size: 1.0rem; +.title.search-container input[type="text"] { + color: var(--text-colour); + font-family: var(--font-family); + font-size: 1rem; + width: auto; + max-width: 56%; border: none; - border-bottom: 2px solid red; - background-color: rgba(255, 255, 255, 0.2); + background-color: transparent; + transition: background-color 0.2s ease, color 0.2s ease; + outline: none; } -.search-container input[type="text"]:focus { - outline: none; - border-bottom: 2px solid red; + +.title.search-container input[type="text"]::placeholder { + color: var(--text-colour); + opacity: 0.5; +} + +.progress-container { + z-index: 99999; + width: auto; + max-width: 56%; + background-color: var(--background-title); + height: 0.4vh; + padding-left: 0.45rem; + padding-right: 0.45rem; + margin: auto; +} + +.progress-bar { + height: 90%; + max-width: auto; + transition: width 0.3s ease-in-out; +} + + + +.hidden { + display: none !important; +} + +#input-popup { + position: fixed; + top: 0; + left: 0; + height: 100vh; + width: 100vw; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.5); + z-index: 999; +} + +.input-box { + font-weight: 300; + background: var(--background-button); + color: var(--text-colour); + width: 25vw; + box-shadow: 0 0 15px var(--background-button-hover); +} + +.input-header { + font-size: 1.4rem; + margin-bottom: 1rem; + margin-top: 0.5rem; + font-weight: 500; + text-align: center; +} + +.input-actions { + display: flex; + justify-content: space-between; + margin-top: 1rem; +} + +.input-actions button { + padding: 0.5rem 1rem; + border: none; + border-radius: 6px; + margin-bottom: 0.8rem; + margin-left: 0.6rem; + margin-right: 0.6rem; + cursor: pointer; + background: var(--background-button); + color: var(--text-colour); + transition: background 0.2s ease; +} + +.input-actions button:hover { + color: var(--text-colour-hover); + background: var(--background-button-hover); +} + +.input-wrapper { + position: relative; + width: 95%; + padding: 0.1rem; + margin-left: 0.6rem; + margin-bottom: 0.8rem; + background-color: var(--background-title); + overflow: hidden; +} + +.input-wrapper::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 0%; + height: 100%; + background-color: var(--background-button-hover); + z-index: 0; + transition: width 0.2s ease; +} + +.input-wrapper:focus-within::before { + width: 100%; +} + +.input-wrapper:focus-within .input-field { + color: var(--text-colour-hover) !important; +} + +.input-wrapper:focus-within .input-label { + color: var(--text-colour-hover) !important; +} + +.checkbox-wrapper:has(.checkbox-input:focus) .checkbox-label { + color: var(--text-colour-hover) !important; +} + +.input-wrapper:focus-within .color-preview { + color: var(--text-colour-hover) !important; +} + +.input-wrapper:has(input[type="range"]:focus) .slider-values { + color: var(--text-colour-hover) !important; +} + +.input-wrapper:focus-within .slider-current { + color: var(--text-colour-hover) !important; +} + + +.input-field { + width: 100%; + position: relative; + z-index: 1; + border: transparent; + background: transparent; + color: var(--text-colour); + font-size: 1rem; + transition: color 0.2s ease; +} + +.input-wrapper:hover .input-field { + color: var(--text-colour-hover); +} + +.input-label { + display: block; + font-size: 0.9rem; + font-weight: 500; + color: var(--text-colour); + margin-bottom: 0.25rem; + z-index: 1; + position: relative; +} + +.radio-wrapper { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem; + background: var(--background-title); + border-radius: 4px; + z-index: 1; +} + +.radio-input { + transform: scale(1.2); + accent-color: var(--background-button-hover); + z-index: 1; +} + +.radio-label { + color: var(--text-colour); + font-size: 0.95rem; + z-index: 1; +} + +/* Focus styling */ +.radio-wrapper:has(.radio-input:focus) .radio-label { + color: var(--text-colour-hover) !important; +} +.input-wrapper:focus-within .radio-label { + color: var(--text-colour-hover) !important; + } +.input-wrapper.color-picker { + display: flex; + flex-direction: column; /* this is key */ + align-items: flex-start; + justify-content: flex-start; + z-index: 1; +} + +.input-field[type="color"] { + width: 100%; /* half width */ + padding: 0.2rem; + cursor: pointer; + z-index: 1; +} + +.color-preview { + display: inline-block; + margin-left: 0.5rem; + font-size: 0.9rem; + color: var(--text-colour); + z-index: 1; + position: relative; +} + +.slider-values { + z-index: 2; + position: relative; /* <-- this is the key */ + margin-left: 0.6rem; + margin-right: 0.6rem; + display: flex; + justify-content: space-between; + font-size: 0.85rem; + color: var(--text-colour); + margin-bottom: 0.3rem; +} + +.slider-current { + font-weight: bold; + color: var(--text-colour); + z-index: 1; +} + +.input-field select, +.input-field option { + background-color: var(--background-button); + color: var(--text-colour); +} + +.input-field option:hover, +.input-field option:focus { + background-color: var(--background-button-hover); + color: var(--text-colour-hover); } \ No newline at end of file diff --git a/shared/contextmenus.lua b/shared/contextmenus.lua index b4c9a2d..48a0b39 100644 --- a/shared/contextmenus.lua +++ b/shared/contextmenus.lua @@ -3,7 +3,7 @@ --------------------- This module provides a unified function to open menus using the configured menu system. Supported systems include: - • jim-nui (kinda) + • jim_bridge (built-in nui menu that works on any framework) • ox (or ox_context) • qb (using QBMenuExport) • gta (using WarMenu) diff --git a/shared/drawText.lua b/shared/drawText.lua index 17a30fb..62ccb1f 100644 --- a/shared/drawText.lua +++ b/shared/drawText.lua @@ -63,15 +63,6 @@ function drawText(image, input, style, oxStyleTable) end ESX.TextUI(text, nil) - elseif Config.System.drawText == "jim" then - for k, v in pairs(input) do - input[k] = v.."
" - end - exports["jim-nui"]:drawText({ - icon = nil, - text = text, - }) - elseif Config.System.drawText == "red" then -- Concatenate input lines and apply GTA style formatting. for i = 1, #input do diff --git a/shared/notify.lua b/shared/notify.lua index d1b9532..4dc327c 100644 --- a/shared/notify.lua +++ b/shared/notify.lua @@ -70,13 +70,6 @@ function triggerNotify(title, message, type, src) TriggerClientEvent(getScript()..":DisplayESXNotify", src, type, message) end - elseif Config.System.Notify == "jim" then - if not src then - exports["jim-nui"]:Notify(type, message) - else - TriggerClientEvent("jim-nui:client:notify'", src, type, message) - end - elseif Config.System.Notify == "red" then if isStarted("jim-redui") then if not src then