Scripts/resources/[standalone]/menuv/menuv.lua
2024-12-29 21:02:43 +01:00

2487 lines
84 KiB
Lua
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

----------------------- [ MenuV ] -----------------------
-- GitHub: https://github.com/ThymonA/menuv/
-- License: GNU General Public License v3.0
-- https://choosealicense.com/licenses/gpl-3.0/
-- Author: Thymon Arens <contact@arens.io>
-- Name: MenuV
-- Version: 1.4.1
-- Description: FiveM menu library for creating menu's
----------------------- [ MenuV ] -----------------------
local assert = assert
local pairs = assert(pairs)
local rawget = assert(rawget)
local rawset = assert(rawset)
local insert = assert(table.insert)
local remove = assert(table.remove)
local format = assert(string.format)
local upper = assert(string.upper)
local lower = assert(string.lower)
local setmetatable = assert(setmetatable)
--- FiveM globals
local GET_CURRENT_RESOURCE_NAME = assert(GetCurrentResourceName)
local HAS_STREAMED_TEXTURE_DICT_LOADED = assert(HasStreamedTextureDictLoaded)
local REQUEST_STREAMED_TEXTURE_DICT = assert(RequestStreamedTextureDict)
local REGISTER_KEY_MAPPING = assert(RegisterKeyMapping)
local REGISTER_COMMAND = assert(RegisterCommand)
local GET_HASH_KEY = assert(GetHashKey)
local CreateThread = assert(Citizen.CreateThread)
local Wait = assert(Citizen.Wait)
----------------------- [ MenuV ] -----------------------
-- GitHub: https://github.com/ThymonA/menuv/
-- License: GNU General Public License v3.0
-- https://choosealicense.com/licenses/gpl-3.0/
-- Author: Thymon Arens <contact@arens.io>
-- Name: MenuV
-- Version: 1.4.1
-- Description: FiveM menu library for creating menu's
----------------------- [ MenuV ] -----------------------
Config = {
Language = 'da',
HideInterval = 250,
Sounds = {
UP = {
type = 'native',
name = 'NAV_UP_DOWN',
library = 'HUD_FREEMODE_SOUNDSET'
},
DOWN = {
type = 'native',
name = 'NAV_UP_DOWN',
library = 'HUD_FREEMODE_SOUNDSET'
},
LEFT = {
type = 'native',
name = 'NAV_LEFT_RIGHT',
library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
},
RIGHT = {
type = 'native',
name = 'NAV_LEFT_RIGHT',
library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
},
ENTER = {
type = 'native',
name = 'SELECT',
library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
},
CLOSE = {
type = 'native',
name = 'BACK',
library = 'HUD_FRONTEND_DEFAULT_SOUNDSET'
}
}
}
_G.Config = Config
_ENV.Config = Config
----------------------- [ MenuV ] -----------------------
-- GitHub: https://github.com/ThymonA/menuv/
-- License: GNU General Public License v3.0
-- https://choosealicense.com/licenses/gpl-3.0/
-- Author: Thymon Arens <contact@arens.io>
-- Name: MenuV
-- Version: 1.4.1
-- Description: FiveM menu library for creating menu's
----------------------- [ MenuV ] -----------------------
local assert = assert
local type = assert(type)
local tonumber = assert(tonumber)
local tostring = assert(tostring)
local lower = assert(string.lower)
local upper = assert(string.upper)
local sub = assert(string.sub)
local encode = assert(json.encode)
local decode = assert(json.decode)
local floor = assert(math.floor)
local random = assert(math.random)
local randomseed = assert(math.randomseed)
local rawget = assert(rawget)
local setmetatable = assert(setmetatable)
--- FiveM globals
local GET_GAME_TIMER = assert(GetGameTimer)
local GET_CURRENT_RESOURCE_NAME = assert(GetCurrentResourceName)
--- Utilities for MenuV
---@class Utilities
local Utilities = setmetatable({ __class = 'Utilities' }, {})
--- Returns `true` if `input` starts with `start`, otherwise `false`
---@param input string Checks if this string starts with `start`
---@param start string Checks if `input` starts with this
---@return boolean `true` if `input` starts with `start`, otherwise `false`
function Utilities:StartsWith(input, start)
if (self:Typeof(input) ~= 'string') then return false end
if (self:Typeof(start) == 'number') then start = tostring(start) end
if (self:Typeof(start) ~= 'string') then return false end
return sub(input, 1, #start) == start
end
--- Returns `true` if `input` ends with `ends`, otherwise `false`
---@param input string Checks if this string ends with `ends`
---@param ends string Checks if `input` ends with this
---@return boolean `true` if `input` ends with `ends`, otherwise `false`
function Utilities:EndsWith(input, ends)
if (self:Typeof(input) ~= 'string') then return false end
if (self:Typeof(ends) == 'number') then ends = tostring(ends) end
if (self:Typeof(ends) ~= 'string') then return false end
return sub(input, -#ends) == ends
end
--- Returns the type of given `input`
---@param input any Any input
---@return string Type of given input
function Utilities:Typeof(input)
if (input == nil) then return 'nil' end
local rawType = type(input) or 'nil'
if (rawType ~= 'table') then return rawType end
local isFXFunction = rawget(input, '__cfx_functionReference') ~= nil or
rawget(input, '__cfx_async_retval') ~= nil
if (isFXFunction) then return 'function' end
if (rawget(input, '__cfx_functionSource') ~= nil) then return 'number' end
local rawClass = rawget(input, '__class')
if (rawClass ~= nil) then return type(rawClass) == 'string' and rawClass or 'class' end
local rawTableType = rawget(input, '__type')
if (rawTableType ~= nil) then return type(rawTableType) == 'string' and rawTableType or 'table' end
return rawType
end
local INPUT_GROUPS = {
[0] = "KEYBOARD",
[2] = "CONTROLLER"
}
local INPUT_TYPE_GROUPS = {
["KEYBOARD"] = 0,
["MOUSE_ABSOLUTEAXIS"] = 0,
["MOUSE_CENTEREDAXIS"] = 0,
["MOUSE_RELATIVEAXIS"] = 0,
["MOUSE_SCALEDAXIS"] = 0,
["MOUSE_NORMALIZED"] = 0,
["MOUSE_WHEEL"] = 0,
["MOUSE_BUTTON"] = 0,
["MOUSE_BUTTONANY"] = 0,
["MKB_AXIS"] = 0,
["PAD_AXIS"] = 2,
["PAD_DIGITALBUTTON"] = 2,
["PAD_DIGITALBUTTONANY"] = 2,
["PAD_ANALOGBUTTON"] = 2,
["JOYSTICK_POV"] = 2,
["JOYSTICK_POV_AXIS"] = 2,
["JOYSTICK_BUTTON"] = 2,
["JOYSTICK_AXIS"] = 2,
["JOYSTICK_IAXIS"] = 2,
["JOYSTICK_AXIS_NEGATIVE"] = 2,
["JOYSTICK_AXIS_POSITIVE"] = 2,
["PAD_DEBUGBUTTON"] = 2,
["GAME_CONTROLLED"] = 2,
["DIGITALBUTTON_AXIS"] = 2,
}
function Utilities:GetInputTypeGroup(inputType)
return INPUT_TYPE_GROUPS[inputType] or 0
end
function Utilities:GetInputGroupName(inputTypeGroup)
return INPUT_GROUPS[inputTypeGroup] or "KEYBOARD"
end
--- Transform any `input` to the same type as `defaultValue`
---@type function
---@param input any Transform this `input` to `defaultValue`'s type
---@param defaultValue any Returns this if `input` can't transformed to this type
---@param ignoreDefault boolean Don't return default value if this is true
---@return any Returns `input` matches the `defaultValue` type or `defaultValue`
function Utilities:Ensure(input, defaultValue, ignoreDefault)
ignoreDefault = type(ignoreDefault) == 'boolean' and ignoreDefault or false
if (defaultValue == nil) then return nil end
local requiredType = self:Typeof(defaultValue)
if (requiredType == 'nil') then return nil end
local inputType = self:Typeof(input)
if (inputType == requiredType) then return input end
if (inputType == 'nil') then return defaultValue end
if (requiredType == 'number') then
if (inputType == 'boolean') then return input and 1 or 0 end
return tonumber(input) or (not ignoreDefault and defaultValue or nil)
end
if (requiredType == 'string') then
if (inputType == 'boolean') then return input and 'yes' or 'no' end
if (inputType == 'vector3') then return encode({ x = input.x, y = input.y, z = input.z }) or (not ignoreDefault and defaultValue or nil) end
if (inputType == 'vector2') then return encode({ x = input.x, y = input.y }) or (not ignoreDefault and defaultValue or nil) end
if (inputType == 'table') then return encode(input) or (not ignoreDefault and defaultValue or nil) end
local result = tostring(input)
if (result == 'nil') then
return not ignoreDefault and defaultValue or 'nil'
end
return result
end
if (requiredType == 'boolean') then
if (inputType == 'string') then
input = lower(input)
if (input == 'true' or input == '1' or input == 'yes' or input == 'y') then return true end
if (input == 'false' or input == '0' or input == 'no' or input == 'n') then return false end
return (not ignoreDefault and defaultValue or nil)
end
if (inputType == 'number') then
if (input == 1) then return true end
if (input == 0) then return false end
return (not ignoreDefault and defaultValue or nil)
end
return (not ignoreDefault and defaultValue or nil)
end
if (requiredType == 'table') then
if (inputType == 'string') then
if (self:StartsWith(input, '{') and self:EndsWith(input, '}')) then
return decode(input) or (not ignoreDefault and defaultValue or nil)
end
if (self:StartsWith(input, '[') and self:EndsWith(input, ']')) then
return decode(input) or (not ignoreDefault and defaultValue or nil)
end
return (not ignoreDefault and defaultValue or nil)
end
if (inputType == 'vector3') then return { x = input.x or 0, y = input.y or 0, z = input.z or 0 } end
if (inputType == 'vector2') then return { x = input.x or 0, y = input.y or 0 } end
return (not ignoreDefault and defaultValue or nil)
end
if (requiredType == 'vector3') then
if (inputType == 'table') then
local _x = self:Ensure(input.x, defaultValue.x)
local _y = self:Ensure(input.y, defaultValue.y)
local _z = self:Ensure(input.z, defaultValue.z)
return vector3(_x, _y, _z)
end
if (inputType == 'vector2') then
local _x = self:Ensure(input.x, defaultValue.x)
local _y = self:Ensure(input.y, defaultValue.y)
return vector3(_x, _y, 0)
end
if (inputType == 'number') then
return vector3(input, input, input)
end
return (not ignoreDefault and defaultValue or nil)
end
if (requiredType == 'vector2') then
if (inputType == 'table') then
local _x = self:Ensure(input.x, defaultValue.x)
local _y = self:Ensure(input.y, defaultValue.y)
return vector2(_x, _y)
end
if (inputType == 'vector3') then
local _x = self:Ensure(input.x, defaultValue.x)
local _y = self:Ensure(input.y, defaultValue.y)
return vector2(_x, _y)
end
if (inputType == 'number') then
return vector2(input, input)
end
return (not ignoreDefault and defaultValue or nil)
end
return (not ignoreDefault and defaultValue or nil)
end
--- Checks if input exists in inputs
--- '0' and 0 are both the same '0' == 0 equals `true`
--- 'yes' and true are both the same 'yes' == true equals `true`
---@param input any Any input
---@param inputs any[] Any table
---@param checkType string | "'value'" | "'key'" | "'both'"
---@return boolean Returns `true` if input has been found as `key` and/or `value`
function Utilities:Any(input, inputs, checkType)
if (input == nil) then return false end
if (inputs == nil) then return false end
inputs = self:Ensure(inputs, {})
checkType = lower(self:Ensure(checkType, 'value'))
local checkMethod = 1
if (checkType == 'value' or checkType == 'v') then
checkMethod = 1
elseif (checkType == 'key' or checkType == 'k') then
checkMethod = -1
elseif (checkType == 'both' or checkType == 'b') then
checkMethod = 0
end
for k, v in pairs(inputs) do
if (checkMethod == 0 or checkMethod == -1) then
local checkK = self:Ensure(input, k, true)
if (checkK ~= nil and checkK == k) then return true end
end
if (checkMethod == 0 or checkMethod == 1) then
local checkV = self:Ensure(input, v, true)
if (checkV ~= nil and checkV == v) then return true end
end
end
return false
end
--- Round any `value`
---@param value number Round this value
---@param decimal number Number of decimals
---@return number Rounded number
function Utilities:Round(value, decimal)
value = self:Ensure(value, 0)
decimal = self:Ensure(decimal, 0)
if (decimal > 0) then
return floor((value * 10 ^ decimal) + 0.5) / (10 ^ decimal)
end
return floor(value + 0.5)
end
--- Checks if `item1` equals `item2`
---@param item1 any Item1
---@param item2 any Item2
---@return boolean `true` if both are equal, otherwise `false`
function Utilities:Equal(item1, item2)
if (item1 == nil and item2 == nil) then return true end
if (item1 == nil or item2 == nil) then return false end
if (type(item1) == 'table') then
local item1EQ = rawget(item1, '__eq')
if (item1EQ ~= nil and self:Typeof(item1EQ) == 'function') then
return item1EQ(item1, item2)
end
return item1 == item2
end
if (type(item2) == 'table') then
local item2EQ = rawget(item2, '__eq')
if (item2EQ ~= nil and self:Typeof(item2EQ) == 'function') then
return item2EQ(item2, item1)
end
return item2 == item1
end
return item1 == item2
end
local function tohex(x)
x = Utilities:Ensure(x, 32)
local s, base, d = '', 16
while x > 0 do
d = x % base + 1
x = floor(x / base)
s = sub('0123456789abcdef', d, d) .. s
end
while #s < 2 do s = ('0%s'):format(s) end
return s
end
local function bitwise(x, y, matrix)
x = Utilities:Ensure(x, 32)
y = Utilities:Ensure(y, 16)
matrix = Utilities:Ensure(matrix, {{0,0}, {0, 1}})
local z, pow = 0, 1
while x > 0 or y > 0 do
z = z + (matrix[x %2 + 1][y %2 + 1] * pow)
pow = pow * 2
x = floor(x / 2)
y = floor(y / 2)
end
return z
end
--- Generates a random UUID like: 00000000-0000-0000-0000-000000000000
---@return string Random generated UUID
function Utilities:UUID()
randomseed(GET_GAME_TIMER() + random(30720, 92160))
---@type number[]
local bytes = {
random(0, 255),
random(0, 255),
random(0, 255),
random(0, 255),
random(0, 255),
random(0, 255),
random(0, 255),
random(0, 255),
random(0, 255),
random(0, 255),
random(0, 255),
random(0, 255),
random(0, 255),
random(0, 255),
random(0, 255),
random(0, 255)
}
bytes[7] = bitwise(bytes[7], 0x0f, {{0,0},{0,1}})
bytes[7] = bitwise(bytes[7], 0x40, {{0,1},{1,1}})
bytes[9] = bitwise(bytes[7], 0x3f, {{0,0},{0,1}})
bytes[9] = bitwise(bytes[7], 0x80, {{0,1},{1,1}})
return upper(('%s%s%s%s-%s%s-%s%s-%s%s-%s%s%s%s%s%s'):format(
tohex(bytes[1]), tohex(bytes[2]), tohex(bytes[3]), tohex(bytes[4]),
tohex(bytes[5]), tohex(bytes[6]),
tohex(bytes[7]), tohex(bytes[8]),
tohex(bytes[9]), tohex(bytes[10]),
tohex(bytes[11]), tohex(bytes[12]), tohex(bytes[13]), tohex(bytes[14]), tohex(bytes[15]), tohex(bytes[16])
))
end
--- Replace a string that contains `this` to `that`
---@param str string String where to replace in
---@param this string Word that's need to be replaced
---@param that string Replace `this` whit given string
---@return string String where `this` has been replaced with `that`
function Utilities:Replace(str, this, that)
local b, e = str:find(this, 1, true)
if b == nil then
return str
else
return str:sub(1, b - 1) .. that .. self:Replace(str:sub(e + 1), this, that)
end
end
_G.Utilities = Utilities
_ENV.Utilities = Utilities
----------------------- [ MenuV ] -----------------------
-- GitHub: https://github.com/ThymonA/menuv/
-- License: GNU General Public License v3.0
-- https://choosealicense.com/licenses/gpl-3.0/
-- Author: Thymon Arens <contact@arens.io>
-- Name: MenuV
-- Version: 1.4.1
-- Description: FiveM menu library for creating menu's
----------------------- [ MenuV ] -----------------------
local assert = assert
---@type Utilities
local U = assert(Utilities)
local type = assert(type)
local pairs = assert(pairs)
local lower = assert(string.lower)
local upper = assert(string.upper)
local sub = assert(string.sub)
local pack = assert(table.pack)
local unpack = assert(table.unpack)
local insert = assert(table.insert)
local rawset = assert(rawset)
local rawget = assert(rawget)
local setmetatable = assert(setmetatable)
--- FiveM globals
local CreateThread = assert(Citizen.CreateThread)
--- Create a new menu item
---@param info table Menu information
---@return Item New item
function CreateMenuItem(info)
info = U:Ensure(info, {})
local item = {
---@type Menu|nil
__menu = U:Ensure(info.__Menu or info.__menu, { __class = 'Menu', __type = 'Menu' }, true) or nil,
---@type string
__event = U:Ensure(info.PrimaryEvent or info.primaryEvent, 'unknown'),
---@type string
UUID = U:UUID(),
---@type string
Icon = U:Ensure(info.Icon or info.icon, 'none'),
---@type string
Label = U:Ensure(info.Label or info.label, ''),
---@type string
Description = U:Ensure(info.Description or info.description, ''),
---@type any
Value = info.Value or info.value,
---@type table[]
Values = {},
---@type number
Min = U:Ensure(info.Min or info.min, 0),
---@type number
Max = U:Ensure(info.Max or info.max, 0),
---@type boolean
Disabled = U:Ensure(info.Disabled or info.disabled, false),
---@type table
Events = U:Ensure(info.Events or info.events, {}),
---@type boolean
SaveOnUpdate = U:Ensure(info.SaveOnUpdate or info.saveOnUpdate, false),
---@param t Item
---@param event string Name of Event
Trigger = function(t, event, ...)
event = lower(U:Ensure(event, 'unknown'))
if (event == 'unknown') then return end
if (U:StartsWith(event, 'on')) then
event = 'On' .. sub(event, 3):gsub('^%l', upper)
else
event = 'On' .. event:gsub('^%l', upper)
end
if (not U:Any(event, (t.Events or {}), 'key')) then
return
end
local args = pack(...)
for _, v in pairs(t.Events[event]) do
CreateThread(function()
v(t, unpack(args))
end)
end
end,
---@param t Item
---@param event string Name of event
---@param func function|Menu Function or Menu to trigger
On = function(t, event, func)
event = lower(U:Ensure(event, 'unknown'))
if (event == 'unknown') then return end
if (U:StartsWith(event, 'on')) then
event = 'On' .. sub(event, 3):gsub('^%l', upper)
else
event = 'On' .. event:gsub('^%l', upper)
end
if (not U:Any(event, (t.Events or {}), 'key')) then
return
end
local _type = U:Typeof(func)
if (_type == 'Menu') then
local menu_t = {
__class = 'function',
__type = 'function',
func = function(t) MenuV:OpenMenu(t.uuid) end,
uuid = func.UUID or func.uuid or U:UUID()
}
local menu_mt = { __index = menu_t, __call = function(t) t:func() end }
local menu_item = setmetatable(menu_t, menu_mt)
insert(t.Events[event], menu_item)
return
end
func = U:Ensure(func, function() end)
insert(t.Events[event], func)
end,
---@param t Item
---@param k string
---@param v string
Validate = U:Ensure(info.Validate or info.validate, function(t, k, v)
return true
end),
---@param t Item
---@param k string
---@param v string
Parser = U:Ensure(info.Parser or info.parser, function(t, k, v)
return v
end),
---@param t Item
---@param k string
---@param v string
NewIndex = U:Ensure(info.NewIndex or info.newIndex, function(t, k, v)
end),
---@param t Item
---@return any
GetValue = function(t)
local itemType = U:Ensure(t.__type, 'unknown')
if (itemType == 'button' or itemType == 'menu' or itemType == 'unknown') then
return t.Value
end
if (itemType == 'checkbox' or itemType == 'confirm') then
return U:Ensure(t.Value, false)
end
if (itemType == 'slider') then
for _, item in pairs(t.Values) do
if (item.Value == t.Value) then
return item.Value
end
end
return nil
end
if (itemType == 'range') then
local rawValue = U:Ensure(t.Value, 0)
if (t.Min > rawValue) then
return t.Min
end
if (t.Max < rawValue) then
return t.Max
end
return rawValue
end
end,
---@return Menu|nil
GetParentMenu = function(t)
return t.__menu or nil
end
}
item.Events.OnEnter = {}
item.Events.OnLeave = {}
item.Events.OnUpdate = {}
item.Events.OnDestroy = {}
local mt = {
__index = function(t, k)
return rawget(t.data, k)
end,
__tostring = function(t)
return t.UUID
end,
__call = function(t, ...)
if (t.Trigger ~= nil and type(t.Trigger) == 'function') then
t:Trigger(t.__event, ...)
end
end,
__newindex = function(t, k, v)
local key = U:Ensure(k, 'unknown')
local oldValue = rawget(t.data, k)
local checkInput = t.Validate ~= nil and type(t.Validate) == 'function'
local inputParser = t.Parser ~= nil and type(t.Parser) == 'function'
local updateIndexTrigger = t.NewIndex ~= nil and type(t.NewIndex) == 'function'
if (checkInput) then
local result = t:Validate(key, v)
result = U:Ensure(result, true)
if (not result) then
return
end
end
if (inputParser) then
local parsedValue = t:Parser(key, v)
v = parsedValue or v
end
rawset(t.data, k, v)
if (updateIndexTrigger) then
t:NewIndex(key, v)
end
if (t.__menu ~= nil and U:Typeof(t.__menu) == 'Menu' and t.__menu.Trigger ~= nil and U:Typeof( t.__menu.Trigger) == 'function') then
t.__menu:Trigger('update', 'UpdateItem', t)
end
if (key == 'Value' and t.Trigger ~= nil and type(t.Trigger) == 'function') then
t:Trigger('update', key, v, oldValue)
end
end,
__metatable = 'MenuV'
}
---@class Item
---@filed private __event string Name of primary event
---@field public UUID string UUID of Item
---@field public Icon string Icon/Emoji for Item
---@field public Label string Label of Item
---@field public Description string Description of Item
---@field public Value any Value of Item
---@field public Values table[] List of values
---@field public Min number Min range value
---@field public Max number Max range value
---@field public Disabled boolean Disabled state of Item
---@field public SaveOnUpdate boolean Save on `update`
---@field private Events table<string, function[]> List of registered `on` events
---@field public Trigger fun(t: Item, event: string)
---@field public On fun(t: Item, event: string, func: function|Menu)
---@field public Validate fun(t: Item, k: string, v:any)
---@field public NewIndex fun(t: Item, k: string, v: any)
---@field public Parser fun(t: Item, k: string, v: any)
---@field public GetValue fun(t: Item):any
---@field public GetParentMenu func(t: Item):Menu|nil
local i = setmetatable({ data = item, __class = 'Item', __type = U:Ensure(info.Type or info.type, 'unknown') }, mt)
for k, v in pairs(info or {}) do
local key = U:Ensure(k, 'unknown')
if (key == 'unknown') then return end
i:On(key, v)
end
return i
end
_ENV.CreateMenuItem = CreateMenuItem
_G.CreateMenuItem = CreateMenuItem
----------------------- [ MenuV ] -----------------------
-- GitHub: https://github.com/ThymonA/menuv/
-- License: GNU General Public License v3.0
-- https://choosealicense.com/licenses/gpl-3.0/
-- Author: Thymon Arens <contact@arens.io>
-- Name: MenuV
-- Version: 1.4.1
-- Description: FiveM menu library for creating menu's
----------------------- [ MenuV ] -----------------------
local assert = assert
---@type Utilities
local U = assert(Utilities)
local type = assert(type)
local next = assert(next)
local pairs = assert(pairs)
local ipairs = assert(ipairs)
local lower = assert(string.lower)
local upper = assert(string.upper)
local sub = assert(string.sub)
local insert = assert(table.insert)
local remove = assert(table.remove)
local pack = assert(table.pack)
local unpack = assert(table.unpack)
local encode = assert(json.encode)
local rawset = assert(rawset)
local rawget = assert(rawget)
local setmetatable = assert(setmetatable)
--- FiveM globals
local GET_CURRENT_RESOURCE_NAME = assert(GetCurrentResourceName)
local GET_INVOKING_RESOURCE = assert(GetInvokingResource)
local HAS_STREAMED_TEXTURE_DICT_LOADED = assert(HasStreamedTextureDictLoaded)
local REQUEST_STREAMED_TEXTURE_DICT = assert(RequestStreamedTextureDict)
--- MenuV local variable
local current_resource = GET_CURRENT_RESOURCE_NAME()
--- Returns default empty table for items
---@returns items
function CreateEmptyItemsTable(data)
data = U:Ensure(data, {})
data.ToTable = function(t)
local tempTable = {}
local index = 0
for _, option in pairs(t) do
index = index + 1
tempTable[index] = {
index = index,
type = option.__type,
uuid = U:Ensure(option.UUID, 'unknown'),
icon = U:Ensure(option.Icon, 'none'),
label = U:Ensure(option.Label, 'Unknown'),
description = U:Ensure(option.Description, ''),
value = 'none',
values = {},
min = U:Ensure(option.Min, 0),
max = U:Ensure(option.Max, 0),
disabled = U:Ensure(option.Disabled, false)
}
if (option.__type == 'button' or option.__type == 'menu') then
tempTable[index].value = 'none'
elseif (option.__type == 'checkbox' or option.__type == 'confirm') then
tempTable[index].value = U:Ensure(option.Value, false)
elseif (option.__type == 'range') then
tempTable[index].value = U:Ensure(option.Value, 0)
if (tempTable[index].value <= tempTable[index].min) then
tempTable[index].value = tempTable[index].min
elseif (tempTable[index].value >= tempTable[index].max) then
tempTable[index].value = tempTable[index].max
end
elseif (option.__type == 'slider') then
tempTable[index].value = 0
end
local _values = U:Ensure(option.Values, {})
local vIndex = 0
for valueIndex, value in pairs(_values) do
vIndex = vIndex + 1
tempTable[index].values[vIndex] = {
label = U:Ensure(value.Label, 'Option'),
description = U:Ensure(value.Description, ''),
value = vIndex
}
if (option.__type == 'slider') then
if (U:Ensure(option.Value, 0) == valueIndex) then
tempTable[index].value = (valueIndex - 1)
end
end
end
end
return tempTable
end
data.ItemToTable = function(t, i)
local tempTable = {}
local index = 0
local uuid = U:Typeof(i) == 'Item' and i.UUID or U:Ensure(i, '00000000-0000-0000-0000-000000000000')
for _, option in pairs(t) do
index = index + 1
if (option.UUID == uuid) then
tempTable = {
index = index,
type = option.__type,
uuid = U:Ensure(option.UUID, 'unknown'),
icon = U:Ensure(option.Icon, 'none'),
label = U:Ensure(option.Label, 'Unknown'),
description = U:Ensure(option.Description, ''),
value = 'none',
values = {},
min = U:Ensure(option.Min, 0),
max = U:Ensure(option.Max, 0),
disabled = U:Ensure(option.Disabled, false)
}
if (option.__type == 'button' or option.__type == 'menu') then
tempTable.value = 'none'
elseif (option.__type == 'checkbox' or option.__type == 'confirm') then
tempTable.value = U:Ensure(option.Value, false)
elseif (option.__type == 'range') then
tempTable.value = U:Ensure(option.Value, 0)
if (tempTable.value <= tempTable.min) then
tempTable.value = tempTable.min
elseif (tempTable.value >= tempTable.max) then
tempTable.value = tempTable.max
end
elseif (option.__type == 'slider') then
tempTable.value = 0
end
local _values = U:Ensure(option.Values, {})
local vIndex = 0
for valueIndex, value in pairs(_values) do
vIndex = vIndex + 1
tempTable.values[vIndex] = {
label = U:Ensure(value.Label, 'Option'),
description = U:Ensure(value.Description, ''),
value = vIndex
}
if (option.__type == 'slider') then
if (U:Ensure(option.Value, 0) == valueIndex) then
tempTable.value = (valueIndex - 1)
end
end
end
return tempTable
end
end
return tempTable
end
data.AddItem = function(t, item)
if (U:Typeof(item) == 'Item') then
local newIndex = #(U:Ensure(rawget(t, 'data'), {})) + 1
rawset(t.data, newIndex, item)
if (t.Trigger ~= nil and type(t.Trigger) == 'function') then
t:Trigger('update', 'AddItem', item)
end
end
return U:Ensure(rawget(t, 'data'), {})
end
local item_pairs = function(t, k)
local _k, _v = next((rawget(t, 'data') or {}), k)
if (_v ~= nil and type(_v) ~= 'table') then
return item_pairs(t, _k)
end
return _k, _v
end
local item_ipairs = function(t, k)
local _k, _v = next((rawget(t, 'data') or {}), k)
if (_v ~= nil and (type(_v) ~= 'table' or type(_k) ~= 'number')) then
return item_ipairs(t, _k)
end
return _k, _v
end
_G.item_pairs = item_pairs
_ENV.item_pairs = item_pairs
---@class items
return setmetatable({ data = data, Trigger = nil }, {
__index = function(t, k)
return rawget(t.data, k)
end,
__newindex = function(t, k, v)
local oldValue = rawget(t.data, k)
rawset(t.data, k, v)
if (t.Trigger ~= nil and type(t.Trigger) == 'function') then
if (oldValue == nil) then
t:Trigger('update', 'AddItem', v)
elseif (oldValue ~= nil and v == nil) then
t:Trigger('update', 'RemoveItem', oldValue)
elseif (oldValue ~= v) then
t:Trigger('update', 'UpdateItem', v, oldValue)
end
end
end,
__call = function(t, func)
rawset(t, 'Trigger', U:Ensure(func, function() end))
end,
__pairs = function(t)
local k = nil
return function()
local v
k, v = item_pairs(t, k)
return k, v
end, t, nil
end,
__ipairs = function(t)
local k = nil
return function()
local v
k, v = item_ipairs(t, k)
return k, v
end, t, 0
end,
__len = function(t)
local items = U:Ensure(rawget(t, 'data'), {})
local itemCount = 0
for _, v in pairs(items) do
if (U:Typeof(v) == 'Item') then
itemCount = itemCount + 1
end
end
return itemCount
end
})
end
--- Load a texture dictionary if not already loaded
---@param textureDictionary string Name of texture dictionary
local function LoadTextureDictionary(textureDictionary)
textureDictionary = U:Ensure(textureDictionary, 'menuv')
if (HAS_STREAMED_TEXTURE_DICT_LOADED(textureDictionary)) then return end
REQUEST_STREAMED_TEXTURE_DICT(textureDictionary, true)
end
--- Create a new menu item
---@param info table Menu information
---@return Menu New item
function CreateMenu(info)
info = U:Ensure(info, {})
local namespace = U:Ensure(info.Namespace or info.namespace, 'unknown')
local namespace_available = MenuV:IsNamespaceAvailable(namespace)
if (not namespace_available) then
error(("[MenuV] Namespace '%s' is already taken, make sure it is unique."):format(namespace))
end
local theme = lower(U:Ensure(info.Theme or info.theme, 'default'))
if (theme ~= 'default' and theme ~= 'native') then
theme = 'default'
end
if (theme == 'native') then
info.R, info.G, info.B = 255, 255, 255
info.r, info.g, info.b = 255, 255, 255
end
local item = {
---@type string
Namespace = namespace,
---@type boolean
IsOpen = false,
---@type string
UUID = U:UUID(),
---@type string
Title = not (info.Title or info.title) and ' ' or U:Ensure(info.Title or info.title, 'MenuV'),
---@type string
Subtitle = U:Ensure(info.Subtitle or info.subtitle, ''),
---@type string | "'topleft'" | "'topcenter'" | "'topright'" | "'centerleft'" | "'center'" | "'centerright'" | "'bottomleft'" | "'bottomcenter'" | "'bottomright'"
Position = U:Ensure(info.Position or info.position, 'topleft'),
---@type table
Color = {
R = U:Ensure(info.R or info.r, 0),
G = U:Ensure(info.G or info.g, 0),
B = U:Ensure(info.B or info.b, 255)
},
---@type string | "'size-100'" | "'size-110'" | "'size-125'" | "'size-150'" | "'size-175'" | "'size-200'"
Size = U:Ensure(info.Size or info.size, 'size-110'),
---@type string
Dictionary = U:Ensure(info.Dictionary or info.dictionary, 'menuv'),
---@type string
Texture = U:Ensure(info.Texture or info.texture, 'default'),
---@type table
Events = U:Ensure(info.Events or info.events, {}),
---@type string
Theme = theme,
---@type Item[]
Items = CreateEmptyItemsTable({}),
---@param t Menu
---@param event string Name of Event
Trigger = function(t, event, ...)
event = lower(U:Ensure(event, 'unknown'))
if (event == 'unknown') then return end
if (U:StartsWith(event, 'on')) then
event = 'On' .. sub(event, 3):gsub('^%l', upper)
else
event = 'On' .. event:gsub('^%l', upper)
end
if (not U:Any(event, (t.Events or {}), 'key')) then
return
end
if (event == 'OnOpen') then rawset(t, 'IsOpen', true)
elseif (event == 'OnClose') then rawset(t, 'IsOpen', false) end
local args = pack(...)
for _, v in pairs(t.Events[event]) do
if (type(v) == 'table' and U:Typeof(v.func) == 'function') then
CreateThread(function()
if (event == 'OnClose') then
v.func(t, unpack(args))
else
local threadId = coroutine.running()
if (threadId ~= nil) then
insert(t.data.Threads, threadId)
v.func(t, unpack(args))
for i = 0, #(t.data.Threads or {}), 1 do
if (t.data.Threads[i] == threadId) then
remove(t.data.Threads, i)
return
end
end
end
end
end)
end
end
end,
---@type thread[]
Threads = {},
---@param t Menu
DestroyThreads = function(t)
for _, threadId in pairs(t.data.Threads or {}) do
local threadStatus = coroutine.status(threadId)
if (threadStatus ~= nil and threadStatus ~= 'dead') then
coroutine.close(threadId)
end
end
t.data.Threads = {}
end,
---@param t Menu
---@param event string Name of event
---@param func function|Menu Function or Menu to trigger
---@return string UUID of event
On = function(t, event, func)
local ir = GET_INVOKING_RESOURCE()
local resource = U:Ensure(ir, current_resource)
event = lower(U:Ensure(event, 'unknown'))
if (event == 'unknown') then return end
if (U:StartsWith(event, 'on')) then
event = 'On' .. sub(event, 3):gsub('^%l', upper)
else
event = 'On' .. event:gsub('^%l', upper)
end
if (not U:Any(event, (t.Events or {}), 'key')) then
return
end
func = U:Ensure(func, function() end)
local uuid = U:UUID()
insert(t.Events[event], {
__uuid = uuid,
__resource = resource,
func = func
})
return uuid
end,
---@param t Menu
---@param event string Name of event
---@param uuid string UUID of event
RemoveOnEvent = function(t, event, uuid)
local ir = GET_INVOKING_RESOURCE()
local resource = U:Ensure(ir, current_resource)
event = lower(U:Ensure(event, 'unknown'))
if (event == 'unknown') then return end
if (U:StartsWith(event, 'on')) then
event = 'On' .. sub(event, 3):gsub('^%l', upper)
else
event = 'On' .. event:gsub('^%l', upper)
end
if (not U:Any(event, (t.Events or {}), 'key')) then
return
end
uuid = U:Ensure(uuid, '00000000-0000-0000-0000-000000000000')
for i = 1, #t.Events[event], 1 do
if (t.Events[event][i] ~= nil and
t.Events[event][i].__uuid == uuid and
t.Events[event][i].__resource == resource) then
remove(t.Events[event], i)
end
end
end,
---@param t Item
---@param k string
---@param v string
Validate = U:Ensure(info.Validate or info.validate, function(t, k, v)
return true
end),
---@param t Item
---@param k string
---@param v string
Parser = function(t, k, v)
if (k == 'Position' or k == 'position') then
local position = lower(U:Ensure(v, 'topleft'))
if (U:Any(position, {'topleft', 'topcenter', 'topright', 'centerleft', 'center', 'centerright', 'bottomleft', 'bottomcenter', 'bottomright'}, 'value')) then
return position
else
return 'topleft'
end
end
return v
end,
---@param t Item
---@param k string
---@param v string
NewIndex = U:Ensure(info.NewIndex or info.newIndex, function(t, k, v)
end),
---@type function
---@param t Menu MenuV menu
---@param info table Information about button
---@return Item New item
AddButton = function(t, info)
info = U:Ensure(info, {})
info.Type = 'button'
info.Events = { OnSelect = {} }
info.PrimaryEvent = 'OnSelect'
info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
info.__menu = t
if (U:Typeof(info.Value or info.value) == 'Menu') then
info.Type = 'menu'
end
local item = CreateMenuItem(info)
if (info.Type == 'menu') then
item:On('select', function() item.Value() end)
end
if (info.TriggerUpdate) then
t.Items:AddItem(item)
else
local items = rawget(t.data, 'Items')
if (items) then
local newIndex = #items + 1
rawset(items.data, newIndex, item)
return items.data[newIndex] or item
end
end
return t.Items[#t.Items] or item
end,
---@type function
---@param t Menu MenuV menu
---@param info table Information about checkbox
---@return Item New item
AddCheckbox = function(t, info)
info = U:Ensure(info, {})
info.Type = 'checkbox'
info.Value = U:Ensure(info.Value or info.value, false)
info.Events = { OnChange = {}, OnCheck = {}, OnUncheck = {} }
info.PrimaryEvent = 'OnCheck'
info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
info.__menu = t
info.NewIndex = function(t, k, v)
if (k == 'Value') then
local value = U:Ensure(v, false)
if (value) then
t:Trigger('check', t)
else
t:Trigger('uncheck', t)
end
end
end
local item = CreateMenuItem(info)
if (info.TriggerUpdate) then
t.Items:AddItem(item)
else
local items = rawget(t.data, 'Items')
if (items) then
local newIndex = #items + 1
rawset(items.data, newIndex, item)
return items.data[newIndex] or item
end
end
return t.Items[#t.Items] or item
end,
---@type function
---@param t Menu MenuV menu
---@param info table Information about slider
---@return SliderItem New slider item
AddSlider = function(t, info)
info = U:Ensure(info, {})
info.Type = 'slider'
info.Events = { OnChange = {}, OnSelect = {} }
info.PrimaryEvent = 'OnSelect'
info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
info.__menu = t
---@class SliderItem : Item
---@filed private __event string Name of primary event
---@field public UUID string UUID of Item
---@field public Icon string Icon/Emoji for Item
---@field public Label string Label of Item
---@field public Description string Description of Item
---@field public Value any Value of Item
---@field public Values table[] List of values
---@field public Min number Min range value
---@field public Max number Max range value
---@field public Disabled boolean Disabled state of Item
---@field private Events table<string, function[]> List of registered `on` events
---@field public Trigger fun(t: Item, event: string)
---@field public On fun(t: Item, event: string, func: function)
---@field public Validate fun(t: Item, k: string, v:any)
---@field public NewIndex fun(t: Item, k: string, v: any)
---@field public GetValue fun(t: Item):any
---@field public AddValue fun(t: Item, info: table)
---@field public AddValues fun(t: Item)
local item = CreateMenuItem(info)
--- Add a value to slider
---@param info table Information about slider
function item:AddValue(info)
info = U:Ensure(info, {})
local value = {
Label = U:Ensure(info.Label or info.label, 'Value'),
Description = U:Ensure(info.Description or info.description, ''),
Value = info.Value or info.value
}
insert(self.Values, value)
end
--- Add values to slider
---@vararg table[] List of values
function item:AddValues(...)
local arguments = pack(...)
for _, argument in pairs(arguments) do
if (U:Typeof(argument) == 'table') then
local hasIndex = argument[1] or nil
if (hasIndex and U:Typeof(hasIndex) == 'table') then
self:AddValues(unpack(argument))
else
self:AddValue(argument)
end
end
end
end
local values = U:Ensure(info.Values or info.values, {})
if (#values > 0) then
item:AddValues(values)
end
if (info.TriggerUpdate) then
t.Items:AddItem(item)
else
local items = rawget(t.data, 'Items')
if (items) then
local newIndex = #items + 1
rawset(items.data, newIndex, item)
return items.data[newIndex] or item
end
end
return t.Items[#t.Items] or item
end,
---@type function
---@param t Menu MenuV menu
---@param info table Information about range
---@return RangeItem New Range item
AddRange = function(t, info)
info = U:Ensure(info, {})
info.Type = 'range'
info.Events = { OnChange = {}, OnSelect = {}, OnMin = {}, OnMax = {} }
info.PrimaryEvent = 'OnSelect'
info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
info.__menu = t
info.Value = U:Ensure(info.Value or info.value, 0)
info.Min = U:Ensure(info.Min or info.min, 0)
info.Max = U:Ensure(info.Max or info.max, 0)
info.Validate = function(t, k, v)
if (k == 'Value' or k == 'value') then
v = U:Ensure(v, 0)
if (t.Min > v) then return false end
if (t.Max < v) then return false end
end
return true
end
if (info.Min > info.Max) then
local min = info.Min
local max = info.Max
info.Min = min
info.Max = max
end
if (info.Value < info.Min) then info.Value = info.Min end
if (info.Value > info.Max) then info.Value = info.Max end
---@class RangeItem : Item
---@filed private __event string Name of primary event
---@field public UUID string UUID of Item
---@field public Icon string Icon/Emoji for Item
---@field public Label string Label of Item
---@field public Description string Description of Item
---@field public Value any Value of Item
---@field public Values table[] List of values
---@field public Min number Min range value
---@field public Max number Max range value
---@field public Disabled boolean Disabled state of Item
---@field private Events table<string, function[]> List of registered `on` events
---@field public Trigger fun(t: Item, event: string)
---@field public On fun(t: Item, event: string, func: function)
---@field public Validate fun(t: Item, k: string, v:any)
---@field public NewIndex fun(t: Item, k: string, v: any)
---@field public GetValue fun(t: Item):any
---@field public SetMinValue fun(t: any)
---@field public SetMaxValue fun(t: any)
local item = CreateMenuItem(info)
--- Update min value of range
---@param input number Minimum value of Range
function item:SetMinValue(input)
input = U:Ensure(input, 0)
self.Min = input
if (self.Value < self.Min) then
self.Value = self.Min
end
if (self.Min > self.Max) then
self.Max = self.Min
end
end
--- Update max value of range
---@param input number Minimum value of Range
function item:SetMaxValue(input)
input = U:Ensure(input, 0)
self.Min = input
if (self.Value > self.Max) then
self.Value = self.Max
end
if (self.Min < self.Max) then
self.Min = self.Max
end
end
if (info.TriggerUpdate) then
t.Items:AddItem(item)
else
local items = rawget(t.data, 'Items')
if (items) then
local newIndex = #items + 1
rawset(items.data, newIndex, item)
return items.data[newIndex] or item
end
end
return t.Items[#t.Items] or item
end,
---@type function
---@param t Menu MenuV menu
---@param info table Information about confirm
---@return ConfirmItem New Confirm item
AddConfirm = function(t, info)
info = U:Ensure(info, {})
info.Type = 'confirm'
info.Value = U:Ensure(info.Value or info.value, false)
info.Events = { OnConfirm = {}, OnDeny = {}, OnChange = {} }
info.PrimaryEvent = 'OnConfirm'
info.TriggerUpdate = not U:Ensure(info.IgnoreUpdate or info.ignoreUpdate, false)
info.__menu = t
info.NewIndex = function(t, k, v)
if (k == 'Value') then
local value = U:Ensure(v, false)
if (value) then
t:Trigger('confirm', t)
else
t:Trigger('deny', t)
end
end
end
---@class ConfirmItem : Item
---@filed private __event string Name of primary event
---@field public UUID string UUID of Item
---@field public Icon string Icon/Emoji for Item
---@field public Label string Label of Item
---@field public Description string Description of Item
---@field public Value any Value of Item
---@field public Values table[] List of values
---@field public Min number Min range value
---@field public Max number Max range value
---@field public Disabled boolean Disabled state of Item
---@field private Events table<string, function[]> List of registered `on` events
---@field public Trigger fun(t: Item, event: string)
---@field public On fun(t: Item, event: string, func: function)
---@field public Validate fun(t: Item, k: string, v:any)
---@field public NewIndex fun(t: Item, k: string, v: any)
---@field public GetValue fun(t: Item):any
---@field public Confirm fun(t: Item)
---@field public Deny fun(t: Item)
local item = CreateMenuItem(info)
--- Confirm this item
function item:Confirm() item.Value = true end
--- Deny this item
function item:Deny() item.Value = false end
if (info.TriggerUpdate) then
t.Items:AddItem(item)
else
local items = rawget(t.data, 'Items')
if (items) then
local newIndex = #items + 1
rawset(items.data, newIndex, item)
return items.data[newIndex] or item
end
end
return t.Items[#t.Items] or item
end,
--- Create child menu from properties of this object
---@param t Menu|string MenuV menu
---@param overrides table<string, string|number> Properties to override in menu object (ignore parent)
---@param namespace string Namespace of menu
InheritMenu = function(t, overrides, namespace)
return MenuV:InheritMenu(t, overrides, namespace)
end,
--- Add control key for specific menu
---@param t Menu|string MenuV menu
---@param action string Name of action
---@param func function This will be executed
---@param description string Key description
---@param defaultType string Default key type
---@param defaultKey string Default key
AddControlKey = function(t, action, func, description, defaultType, defaultKey)
if (U:Typeof(t.Namespace) ~= 'string' or t.Namespace == 'unknown') then
error('[MenuV] Namespace is required for assigning keys.')
return
end
MenuV:AddControlKey(t, action, func, description, defaultType, defaultKey)
end,
--- Assign key for opening this menu
---@param t Menu|string MenuV menu
---@param defaultType string Default key type
---@param defaultKey string Default key
OpenWith = function(t, defaultType, defaultKey)
t:AddControlKey('open', function(m)
MenuV:CloseAll(function()
MenuV:OpenMenu(m)
end)
end, MenuV:T('open_menu'):format(t.Namespace), defaultType, defaultKey)
end,
--- Change title of menu
---@param t Menu
---@param title string Title of menu
SetTitle = function(t, title)
t.Title = U:Ensure(title, 'MenuV')
end,
--- Change subtitle of menu
---@param t Menu
---@param subtitle string Subtitle of menu
SetSubtitle = function(t, subtitle)
t.Subtitle = U:Ensure(subtitle, '')
end,
--- Change subtitle of menu
---@param t Menu
---@param position string | "'topleft'" | "'topcenter'" | "'topright'" | "'centerleft'" | "'center'" | "'centerright'" | "'bottomleft'" | "'bottomcenter'" | "'bottomright'"
SetPosition = function(t, position)
t.Position = U:Ensure(position, 'topleft')
end,
--- Clear all Menu items
---@param t Menu
ClearItems = function(t, update)
update = U:Ensure(update, true)
local items = CreateEmptyItemsTable({})
items(function(_, trigger, key, index, value, oldValue)
t:Trigger(trigger, key, index, value, oldValue)
end)
rawset(t.data, 'Items', items)
if (update and t.Trigger ~= nil and type(t.Trigger) == 'function') then
t:Trigger('update', 'Items', items)
end
end,
Open = function(t)
MenuV:OpenMenu(t)
end,
Close = function(t)
MenuV:CloseMenu(t)
end,
--- @see Menu to @see table
---@param t Menu
---@return table
ToTable = function(t)
local tempTable = {
theme = U:Ensure(t.Theme, 'default'),
uuid = U:Ensure(t.UUID, '00000000-0000-0000-0000-000000000000'),
title = U:Ensure(t.Title, 'MenuV'),
subtitle = U:Ensure(t.Subtitle, ''),
position = U:Ensure(t.Position, 'topleft'),
size = U:Ensure(t.Size, 'size-110'),
dictionary = U:Ensure(t.Dictionary, 'menuv'),
texture = U:Ensure(t.Texture, 'default'),
color = {
r = U:Ensure(t.Color.R, 0),
g = U:Ensure(t.Color.G, 0),
b = U:Ensure(t.Color.B, 255)
},
items = {}
}
local items = rawget(t.data, 'Items')
if (items ~= nil and items.ToTable ~= nil) then
tempTable.items = items:ToTable()
end
if (tempTable.color.r <= 0) then tempTable.color.r = 0 end
if (tempTable.color.r >= 255) then tempTable.color.r = 255 end
if (tempTable.color.g <= 0) then tempTable.color.g = 0 end
if (tempTable.color.g >= 255) then tempTable.color.g = 255 end
if (tempTable.color.b <= 0) then tempTable.color.b = 0 end
if (tempTable.color.b >= 255) then tempTable.color.b = 255 end
return tempTable
end
}
if (lower(item.Texture) == 'default' and lower(item.Dictionary) == 'menuv' and theme == 'native') then
item.Texture = 'default_native'
end
item.Events.OnOpen = {}
item.Events.OnClose = {}
item.Events.OnSelect = {}
item.Events.OnUpdate = {}
item.Events.OnSwitch = {}
item.Events.OnChange = {}
item.Events.OnIChange = {}
if (not U:Any(item.Size, { 'size-100', 'size-110', 'size-125', 'size-150', 'size-175', 'size-200' }, 'value')) then
item.Size = 'size-110'
end
local mt = {
__index = function(t, k)
return rawget(t.data, k)
end,
---@param t Menu
__tostring = function(t)
return encode(t:ToTable())
end,
__call = function(t)
MenuV:OpenMenu(t)
end,
__newindex = function(t, k, v)
local whitelisted = { 'Title', 'Subtitle', 'Position', 'Color', 'R', 'G', 'B', 'Size', 'Dictionary', 'Texture', 'Theme' }
local key = U:Ensure(k, 'unknown')
local oldValue = rawget(t.data, k)
if (not U:Any(key, whitelisted, 'value') and oldValue ~= nil) then
return
end
local checkInput = t.Validate ~= nil and type(t.Validate) == 'function'
local inputParser = t.Parser ~= nil and type(t.Parser) == 'function'
local updateIndexTrigger = t.NewIndex ~= nil and type(t.NewIndex) == 'function'
if (checkInput) then
local result = t:Validate(key, v)
result = U:Ensure(result, true)
if (not result) then
return
end
end
if (inputParser) then
local parsedValue = t:Parser(key, v)
v = parsedValue or v
end
rawset(t.data, k, v)
if (key == 'Theme' or key == 'theme') then
local theme_value = string.lower(U:Ensure(v, 'default'))
if (theme_value == 'native') then
rawset(t.data, 'color', { R = 255, G = 255, B = 255 })
local texture = U:Ensure(rawget(t.data, 'Texture'), 'default')
if (texture == 'default') then
rawset(t.data, 'Texture', 'default_native')
end
elseif (theme_value == 'default') then
local texture = U:Ensure(rawget(t.data, 'Texture'), 'default')
if (texture == 'default_native') then
rawset(t.data, 'Texture', 'default')
end
end
end
if (updateIndexTrigger) then
t:NewIndex(key, v)
end
if (t.Trigger ~= nil and type(t.Trigger) == 'function') then
t:Trigger('update', key, v, oldValue)
end
end,
__len = function(t)
return #t.Items
end,
__pairs = function(t)
return pairs(rawget(t.data, 'Items') or {})
end,
__ipairs = function(t)
return ipairs(rawget(t.data, 'Items') or {})
end,
__metatable = 'MenuV',
}
---@class Menu
---@field public IsOpen boolean `true` if menu is open, otherwise `false`
---@field public UUID string UUID of Menu
---@field public Title string Title of Menu
---@field public Subtitle string Subtitle of Menu
---@field public Position string | "'topleft'" | "'topcenter'" | "'topright'" | "'centerleft'" | "'center'" | "'centerright'" | "'bottomleft'" | "'bottomcenter'" | "'bottomright'"
---@field public Texture string Name of texture example: "default"
---@field public Dictionary string Name of dictionary example: "menuv"
---@field public Color table<string, number> Color of Menu
---@field private Events table<string, fun[]> List of registered `on` events
---@field public Items Item[] List of items
---@field public Trigger fun(t: Item, event: string)
---@field public On fun(t: Menu, event: string, func: function|Menu): string
---@field public RemoveOnEvent fun(t: Menu, event: string, uuid: string)
---@field public Validate fun(t: Menu, k: string, v:any)
---@field public NewIndex fun(t: Menu, k: string, v: any)
---@field public Parser fun(t: Menu, k: string, v: any)
---@field public AddButton fun(t: Menu, info: table):Item
---@field public AddCheckbox fun(t: Menu, info: table):Item
---@field public AddSlider fun(t: Menu, info: table):SliderItem
---@field public AddRange fun(t: Menu, info: table):RangeItem
---@field public AddConfirm fun(t: Menu, info: table):ConfirmItem
---@field public AddControlKey fun(t: Menu, action: string, func: function, description: string, defaultType: string, defaultKey: string)
---@field public OpenWith fun(t: Menu, defaultType: string, defaultKey: string)
---@field public SetTitle fun(t: Menu, title: string)
---@field public SetSubtitle fun(t: Menu, subtitle: string)
---@field public SetPosition fun(t: Menu, position: string)
---@field public ClearItems fun(t: Menu)
---@field public Open fun(t: Menu)
---@field public Close fun(t: Menu)
---@field public ToTable fun(t: Menu):table
local menu = setmetatable({ data = item, __class = 'Menu', __type = 'Menu' }, mt)
menu.Items(function(items, trigger, key, index, value, oldValue)
menu:Trigger(trigger, key, index, value, oldValue)
end)
for k, v in pairs(info or {}) do
local key = U:Ensure(k, 'unknown')
if (key == 'unknown') then return end
menu:On(key, v)
end
LoadTextureDictionary(menu.Dictionary)
return menu
end
_ENV.CreateMenu = CreateMenu
_G.CreateMenu = CreateMenu
----------------------- [ MenuV ] -----------------------
-- GitHub: https://github.com/ThymonA/menuv/
-- License: GNU General Public License v3.0
-- https://choosealicense.com/licenses/gpl-3.0/
-- Author: Thymon Arens <contact@arens.io>
-- Name: MenuV
-- Version: 1.4.1
-- Description: FiveM menu library for creating menu's
----------------------- [ MenuV ] -----------------------
local assert = assert
local decode = assert(json.decode)
--- FiveM globals
local LoadResourceFile = assert(LoadResourceFile)
--- MenuV globals
---@type Utilities
local Utilities = assert(Utilities)
--- Empty translations table
local translations = {}
--- Load all translations
local lang = Utilities:Ensure((Config or {}).Language, 'en')
local translations_path = ('languages/%s.json'):format(lang)
local translations_raw = LoadResourceFile('menuv', translations_path)
if (translations_raw) then
local transFile = decode(translations_raw)
if (transFile) then translations = Utilities:Ensure(transFile.translations, {}) end
end
_ENV.translations = translations
_G.translations = translations
--- MenuV table
local menuv_table = {
---@type string
__class = 'MenuV',
---@type string
__type = 'MenuV',
---@type Menu|nil
CurrentMenu = nil,
---@type string|nil
CurrentUpdateUUID = nil,
---@type string
CurrentResourceName = GET_CURRENT_RESOURCE_NAME(),
---@type boolean
Loaded = false,
---@type Menu[]
Menus = {},
---@type Menu[]
ParentMenus = {},
---@type table<string, function>
NUICallbacks = {},
---@type table<string, string>
Translations = translations,
---@class keys
Keys = setmetatable({ data = {}, __class = 'MenuVKeys', __type = 'keys' }, {
__index = function(t, k)
return rawget(t.data, k)
end,
__newindex = function(t, actionHax, v)
actionHax = Utilities:Ensure(actionHax, 'unknown')
if (actionHax == 'unknown') then return end
local rawKey = rawget(t.data, actionHax)
local keyExists = rawKey ~= nil
local prevState = Utilities:Ensure((rawKey or {}).status, false)
local newState = Utilities:Ensure(v, false)
if (keyExists) then
rawset(t.data[actionHax], 'status', newState)
if (prevState ~= newState and newState) then
rawKey.func(rawKey.menu)
end
end
end,
__call = function(t, actionHax, m, actionFunc, inputType)
actionHax = Utilities:Ensure(actionHax, 'unknown')
m = Utilities:Typeof(m) == 'Menu' and m or nil
actionFunc = Utilities:Ensure(actionFunc, function() end)
inputType = Utilities:Ensure(inputType, 'KEYBOARD')
inputType = upper(inputType)
if (actionHax == 'unknown') then return end
local rawKey = rawget(t.data, actionHax)
local keyExists = rawKey ~= nil
if (keyExists) then
if not rawKey.inputTypes[inputType] then
rawKey.inputTypes[inputType] = true
end
return
end
rawset(t.data, actionHax, { status = false, menu = m, func = actionFunc, inputTypes = { [inputType] = true } })
end
})
}
---@class MenuV
MenuV = setmetatable(menuv_table, {})
--- Send a NUI message to MenuV resource
---@param input any
local SEND_NUI_MESSAGE = function(input)
exports['menuv']:SendNUIMessage(input)
end
--- Register a NUI callback event
---@param name string Name of callback
---@param cb function Callback to execute
local REGISTER_NUI_CALLBACK = function(name, cb)
name = Utilities:Ensure(name, 'unknown')
cb = Utilities:Ensure(cb, function(_, cb) cb('ok') end)
MenuV.NUICallbacks[name] = cb
end
--- Load translation
---@param k string Translation key
---@return string Translation or 'MISSING TRANSLATION'
function MenuV:T(k)
k = Utilities:Ensure(k, 'unknown')
return Utilities:Ensure(MenuV.Translations[k], 'MISSING TRANSLATION')
end
--- Create a `MenuV` menu
---@param title string Title of Menu
---@param subtitle string Subtitle of Menu
---@param position string Position of Menu
---@param r number 0-255 RED
---@param g number 0-255 GREEN
---@param b number 0-255 BLUE
---@param size string | "'size-100'" | "'size-110'" | "'size-125'" | "'size-150'" | "'size-175'" | "'size-200'"
---@param texture string Name of texture example: "default"
---@param dictionary string Name of dictionary example: "menuv"
---@param namespace string Namespace of Menu
---@param theme string Theme of Menu
---@return Menu
function MenuV:CreateMenu(title, subtitle, position, r, g, b, size, texture, dictionary, namespace, theme)
local menu = CreateMenu({
Theme = theme,
Title = title,
Subtitle = subtitle,
Position = position,
R = r,
G = g,
B = b,
Size = size,
Texture = texture,
Dictionary = dictionary,
Namespace = namespace
})
local index = #(self.Menus or {}) + 1
insert(self.Menus, index, menu)
return self.Menus[index] or menu
end
--- Create a menu that inherits properties from another menu
---@param parent Menu|string Menu or UUID of menu
---@param overrides table<string, string|number> Properties to override in menu object (ignore parent)
---@param namespace string Namespace of menu
function MenuV:InheritMenu(parent, overrides, namespace)
overrides = Utilities:Ensure(overrides, {})
local uuid = Utilities:Typeof(parent) == 'Menu' and parent.UUID or Utilities:Typeof(parent) == 'string' and parent
if (uuid == nil) then return end
local parentMenu = self:GetMenu(uuid)
if (parentMenu == nil) then return end
local menu = CreateMenu({
Theme = Utilities:Ensure(overrides.theme or overrides.Theme, parentMenu.Theme),
Title = Utilities:Ensure(overrides.title or overrides.Title, parentMenu.Title),
Subtitle = Utilities:Ensure(overrides.subtitle or overrides.Subtitle, parentMenu.Subtitle),
Position = Utilities:Ensure(overrides.position or overrides.Position, parentMenu.Position),
R = Utilities:Ensure(overrides.r or overrides.R, parentMenu.Color.R),
G = Utilities:Ensure(overrides.g or overrides.G, parentMenu.Color.G),
B = Utilities:Ensure(overrides.b or overrides.B, parentMenu.Color.B),
Size = Utilities:Ensure(overrides.size or overrides.Size, parentMenu.Size),
Texture = Utilities:Ensure(overrides.texture or overrides.Texture, parentMenu.Texture),
Dictionary = Utilities:Ensure(overrides.dictionary or overrides.Dictionary, parentMenu.Dictionary),
Namespace = Utilities:Ensure(namespace, 'unknown')
})
local index = #(self.Menus or {}) + 1
insert(self.Menus, index, menu)
return self.Menus[index] or menu
end
--- Load a menu based on `uuid`
---@param uuid string UUID of menu
---@return Menu|nil Founded menu or `nil`
function MenuV:GetMenu(uuid)
uuid = Utilities:Ensure(uuid, '00000000-0000-0000-0000-000000000000')
for _, v in pairs(self.Menus) do
if (v.UUID == uuid) then
return v
end
end
return nil
end
--- Open a menu
---@param menu Menu|string Menu or UUID of Menu
---@param cb function Execute this callback when menu has opened
function MenuV:OpenMenu(menu, cb, reopen)
local uuid = Utilities:Typeof(menu) == 'Menu' and menu.UUID or Utilities:Typeof(menu) == 'string' and menu
if (uuid == nil) then return end
cb = Utilities:Ensure(cb, function() end)
menu = self:GetMenu(uuid)
if (menu == nil) then return end
local dictionaryLoaded = HAS_STREAMED_TEXTURE_DICT_LOADED(menu.Dictionary)
if (not self.Loaded or not dictionaryLoaded) then
if (not dictionaryLoaded) then REQUEST_STREAMED_TEXTURE_DICT(menu.Dictionary) end
CreateThread(function()
repeat Wait(0) until MenuV.Loaded
if (not dictionaryLoaded) then
repeat Wait(10) until HAS_STREAMED_TEXTURE_DICT_LOADED(menu.Dictionary)
end
MenuV:OpenMenu(uuid, cb)
end)
return
end
if (self.CurrentMenu ~= nil) then
insert(self.ParentMenus, self.CurrentMenu)
self.CurrentMenu:RemoveOnEvent('update', self.CurrentUpdateUUID)
self.CurrentMenu:DestroyThreads()
end
self.CurrentMenu = menu
self.CurrentUpdateUUID = menu:On('update', function(m, k, v)
k = Utilities:Ensure(k, 'unknown')
if (k == 'Title' or k == 'title') then
SEND_NUI_MESSAGE({ action = 'UPDATE_TITLE', title = Utilities:Ensure(v, 'MenuV'), __uuid = m.UUID })
elseif (k == 'Subtitle' or k == 'subtitle') then
SEND_NUI_MESSAGE({ action = 'UPDATE_SUBTITLE', subtitle = Utilities:Ensure(v, ''), __uuid = m.UUID })
elseif (k == 'Items' or k == 'items') then
SEND_NUI_MESSAGE({ action = 'UPDATE_ITEMS', items = (m.Items:ToTable() or {}), __uuid = m.UUID })
elseif (k == 'Item' or k == 'item' and Utilities:Typeof(v) == 'Item') then
SEND_NUI_MESSAGE({ action = 'UPDATE_ITEM', item = m.Items:ItemToTable(v) or {}, __uuid = m.UUID })
elseif (k == 'AddItem' or k == 'additem' and Utilities:Typeof(v) == 'Item') then
SEND_NUI_MESSAGE({ action = 'ADD_ITEM', item = m.Items:ItemToTable(v), __uuid = m.UUID })
elseif (k == 'RemoveItem' or k == 'removeitem' and Utilities:Typeof(v) == 'Item') then
SEND_NUI_MESSAGE({ action = 'REMOVE_ITEM', uuid = v.UUID, __uuid = m.UUID })
elseif (k == 'UpdateItem' or k == 'updateitem' and Utilities:Typeof(v) == 'Item') then
SEND_NUI_MESSAGE({ action = 'UPDATE_ITEM', item = m.Items:ItemToTable(v) or {}, __uuid = m.UUID })
end
end)
SEND_NUI_MESSAGE({
action = 'OPEN_MENU',
menu = menu:ToTable(),
reopen = Utilities:Ensure(reopen, false)
})
cb()
end
function MenuV:Refresh()
if (self.CurrentMenu == nil) then
return
end
SEND_NUI_MESSAGE({
action = 'REFRESH_MENU',
menu = self.CurrentMenu:ToTable()
})
end
--- Close a menu
---@param menu Menu|string Menu or UUID of Menu
---@param cb function Execute this callback when menu has is closed or parent menu has opened
function MenuV:CloseMenu(menu, cb)
local uuid = Utilities:Typeof(menu) == 'Menu' and menu.UUID or Utilities:Typeof(menu) == 'string' and menu
if (uuid == nil) then cb() return end
cb = Utilities:Ensure(cb, function() end)
menu = self:GetMenu(uuid)
if (menu == nil or self.CurrentMenu == nil or self.CurrentMenu.UUID ~= uuid) then cb() return end
self.CurrentMenu:RemoveOnEvent('update', self.CurrentUpdateUUID)
self.CurrentMenu:Trigger('close')
self.CurrentMenu:DestroyThreads()
self.CurrentMenu = nil
SEND_NUI_MESSAGE({ action = 'CLOSE_MENU', uuid = uuid })
if (#self.ParentMenus <= 0) then cb() return end
local prev_index = #self.ParentMenus
local prev_menu = self.ParentMenus[prev_index] or nil
if (prev_menu == nil) then cb() return end
remove(self.ParentMenus, prev_index)
self:OpenMenu(prev_menu, function()
cb()
end, true)
end
--- Close all menus
---@param cb function Execute this callback when all menus are closed
function MenuV:CloseAll(cb)
cb = Utilities:Ensure(cb, function() end)
if (not self.Loaded) then
CreateThread(function()
repeat Wait(0) until MenuV.Loaded
MenuV:CloseAll(cb)
end)
return
end
if (self.CurrentMenu == nil) then cb() return end
local uuid = Utilities:Ensure(self.CurrentMenu.UUID, '00000000-0000-0000-0000-000000000000')
self.CurrentMenu:RemoveOnEvent('update', self.CurrentUpdateUUID)
self.CurrentMenu:Trigger('close')
self.CurrentMenu:DestroyThreads()
SEND_NUI_MESSAGE({ action = 'CLOSE_MENU', uuid = uuid })
self.CurrentMenu = nil
self.ParentMenus = {}
cb()
end
--- Register keybind for specific menu
---@param menu Menu|string MenuV menu
---@param action string Name of action
---@param func function This will be executed
---@param description string Key description
---@param defaultType string Default key type
---@param defaultKey string Default key
function MenuV:AddControlKey(menu, action, func, description, defaultType, defaultKey)
local uuid = Utilities:Typeof(menu) == 'Menu' and menu.UUID or Utilities:Typeof(menu) == 'string' and menu
action = Utilities:Ensure(action, 'UNKNOWN')
func = Utilities:Ensure(func, function() end)
description = Utilities:Ensure(description, 'unknown')
defaultType = Utilities:Ensure(defaultType, 'KEYBOARD')
defaultType = upper(defaultType)
defaultKey = Utilities:Ensure(defaultKey, 'F12')
local m = self:GetMenu(uuid)
if (m == nil) then return end
if (Utilities:Typeof(m.Namespace) ~= 'string' or m.Namespace == 'unknown') then
error('[MenuV] Namespace is required for assigning keys.')
return
end
action = Utilities:Replace(action, ' ', '_')
action = upper(action)
local resourceName = Utilities:Ensure(self.CurrentResourceName, 'unknown')
local namespace = Utilities:Ensure(m.Namespace, 'unknown')
local actionHash = GET_HASH_KEY(('%s_%s_%s'):format(resourceName, namespace, action))
local actionHax = format('%x', actionHash)
local typeGroup = Utilities:GetInputTypeGroup(defaultType)
if (self.Keys[actionHax] and self.Keys[actionHax].inputTypes[typeGroup]) then return end
self.Keys(actionHax, m, func, typeGroup)
local k = actionHax
if typeGroup > 0 then
local inputGroupName = Utilities:GetInputGroupName(typeGroup)
k = ('%s_%s'):format(lower(inputGroupName), k)
end
REGISTER_KEY_MAPPING(('+%s'):format(k), description, defaultType, defaultKey)
REGISTER_COMMAND(('+%s'):format(k), function() MenuV.Keys[actionHax] = true end)
REGISTER_COMMAND(('-%s'):format(k), function() MenuV.Keys[actionHax] = false end)
end
--- Checks if namespace is available
---@param namespace string Namespace
---@return boolean Returns `true` if given namespace is available
function MenuV:IsNamespaceAvailable(namespace)
namespace = lower(Utilities:Ensure(namespace, 'unknown'))
if (namespace == 'unknown') then return true end
---@param v Menu
for k, v in pairs(self.Menus or {}) do
local v_namespace = Utilities:Ensure(v.Namespace, 'unknown')
if (namespace == lower(v_namespace)) then
return false
end
end
return true
end
--- Mark MenuV as loaded when `main` resource is loaded
exports['menuv']:IsLoaded(function()
MenuV.Loaded = true
end)
--- Register callback handler for MenuV
exports('NUICallback', function(name, info, cb)
name = Utilities:Ensure(name, 'unknown')
if (MenuV.NUICallbacks == nil or MenuV.NUICallbacks[name] == nil) then
return
end
MenuV.NUICallbacks[name](info, cb)
end)
REGISTER_NUI_CALLBACK('open', function(info, cb)
local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
local new_uuid = Utilities:Ensure(info.new_uuid, '00000000-0000-0000-0000-000000000000')
cb('ok')
if (MenuV.CurrentMenu == nil or MenuV.CurrentMenu.UUID == uuid or MenuV.CurrentMenu.UUID == new_uuid) then return end
for _, v in pairs(MenuV.ParentMenus) do
if (v.UUID == uuid) then
return
end
end
MenuV.CurrentMenu:RemoveOnEvent('update', MenuV.CurrentUpdateUUID)
MenuV.CurrentMenu:Trigger('close')
MenuV.CurrentMenu:DestroyThreads()
MenuV.CurrentMenu = nil
MenuV.ParentMenus = {}
end)
REGISTER_NUI_CALLBACK('opened', function(info, cb)
local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
cb('ok')
if (MenuV.CurrentMenu == nil or MenuV.CurrentMenu.UUID ~= uuid) then return end
MenuV.CurrentMenu:Trigger('open')
end)
REGISTER_NUI_CALLBACK('submit', function(info, cb)
local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
cb('ok')
if (MenuV.CurrentMenu == nil) then return end
for k, v in pairs(MenuV.CurrentMenu.Items) do
if (v.UUID == uuid) then
if (v.__type == 'confirm' or v.__type == 'checkbox') then
v.Value = Utilities:Ensure(info.value, false)
elseif (v.__type == 'range') then
v.Value = Utilities:Ensure(info.value, v.Min)
elseif (v.__type == 'slider') then
v.Value = Utilities:Ensure(info.value, 0) + 1
end
MenuV.CurrentMenu:Trigger('select', v)
if (v.__type == 'button' or v.__type == 'menu') then
MenuV.CurrentMenu.Items[k]:Trigger('select')
elseif (v.__type == 'range') then
MenuV.CurrentMenu.Items[k]:Trigger('select', v.Value)
elseif (v.__type == 'slider') then
local option = MenuV.CurrentMenu.Items[k].Values[v.Value] or nil
if (option == nil) then return end
MenuV.CurrentMenu.Items[k]:Trigger('select', option.Value)
end
return
end
end
end)
REGISTER_NUI_CALLBACK('close', function(info, cb)
local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
if (MenuV.CurrentMenu == nil or MenuV.CurrentMenu.UUID ~= uuid) then cb('ok') return end
MenuV.CurrentMenu:RemoveOnEvent('update', MenuV.CurrentUpdateUUID)
MenuV.CurrentMenu:Trigger('close')
MenuV.CurrentMenu:DestroyThreads()
MenuV.CurrentMenu = nil
if (#MenuV.ParentMenus <= 0) then cb('ok') return end
local prev_index = #MenuV.ParentMenus
local prev_menu = MenuV.ParentMenus[prev_index] or nil
if (prev_menu == nil) then cb('ok') return end
remove(MenuV.ParentMenus, prev_index)
MenuV:OpenMenu(prev_menu, function()
cb('ok')
end, true)
end)
REGISTER_NUI_CALLBACK('close_all', function(info, cb)
if (MenuV.CurrentMenu == nil) then cb('ok') return end
MenuV.CurrentMenu:RemoveOnEvent('update', MenuV.CurrentUpdateUUID)
MenuV.CurrentMenu:Trigger('close')
MenuV.CurrentMenu:DestroyThreads()
MenuV.CurrentMenu = nil
MenuV.ParentMenus = {}
cb('ok')
end)
REGISTER_NUI_CALLBACK('switch', function(info, cb)
local prev_uuid = Utilities:Ensure(info.prev, '00000000-0000-0000-0000-000000000000')
local next_uuid = Utilities:Ensure(info.next, '00000000-0000-0000-0000-000000000000')
local prev_item, next_item = nil, nil
cb('ok')
if (MenuV.CurrentMenu == nil) then return end
for k, v in pairs(MenuV.CurrentMenu.Items) do
if (v.UUID == prev_uuid) then
prev_item = v
MenuV.CurrentMenu.Items[k]:Trigger('leave')
end
if (v.UUID == next_uuid) then
next_item = v
MenuV.CurrentMenu.Items[k]:Trigger('enter')
end
end
if (prev_item ~= nil and next_item ~= nil) then
MenuV.CurrentMenu:Trigger('switch', next_item, prev_item)
end
end)
REGISTER_NUI_CALLBACK('update', function(info, cb)
local uuid = Utilities:Ensure(info.uuid, '00000000-0000-0000-0000-000000000000')
cb('ok')
if (MenuV.CurrentMenu == nil) then return end
for k, v in pairs(MenuV.CurrentMenu.Items) do
if (v.UUID == uuid) then
local newValue, oldValue = nil, nil
if (v.__type == 'confirm' or v.__type == 'checkbox') then
newValue = Utilities:Ensure(info.now, false)
oldValue = Utilities:Ensure(info.prev, false)
elseif (v.__type == 'range') then
newValue = Utilities:Ensure(info.now, v.Min)
oldValue = Utilities:Ensure(info.prev, v.Min)
elseif (v.__type == 'slider') then
newValue = Utilities:Ensure(info.now, 0) + 1
oldValue = Utilities:Ensure(info.prev, 0) + 1
end
if (Utilities:Any(v.__type, { 'button', 'menu', 'label' }, 'value')) then return end
MenuV.CurrentMenu:Trigger('update', v, newValue, oldValue)
MenuV.CurrentMenu.Items[k]:Trigger('change', newValue, oldValue)
if (v.SaveOnUpdate) then
MenuV.CurrentMenu.Items[k].Value = newValue
if (v.__type == 'range') then
MenuV.CurrentMenu.Items[k]:Trigger('select', v.Value)
elseif (v.__type == 'slider') then
local option = MenuV.CurrentMenu.Items[k].Values[v.Value] or nil
if (option == nil) then return end
MenuV.CurrentMenu.Items[k]:Trigger('select', option.Value)
end
end
return
end
end
end)