├── LICENSE ├── client ├── component │ ├── context.lua │ ├── dialog.lua │ ├── input.lua │ ├── instructButton.lua │ ├── notification.lua │ ├── progressBar.lua │ └── textUI.lua ├── functions.lua └── test.lua ├── fxmanifest.lua ├── game ├── client │ ├── cb.lua │ ├── class │ │ └── VehicleEvent.lua │ ├── entity.lua │ ├── raycast.lua │ └── utility.lua ├── framework │ ├── client.lua │ └── server.lua └── server │ ├── cb.lua │ ├── event.lua │ └── utility.lua ├── init.lua ├── readme.MD ├── server └── main.lua ├── shared ├── config.lua ├── math.lua ├── shared.lua └── string.lua └── web ├── build ├── assets │ ├── index-B3Mu_d4D.js │ ├── index-BDn5-7xH.js │ ├── index-BTKS3fo5.css │ ├── index-BTkTTIbY.js │ ├── index-BfZJaoOZ.js │ ├── index-BiCOjNXi.css │ ├── index-C01FryLe.css │ ├── index-C5SsTEtp.js │ ├── index-CBn9auh-.js │ ├── index-CcFNomvE.css │ ├── index-DmLozrSC.js │ ├── index-Do78jPx0.css │ ├── index-OgmWukRk.js │ ├── index-VtQVnvRK.js │ └── index-qrz68GA9.js └── index.html ├── index.html ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── src ├── components │ ├── App.tsx │ ├── ContextMenu.scss │ ├── ContextMenu.tsx │ ├── Dialog.scss │ ├── Dialog.tsx │ ├── Input.tsx │ ├── Instruct.scss │ ├── Instructional.tsx │ ├── Notification.module.scss │ ├── Notification.tsx │ ├── ProgressBar.tsx │ ├── TextUI.scss │ ├── TextUI.tsx │ └── map.tsx ├── hooks │ └── useNuiEvent.ts ├── index.css ├── main.tsx ├── utils │ ├── Icon.tsx │ ├── LangR.tsx │ ├── debugData.ts │ ├── fetchNui.ts │ ├── mapimage │ │ └── map.jpg │ └── misc.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /client/component/context.lua: -------------------------------------------------------------------------------- 1 | local MenuMetaTable = {} 2 | MenuMetaTable.__index = MenuMetaTable 3 | local CONTEXT_MENUS = {} 4 | 5 | function MenuMetaTable.new(menuID, menuTitle, items) 6 | local self = setmetatable({}, MenuMetaTable) 7 | self.id = menuID 8 | self.title = menuTitle 9 | self.items = items 10 | return self 11 | end 12 | 13 | function MenuMetaTable:getItem(index) 14 | return self.items[index] 15 | end 16 | 17 | function MenuMetaTable:selectItem(index) 18 | local item = self:getItem(index) 19 | if item and item.onSelect then 20 | item.onSelect() 21 | return true 22 | end 23 | return false 24 | end 25 | 26 | local function registerMenu(menuID, menuTitle, items) 27 | if not menuID or not menuTitle or not items then 28 | print(('Error: Missing data in registerMenu. MenuID: %s, MenuTitle: %s, Items: %s'):format( 29 | tostring(menuID or "nil"), 30 | tostring(menuTitle or "nil"), 31 | tostring(items or "nil") 32 | )) 33 | return 34 | end 35 | 36 | CONTEXT_MENUS[menuID] = MenuMetaTable.new(menuID, menuTitle, items) 37 | _G.isUIOpen = true 38 | end 39 | 40 | RegisterNuiCallback('LGF_UI.GetContextData', function(data, cb) 41 | local menuID = data.menuID 42 | local menuData = CONTEXT_MENUS[menuID] 43 | 44 | if menuData then 45 | cb({ 46 | id = menuData.id, 47 | title = menuData.title, 48 | items = menuData.items 49 | }) 50 | else 51 | cb(nil) 52 | end 53 | end) 54 | 55 | RegisterNuiCallback('menu:ItemSelected', function(data, cb) 56 | local menu = CONTEXT_MENUS[data.menuID] 57 | if menu then 58 | local itemIndex = tonumber(data.itemIndex) + 1 59 | local success = menu:selectItem(itemIndex) 60 | 61 | cb(success) 62 | else 63 | cb(false) 64 | end 65 | end) 66 | 67 | 68 | exports('RegisterContextMenu', registerMenu) 69 | -------------------------------------------------------------------------------- /client/component/dialog.lua: -------------------------------------------------------------------------------- 1 | ---@class DialogCard 2 | ---@field title string Title of the card 3 | ---@field message string Message or description associated with the card 4 | ---@field actionLabel string|nil Label for the action button (optional) 5 | ---@field actionCloseLabel string|nil Label for the close action button (optional, default is "Close") 6 | ---@field onAction function|nil Function callback triggered when the action button is pressed (optional) 7 | ---@field onClose function|nil Function callback triggered when the dialog is closed (optional) 8 | ---@field image string|nil URL or path to an image to be displayed on the card (optional) 9 | 10 | ---@class DialogData 11 | ---@field id string Unique ID for the dialog 12 | ---@field title string Title of the dialog 13 | ---@field enableCam boolean Whether to enable the custom camera when the dialog is opened 14 | ---@field cards DialogCard[] Array of card objects that represent options or content within the dialog 15 | 16 | 17 | DialogMetaTable = {} 18 | DialogMetaTable.__index = DialogMetaTable 19 | local DIALOGS = {} 20 | local currentCam 21 | LocalPlayer.state.DialogOpened = false 22 | 23 | function CameraDialog() 24 | local entity = PlayerPedId() 25 | local distance = 1.0 26 | local entityCoords = GetOffsetFromEntityInWorldCoords(entity, 0, distance, 0) 27 | 28 | local ENTITYCOORDSCAM = vector3(entityCoords.x, entityCoords.y, entityCoords.z + 0.90) 29 | local defaultCamRot = vector3(-24.0, 0.0, GetEntityHeading(entity) + 180) 30 | local defaultCamZoom = 100.0 31 | 32 | if currentCam then 33 | SetCamActive(currentCam, false) 34 | RenderScriptCams(false, false, 0, true, true) 35 | DestroyCam(currentCam) 36 | end 37 | 38 | currentCam = CreateCam("DEFAULT_SCRIPTED_CAMERA", true) 39 | SetCamCoord(currentCam, ENTITYCOORDSCAM) 40 | SetCamRot(currentCam, defaultCamRot.x, defaultCamRot.y, defaultCamRot.z, 2) 41 | SetCamFov(currentCam, defaultCamZoom) 42 | 43 | SetCamActive(currentCam, true) 44 | RenderScriptCams(true, true, 2000, true, true) 45 | SetFocusArea(ENTITYCOORDSCAM, 0.0, 0.0, 0.0) 46 | SetFocusEntity(entity) 47 | end 48 | 49 | function DestroyCamera() 50 | if currentCam then 51 | RenderScriptCams(false, true, 2000, 1, 0) 52 | DestroyCam(currentCam, false) 53 | currentCam = nil 54 | end 55 | end 56 | 57 | function DialogMetaTable.new(dialogID, dialogTitle, cards) 58 | local self = setmetatable({}, DialogMetaTable) 59 | self.id = dialogID 60 | self.title = dialogTitle 61 | self.cards = cards 62 | return self 63 | end 64 | 65 | function DialogMetaTable:getCard(index) 66 | return self.cards[index] 67 | end 68 | 69 | function DialogMetaTable:handleAction(cardIndex) 70 | print(cardIndex) 71 | local card = self:getCard(cardIndex) 72 | if card and card.onAction then 73 | card.onAction() 74 | return true 75 | end 76 | return false 77 | end 78 | 79 | function DialogMetaTable:handleClose(cardIndex) 80 | local card = self:getCard(cardIndex) 81 | if card then 82 | if card.onClose then 83 | card.onClose() 84 | return true 85 | end 86 | else 87 | print(('Card not found for cardIndex: %s'):format(cardIndex)) 88 | end 89 | return false 90 | end 91 | 92 | local function registerDialog(dialogID, dialogTitle, cards) 93 | if not dialogID or not dialogTitle or not cards then 94 | print(('Error: Missing data in registerDialog. DialogID: %s, DialogTitle: %s, Cards: %s'):format( 95 | tostring(dialogID or "nil"), 96 | tostring(dialogTitle or "nil"), 97 | tostring(cards or "nil") 98 | )) 99 | return 100 | end 101 | 102 | DIALOGS[dialogID] = DialogMetaTable.new(dialogID, dialogTitle, cards) 103 | end 104 | 105 | function OpenDialog(data) 106 | 107 | SetNuiFocus(true, true) 108 | local dialogID = data.id 109 | registerDialog(dialogID, data.title, data.cards or {}) 110 | 111 | if data.enableCam then 112 | CameraDialog() 113 | end 114 | 115 | local CARDS_STEPPER = {} 116 | for _, card in ipairs(data.cards or {}) do 117 | local cardCopy = {} 118 | for key, value in pairs(card) do 119 | if key ~= 'onAction' and key ~= 'onClose' then 120 | cardCopy[key] = value 121 | end 122 | end 123 | 124 | cardCopy.hasOnAction = card.onAction ~= nil 125 | cardCopy.hasOnClose = card.onClose ~= nil 126 | table.insert(CARDS_STEPPER, cardCopy) 127 | end 128 | 129 | SendNUIMessage({ 130 | action = 'showDialog', 131 | id = dialogID, 132 | title = data.title, 133 | cards = CARDS_STEPPER 134 | }) 135 | 136 | LocalPlayer.state.DialogOpened = true 137 | _G.isUIOpen = true 138 | end 139 | 140 | function CloseDialog(dialogID) 141 | if currentCam then 142 | DestroyCamera() 143 | end 144 | SetNuiFocus(false, false) 145 | DIALOGS[dialogID] = nil 146 | SendNUIMessage({ 147 | action = 'hideDialog', 148 | id = dialogID 149 | }) 150 | LocalPlayer.state.DialogOpened = false 151 | _G.isUIOpen = false 152 | end 153 | 154 | RegisterNUICallback('dialogAction', function(data, cb) 155 | local dialog = DIALOGS[data.id] 156 | if dialog then 157 | local success = dialog:handleAction(data.cardIndex) 158 | if success then 159 | cb('ok') 160 | else 161 | cb('error') 162 | end 163 | else 164 | cb('error') 165 | end 166 | end) 167 | 168 | RegisterNUICallback('dialogClose', function(data, cb) 169 | if currentCam then 170 | DestroyCamera() 171 | end 172 | SetNuiFocus(false, false) 173 | local dialog = DIALOGS[data.id] 174 | if dialog then 175 | local success = dialog:handleClose(data.cardIndex) 176 | if success then 177 | cb('ok') 178 | LocalPlayer.state.DialogOpened = false 179 | _G.isUIOpen = false 180 | else 181 | cb('error') 182 | end 183 | else 184 | cb('error') 185 | end 186 | end) 187 | 188 | 189 | local function GetStateDialog() 190 | return LocalPlayer.state.DialogOpened 191 | end 192 | 193 | exports('RegisterDialog', function(data) 194 | print("dwadwadwa") 195 | _G.isUIOpen = true 196 | return OpenDialog(data) 197 | end) 198 | 199 | exports('CloseDialog', function(dialogID) 200 | _G.isUIOpen = false 201 | return CloseDialog(dialogID) 202 | end) 203 | 204 | exports('GetDialogState', GetStateDialog) 205 | 206 | --[[ 207 | exports['LGF_UI']:RegisterDialog(data) 208 | exports['LGF_UI']:CloseDialog(dialogID) 209 | exports['LGF_UI']:GetDialogState() 210 | ]] 211 | -------------------------------------------------------------------------------- /client/component/input.lua: -------------------------------------------------------------------------------- 1 | InputMetaTable = {} 2 | InputMetaTable.__index = InputMetaTable 3 | local INPUT_FIELDS = {} 4 | local CB = nil 5 | LocalPlayer.state.InputOpened = false 6 | 7 | function InputMetaTable.new(inputID, inputTitle, fields, canClose, titleButton) 8 | local self = setmetatable({}, InputMetaTable) 9 | self.id = inputID 10 | self.title = inputTitle 11 | self.fields = fields 12 | self.canClose = canClose 13 | self.titleButton = titleButton 14 | return self 15 | end 16 | 17 | function InputMetaTable:getField(index) 18 | return self.fields[index] 19 | end 20 | 21 | function InputMetaTable:submitFields(data) 22 | local fieldValues = {} 23 | 24 | for index, field in ipairs(self.fields) do 25 | local value = data[index] or "" 26 | fieldValues[field.label] = value 27 | if field.onSubmit then 28 | field.onSubmit(value) 29 | end 30 | end 31 | 32 | return fieldValues 33 | end 34 | 35 | local function sendNuiMessage(action, data) 36 | SendNUIMessage({ 37 | action = action, 38 | data = data 39 | }) 40 | end 41 | 42 | local function showInputForm(inputID) 43 | -- if _G.isUIOpen then return end 44 | local Focused = IsNuiFocused() 45 | SetNuiFocus(not Focused, not Focused) 46 | local inputData = INPUT_FIELDS[inputID] 47 | if inputData then 48 | sendNuiMessage("showInputForm", { 49 | id = inputData.id, 50 | title = inputData.title, 51 | fields = inputData.fields, 52 | canClose = inputData.canClose, 53 | titleButton = inputData.titleButton, 54 | }) 55 | LocalPlayer.state.InputOpened = true 56 | _G.isUIOpen = true 57 | end 58 | end 59 | 60 | 61 | local function closeInputForm(inputID) 62 | local inputData = INPUT_FIELDS[inputID] 63 | if inputData then 64 | local Focused = IsNuiFocused() 65 | if Focused then 66 | SetNuiFocus(false, false) 67 | end 68 | sendNuiMessage("closeInputForm", { 69 | id = inputData.id, 70 | }) 71 | LocalPlayer.state.InputOpened = false 72 | _G.isUIOpen = false 73 | else 74 | print(('Error: Input ID %s not found in INPUT_FIELDS. Available IDs: %s'):format(inputID, 75 | json.encode(INPUT_FIELDS, { indent = true }))) 76 | end 77 | end 78 | 79 | RegisterNuiCallback('input:Close', function(data, cb) 80 | cb('ok') 81 | closeInputForm(data.inputID) 82 | CB:resolve(nil) 83 | LocalPlayer.state.InputOpened = false 84 | _G.isUIOpen = false 85 | end) 86 | 87 | local function registerInput(inputID, inputTitle, fields, canClose, titleButton) 88 | CB = promise.new() 89 | if not inputID or not inputTitle or not fields then 90 | print(('Error: Missing data in registerInput. InputID: %s, InputTitle: %s, Fields: %s'):format( 91 | tostring(inputID or "nil"), 92 | tostring(inputTitle or "nil"), 93 | tostring(fields or "nil") 94 | )) 95 | return 96 | end 97 | 98 | INPUT_FIELDS[inputID] = InputMetaTable.new(inputID, inputTitle, fields, canClose, titleButton) 99 | showInputForm(inputID) 100 | return Citizen.Await(CB) 101 | end 102 | 103 | RegisterNuiCallback('LGF_UI.GetInputData', function(data, cb) 104 | local inputID = data.inputID 105 | local inputData = INPUT_FIELDS[inputID] 106 | if inputData then 107 | cb({ 108 | id = inputData.id, 109 | title = inputData.title, 110 | fields = inputData.fields, 111 | canClose = inputData.canClose, 112 | titleButton = inputData.titleButton, 113 | }) 114 | else 115 | cb(nil) 116 | end 117 | end) 118 | 119 | RegisterNuiCallback('input:Submit', function(data, cb) 120 | local input = INPUT_FIELDS[data.inputID] 121 | if input then 122 | local success = input:submitFields(data.fields) 123 | local Focused = IsNuiFocused() 124 | if Focused then 125 | SetNuiFocus(false, false) 126 | end 127 | cb(success) 128 | CB:resolve(true) 129 | else 130 | cb(false) 131 | end 132 | end) 133 | 134 | local function GetInputState() 135 | return LocalPlayer.state.InputOpened 136 | end 137 | 138 | local function ForceCloseInput() 139 | closeInputForm(nil) 140 | LocalPlayer.state.InputOpened = false 141 | end 142 | 143 | 144 | exports('RegisterInput', registerInput) 145 | exports('CloseInput', closeInputForm) 146 | exports('ShowInput', showInputForm) 147 | exports('GetInputState', GetInputState) 148 | exports('ForceCloseInput', ForceCloseInput) 149 | -------------------------------------------------------------------------------- /client/component/instructButton.lua: -------------------------------------------------------------------------------- 1 | local Instruct = {} 2 | LocalPlayer.state.instructOpened = false 3 | 4 | local function sendNuiMessage(action, data) 5 | LocalPlayer.state.instructOpened = data.visible 6 | SendNUIMessage({ 7 | action = action, 8 | data = data 9 | }) 10 | end 11 | 12 | local Cached = {} 13 | 14 | local function handleKeyBindPressed(keyBind) 15 | if Cached.onBindPressed then 16 | Cached.onBindPressed(keyBind) 17 | end 18 | end 19 | 20 | local function handleKeyBindReleased(keyBind) 21 | if Cached.onBindPressed then 22 | Cached.onBindReleased(keyBind) 23 | end 24 | end 25 | 26 | 27 | local function loop(controls) 28 | while LocalPlayer.state.instructOpened do 29 | Wait(0) 30 | for _, control in pairs(controls) do 31 | if IsControlJustPressed(0, control.indexKey) then 32 | handleKeyBindPressed(control.indexKey) 33 | end 34 | if IsControlJustReleased(0, control.indexKey) then 35 | handleKeyBindReleased(control.indexKey) 36 | end 37 | end 38 | end 39 | end 40 | 41 | function Instruct.OpenControlInstructional(data) 42 | if data.Visible then 43 | Cached = data 44 | sendNuiMessage("openInstructionalButt", { 45 | visible = true, 46 | controls = data.Controls or {}, 47 | schema = data.Schema or { 48 | Styles = {} 49 | }, 50 | }) 51 | 52 | CreateThread(function() 53 | loop(data.Controls) 54 | end) 55 | else 56 | sendNuiMessage("openInstructionalButt", { 57 | visible = false, 58 | controls = Cached.Controls or {}, 59 | schema = Cached.Schema 60 | }) 61 | end 62 | end 63 | 64 | function Instruct.CloseControlInstructional() 65 | Instruct.OpenControlInstructional({ 66 | Visible = false 67 | }) 68 | end 69 | 70 | exports("interactionButton", Instruct.OpenControlInstructional) 71 | exports("closeInteraction", Instruct.CloseControlInstructional) 72 | exports("getStateInteraction", function() return LocalPlayer.state.instructOpened end) 73 | 74 | return Instruct 75 | -------------------------------------------------------------------------------- /client/component/notification.lua: -------------------------------------------------------------------------------- 1 | ---@[[TYPES]] 2 | ---@class NotificationData 3 | ---@field id string Unique ID for the notification 4 | ---@field title string Title of the notification 5 | ---@field message string Message of the notification 6 | ---@field icon string Icon type (e.g., "success", "error", "progress", "line") 7 | ---@field duration number Duration of the notification in milliseconds 8 | ---@field position string Position of the notification (e.g., "top-left", "top-right", "bottom-left", "bottom-right") 9 | 10 | 11 | ---@param data NotificationData 12 | local function showNotification(data) 13 | local message = data.message 14 | local title = data.title 15 | local icon = data.icon 16 | local duration = data.duration 17 | local position = data.position 18 | local id = data.id 19 | 20 | SendNUIMessage({ 21 | action = "SendNotification", 22 | id = id, 23 | title = title, 24 | message = message, 25 | icon = icon, 26 | duration = duration, 27 | position = position 28 | }) 29 | end 30 | 31 | 32 | RegisterNetEvent('LGF_Utility:SendNotification', function(data) 33 | showNotification({ 34 | id = data.id, 35 | title = data.title, 36 | message = data.message, 37 | icon = data.icon, 38 | duration = data.duration, 39 | position = data.position 40 | }) 41 | end) 42 | 43 | 44 | exports('SendNotification', showNotification) 45 | 46 | -- Example usage: 47 | --[[ 48 | TriggerEvent('LGF_Utility:SendNotification', { 49 | id = "progress1", 50 | title = "Processing", 51 | message = "Your request is being processed.", 52 | icon = "progress", 53 | duration = 5000, 54 | position = 'top-right' 55 | }) 56 | 57 | exports['LGF_Utility']:SendNotification({ 58 | id = "example1", 59 | title = "Hello", 60 | message = 'This is a notification example.', 61 | icon = 'success', 62 | duration = 5000, 63 | position = 'top-left' 64 | }) 65 | ]] 66 | 67 | 68 | -------------------------------------------------------------------------------- /client/component/progressBar.lua: -------------------------------------------------------------------------------- 1 | PROGRESS = {} 2 | PROGRESSOPENED = false 3 | LocalPlayer.state.progressOpen = false 4 | local disabledControls = {} 5 | 6 | function delay(ms) 7 | local co = coroutine.running() 8 | Citizen.SetTimeout(ms, function() 9 | coroutine.resume(co) 10 | end) 11 | return coroutine.yield() 12 | end 13 | 14 | function PROGRESS:CreateProgress(data) 15 | if _G.isUIOpen then return end 16 | local message = data.message 17 | local colorProgress = data.colorProgress or "rgba(54, 156, 129, 0.381)" 18 | local position = data.position or "center" 19 | local duration = data.duration or 5000 20 | local transition = data.transition or "fade" 21 | local typeBar = "linear" 22 | local onFinish = data.onFinish 23 | local disableBind = data.disableBind 24 | local disableKeyBind = data.disableKeyBind or {} 25 | 26 | 27 | SendNUIMessage({ 28 | action = "showProgressBar", 29 | message = message, 30 | colorProgress = colorProgress, 31 | position = position, 32 | duration = duration, 33 | transition = transition, 34 | typeBar = typeBar 35 | }) 36 | 37 | LocalPlayer.state.progressOpen = true 38 | _G.isUIOpen = true 39 | 40 | CreateThread(function() 41 | local startTime = GetGameTimer() 42 | 43 | while LocalPlayer.state.progressOpen do 44 | Wait(0) 45 | 46 | for _, key in ipairs(disableKeyBind) do 47 | DisableControlAction(0, key, true) 48 | disabledControls[key] = true 49 | end 50 | 51 | 52 | if disableBind and IsControlJustPressed(0, disableBind) then 53 | PROGRESS:DisableProgressBar() 54 | return 55 | end 56 | 57 | 58 | if GetGameTimer() - startTime >= duration then 59 | delay(200) 60 | PROGRESS:DisableProgressBar() 61 | if onFinish then 62 | onFinish() 63 | end 64 | return 65 | end 66 | end 67 | end) 68 | end 69 | 70 | function PROGRESS:DisableProgressBar() 71 | SendNUIMessage({ action = "hideProgressBar" }) 72 | LocalPlayer.state.progressOpen = false 73 | _G.isUIOpen = false 74 | for key, _ in pairs(disabledControls) do 75 | EnableControlAction(0, key, true) 76 | end 77 | 78 | disabledControls = {} 79 | end 80 | 81 | AddEventHandler('onResourceStop', function(res) 82 | if not res == "LGF_UI" then return end 83 | if LocalPlayer.state.progressOpen then 84 | PROGRESS:DisableProgressBar() 85 | end 86 | 87 | if TEXTUI:GetStateTextUI() then 88 | TEXTUI:HideTextUI() 89 | end 90 | end) 91 | 92 | exports('DisableProgressBar', function() return PROGRESS:DisableProgressBar() end) 93 | exports('CreateProgressBar', function(data) return PROGRESS:CreateProgress(data) end) 94 | exports('GetStateProgress', function() return LocalPlayer.state.progressOpen end) 95 | -------------------------------------------------------------------------------- /client/component/textUI.lua: -------------------------------------------------------------------------------- 1 | TEXTUI = {} 2 | 3 | LocalPlayer.state.textUiOpen = false 4 | 5 | ---@class TextUIData 6 | ---@field title string The title of the Text UI (optional) 7 | ---@field message string The message to display 8 | ---@field colorProgress string Progress color (optional) 9 | ---@field keyBind string Keybind to display (optional, only if useKeybind is true also show the loader) 10 | ---@field position string Position of the UI (e.g., "center", "center-left", "center-right") 11 | ---@field useKeybind boolean Show the keybind (optional) 12 | ---@field useProgress boolean Show the loader (optional) 13 | 14 | ---@param data TextUIData 15 | function TEXTUI:OpenTextUI(data) 16 | local message = data.message 17 | local colorProgress = data.colorProgress or "rgba(54, 156, 129, 0.381)" 18 | local keyBind = data.keyBind or "" 19 | local position = data.position or "center-right" 20 | local useKeybind = data.useKeybind or false 21 | local useProgress = data.useProgress or false 22 | local title = data.title 23 | 24 | SendNUIMessage({ 25 | action = "showTextUI", 26 | title = title, 27 | message = message, 28 | colorProgress = useProgress and colorProgress or nil, 29 | keyBind = useKeybind and keyBind or nil, 30 | position = position, 31 | useKeybind = useKeybind, 32 | useProgress = useProgress 33 | }) 34 | 35 | LocalPlayer.state.textUiOpen = true 36 | _G.isUIOpen = true 37 | end 38 | 39 | function TEXTUI:HideTextUI() 40 | SendNUIMessage({ action = "hideTextUI" }) 41 | LocalPlayer.state.textUiOpen = false 42 | _G.isUIOpen = false 43 | end 44 | 45 | function TEXTUI:GetStateTextUI() 46 | return LocalPlayer.state.textUiOpen 47 | end 48 | 49 | exports('OpenTextUI', function(data) 50 | TEXTUI:OpenTextUI(data) 51 | _G.isUIOpen = true 52 | end) 53 | 54 | exports('GetStateTextUI', function() 55 | return TEXTUI:GetStateTextUI() 56 | end) 57 | 58 | exports('CloseTextUI', function() 59 | return TEXTUI:HideTextUI() 60 | end) 61 | 62 | 63 | -------------------------------------------------------------------------------- /client/functions.lua: -------------------------------------------------------------------------------- 1 | NUI = {} 2 | _G.isUIOpen = false 3 | LocalPlayer.state.contextOpened = false 4 | 5 | 6 | local function CanOpenContext() 7 | return not Config.isPlayerDead() and 8 | not LocalPlayer.state.invOpen and 9 | not IsPauseMenuActive() and 10 | not _G.isUIOpen 11 | end 12 | 13 | function NUI:showNui(action, menuID, show) 14 | print(show, show) 15 | SetNuiFocus(show, show) 16 | SendNUIMessage({ action = action, data = { menuID = menuID, visible = show } }) 17 | if action == 'CreateMenuContext' then 18 | _G.isUIOpen = show 19 | LocalPlayer.state.contextOpened = show 20 | end 21 | end 22 | 23 | RegisterNuiCallback('UI:CloseContext', function(data, cb) 24 | NUI:showNui(data.name, data.menuID, false) 25 | print(json.encode(data, { indent = true })) 26 | cb(true) 27 | end) 28 | 29 | function NUI:isUIOpen() 30 | return _G.isUIOpen 31 | end 32 | 33 | exports('ShowContextMenu', function(menuID, shouldShow) return NUI:showNui('CreateMenuContext', menuID, shouldShow) end) 34 | 35 | exports('ForceCloseContext', function() 36 | NUI:showNui('CreateMenuContext', nil, false) 37 | _G.isUIOpen = false 38 | end) 39 | 40 | exports('CloseContext', function(menuID) 41 | NUI:showNui('CreateMenuContext', menuID, false) 42 | _G.isUIOpen = false 43 | end) 44 | 45 | 46 | exports('GetContextState', function() return NUI:isUIOpen() end) 47 | exports('CanOpenContext', CanOpenContext) 48 | 49 | --[[ 50 | exports['LGF_Utility']:ShowContextMenu(menuID, show) 51 | exports['LGF_Utility']:IsUiOpen() 52 | exports['LGF_Utility']:RegisterContextMenu(menuID, menuTitle, data) 53 | exports['LGF_Utility']:CloseContext(menuID) 54 | exports['LGF_Utility']:ForceCloseContext() 55 | exports['LGF_Utility']:CanOpenContext() 56 | LocalPlayer.state.ContextOpen 57 | ]] 58 | -------------------------------------------------------------------------------- /client/test.lua: -------------------------------------------------------------------------------- 1 | --[[REFERENCE TABLE WITH VEHICLE FOR CONTEXT]] 2 | local Vehicle = { 3 | { model = 'kuruma', Label = 'Kuruma', Fuel = 24, color = 'Black', type = 'Sedan', maxSpeed = 120 }, 4 | { model = 'sultan', Label = 'Sultan', Fuel = 54, color = 'Silver', type = 'Sport', maxSpeed = 150 }, 5 | { model = 'infernus', Label = 'Infernus', Fuel = 34, color = 'Red', type = 'Super', maxSpeed = 220 }, 6 | { model = 'comet', Label = 'Comet', Fuel = 74, color = 'Blue', type = 'Sport', maxSpeed = 180 }, 7 | { model = 'felon', Label = 'Felon', Fuel = 24, color = 'Green', type = 'Coupe', maxSpeed = 160 }, 8 | { model = 't20', Label = 'T20', Fuel = 54, color = 'Yellow', type = 'Super', maxSpeed = 230 }, 9 | { model = 'voltic', Label = 'Voltic', Fuel = 34, color = 'White', type = 'Electric', maxSpeed = 140 }, 10 | { model = 'banshee', Label = 'Banshee', Fuel = 74, color = 'Gray', type = 'Sport', maxSpeed = 200 }, 11 | { model = 'turismor', Label = 'Turismo R', Fuel = 24, color = 'Purple', type = 'Super', maxSpeed = 250 }, 12 | { model = 'ztype', Label = 'Z-Type', Fuel = 54, color = 'Orange', type = 'Classic', maxSpeed = 170 }, 13 | { model = 'exemplar', Label = 'Exemplar', Fuel = 34, color = 'Brown', type = 'Coupe', maxSpeed = 180 }, 14 | { model = 'osiris', Label = 'Osiris', Fuel = 74, color = 'Pink', type = 'Super', maxSpeed = 240 }, 15 | } 16 | 17 | 18 | local function CreateVehicleTest(vehicleModel) 19 | local ModelHash = vehicleModel 20 | if not IsModelInCdimage(ModelHash) then return end 21 | RequestModel(ModelHash) 22 | while not HasModelLoaded(ModelHash) do 23 | Wait(0) 24 | end 25 | local MyPed = PlayerPedId() 26 | local Vehicle = CreateVehicle(ModelHash, GetEntityCoords(MyPed), GetEntityHeading(MyPed), true, false) 27 | SetModelAsNoLongerNeeded(ModelHash) 28 | TaskWarpPedIntoVehicle(MyPed, Vehicle, -1) 29 | end 30 | 31 | 32 | 33 | 34 | 35 | local function GenerateVehicleMenu(vehicle) 36 | local MenuID = 'vehicle_menu_' .. vehicle.Label 37 | local TITLE = vehicle.Label 38 | exports['LGF_Utility']:RegisterContextMenu(MenuID, TITLE, { 39 | { 40 | labelButton = "Spawn", 41 | label = "Spawn Vehicle", 42 | description = vehicle.Label, 43 | icon = 'arrow-up', 44 | 45 | onSelect = function() 46 | CreateVehicleTest(vehicle.model) 47 | exports['LGF_Utility']:CloseContext(MenuID) 48 | end 49 | }, 50 | { 51 | labelButton = "Refuel", 52 | label = "Refuel Vehicle", 53 | description = vehicle.Label, 54 | icon = 'gas-pump', 55 | onSelect = function() 56 | print('Refueling vehicle: ' .. vehicle.Label) 57 | end 58 | }, 59 | { 60 | labelButton = "Back", 61 | label = "Back to Main Menu", 62 | icon = 'arrow-left', 63 | onSelect = function() 64 | GenerateMainMenu() 65 | end 66 | } 67 | }) 68 | exports['LGF_Utility']:ShowContextMenu(MenuID, true) 69 | end 70 | 71 | function GenerateMainMenu() 72 | local options = {} 73 | for i = 1, #Vehicle do 74 | local v = Vehicle[i] 75 | local disabled = v.Fuel <= 60 76 | table.insert(options, { 77 | label = v.Label, 78 | 79 | description = 'Fuel: ' .. v.Fuel .. '%', 80 | icon = 'car', 81 | disabled = disabled, 82 | progress = v.Fuel, -- ringProgress or progress 83 | colorProgress = 'green', 84 | labelButton = 'Select', 85 | metadata = { 86 | title = "Vehicle Details", 87 | iconTitle = 'car', 88 | metadataValue = { 89 | fuel = v.Fuel, 90 | color = v.color, 91 | maxSpeed = v.maxSpeed, 92 | caca = "Example Data", 93 | tititi = "Example Data", 94 | lolol = "Example Data" 95 | } 96 | }, 97 | onSelect = function() 98 | GenerateVehicleMenu(v) 99 | end 100 | }) 101 | end 102 | 103 | exports['LGF_Utility']:RegisterContextMenu('main_menu', 'Select Vehicle', options) 104 | exports['LGF_Utility']:ShowContextMenu('main_menu', true) 105 | end 106 | 107 | local point = lib.points.new({ 108 | coords = vec3(-1220.5801, -805.1790, 16.6298), 109 | distance = 5, 110 | dunak = 'nerd', 111 | }) 112 | 113 | function point:onEnter() 114 | exports['LGF_Utility']:OpenTextUI({ 115 | message = "OPEN LIFE STYLE SELECTOR", 116 | position = "center-right", 117 | useKeybind = true, 118 | keyBind = "E", 119 | useProgress = false, 120 | }) 121 | end 122 | 123 | function point:onExit() 124 | exports['LGF_Utility']:CloseTextUI() 125 | end 126 | 127 | function point:nearby() 128 | if self.currentDistance < 3 and IsControlJustReleased(0, 38) then 129 | OpenDialogTest() 130 | exports['LGF_Utility']:CloseTextUI() 131 | end 132 | end 133 | 134 | local LifeStyle = { 135 | Crimi = { 136 | ['water'] = 3, 137 | ['WEAPON_PISTOL'] = 1, 138 | ['burger'] = 3, 139 | }, 140 | Police = { 141 | ['bandage'] = 1, 142 | ['water'] = 2, 143 | ['radio'] = 1, 144 | }, 145 | Civilian = { 146 | ['burger'] = 2, 147 | ['sprunk'] = 2, 148 | ['water'] = 2, 149 | }, 150 | Medical = { 151 | ['bandage'] = 5, 152 | ['water'] = 3, 153 | ['sprunk'] = 2, 154 | } 155 | } 156 | 157 | function OpenDialogTest() 158 | exports['LGF_Utility']:RegisterDialog({ 159 | id = 'lifeStyle', 160 | title = 'Life Style', 161 | enableCam = true, 162 | cards = { 163 | { 164 | title = 'Criminal', 165 | message = 166 | 'The criminal lifestyle is characterized by activities outside the law. Those who choose this path often engage in illegal activities such as theft, smuggling, and various forms of organized crime. Embracing a criminal life typically involves high risks and the need for secrecy. If you are interested in acquiring items associated with this lifestyle, you will be provided with resources that may assist you in navigating and thriving within this world of crime. Choose wisely, as each decision has its consequences.', 167 | actionLabel = 'Criminal', 168 | image = 169 | "https://cdn.discordapp.com/attachments/1273666294599913524/1273668480801177622/thumb-1920-1343705.png?ex=66c410f5&is=66c2bf75&hm=bba586457be139e24bfed0fe4b1004de17b50cda885f20c3bccc6ebaa114aa6c&", 170 | onAction = function() 171 | local items = LifeStyle.Criminal 172 | ('Criminal Life Selected') 173 | for item, amount in pairs(items) do 174 | TriggerServerEvent('LGF_Utility:Test:GetStyleItems', item, amount) 175 | end 176 | exports['LGF_Utility']:CloseDialog("lifeStyle") 177 | end, 178 | }, 179 | { 180 | title = 'Police', 181 | message = 182 | 'The police lifestyle involves upholding the law and maintaining public safety. As a police officer, you are tasked with enforcing laws, investigating crimes, and protecting citizens. This role requires integrity, courage, and a strong commitment to justice. By selecting this lifestyle, you will receive items that support your duties in law enforcement, including tools and equipment essential for your role in ensuring peace and order within the community.', 183 | actionLabel = 'Police', 184 | onAction = function() 185 | print('Police Life Selected') 186 | local items = LifeStyle.Police 187 | for item, amount in pairs(items) do 188 | TriggerServerEvent('LGF_Utility:Test:GetStyleItems', item, amount) 189 | end 190 | exports['LGF_Utility']:CloseDialog("lifeStyle") 191 | end, 192 | }, 193 | { 194 | title = 'Civilian', 195 | message = 196 | 'The civilian lifestyle is centered around everyday life outside of specialized roles such as criminal or police work. Civilians live and work in their communities, contributing to society in various ways. This lifestyle is marked by a focus on personal and professional growth within a non-law enforcement or criminal context. By choosing this option, you will gain access to items that enhance your experience in a typical civilian role, helping you in your daily activities and interactions.', 197 | actionLabel = 'Civilian', 198 | onAction = function() 199 | local items = LifeStyle.Civilian 200 | print('Civilian Life Selected') 201 | for item, amount in pairs(items) do 202 | TriggerServerEvent('LGF_Utility:Test:GetStyleItems', item, amount) 203 | end 204 | exports['LGF_Utility']:CloseDialog("lifeStyle") 205 | end, 206 | }, 207 | { 208 | title = 'Medical', 209 | message = 210 | 'The medical lifestyle focuses on health and emergency care. Individuals in this role are dedicated to diagnosing, treating, and caring for patients in various medical settings. Whether working in hospitals, clinics, or emergency services, medical professionals play a crucial role in maintaining public health and providing critical care. By choosing the medical lifestyle, you will receive items and resources that are essential for performing medical duties and supporting your role in healthcare.', 211 | actionLabel = 'Medic', 212 | onAction = function() 213 | print('Medical Life Selected') 214 | local items = LifeStyle.Medical 215 | for item, amount in pairs(items) do 216 | TriggerServerEvent('LGF_Utility:Test:GetStyleItems', item, amount) 217 | end 218 | exports['LGF_Utility']:CloseDialog("lifeStyle") 219 | end, 220 | } 221 | 222 | } 223 | }) 224 | end 225 | 226 | RegisterCommand("dia", function() 227 | print("dwa") 228 | OpenDialogTest() 229 | end) 230 | 231 | RegisterCommand('testNotifications', function() 232 | -- Notification 1: Success 233 | TriggerEvent('LGF_Utility:SendNotification', { 234 | id = "success1", 235 | title = "Success", 236 | message = "Operation completed successfully.", 237 | icon = "success", 238 | duration = 3000, 239 | position = 'top-right' 240 | }) 241 | 242 | -- Notification 2: Error 243 | TriggerEvent('LGF_Utility:SendNotification', { 244 | id = "error1", 245 | title = "Error", 246 | message = "An error occurred during the operation.", 247 | icon = "error", 248 | duration = 3000, 249 | position = 'top-left' 250 | }) 251 | 252 | -- Notification 3: Progress 253 | TriggerEvent('LGF_Utility:SendNotification', { 254 | id = "progress1", 255 | title = "Processing", 256 | message = "Your request is being processed.", 257 | icon = "progress", 258 | duration = 5000, 259 | position = 'bottom-right' 260 | }) 261 | 262 | -- Notification 4: Line 263 | TriggerEvent('LGF_Utility:SendNotification', { 264 | id = "line1", 265 | title = "Update Available", 266 | message = "A new update is available. Please download it.", 267 | icon = "line", 268 | duration = 4000, 269 | position = 'bottom-left' 270 | }) 271 | 272 | -- Notification 5: Custom 273 | TriggerEvent('LGF_Utility:SendNotification', { 274 | id = "custom1", 275 | title = "Welcome", 276 | message = "Welcome to the server! Have a great time.", 277 | icon = "success", 278 | duration = 4000, 279 | position = 'top-right' 280 | }) 281 | end) 282 | 283 | 284 | 285 | local function registerInputForm() 286 | local inputData = {} 287 | local INPUT_REGISTERED = exports['LGF_Utility']:RegisterInput('example_form', 'Example Input Form', { 288 | { 289 | label = 'Name', 290 | placeholder = 'Enter your name', 291 | type = 'text', 292 | required = true, 293 | onSubmit = function(value) 294 | inputData.Name = value 295 | end 296 | }, 297 | { 298 | label = 'Age', 299 | placeholder = 'Enter your age', 300 | type = 'number', 301 | min = 0, 302 | max = 120, 303 | required = true, 304 | onSubmit = function(value) 305 | inputData.Age = value 306 | end 307 | }, 308 | { 309 | label = 'Gender', 310 | type = 'select', 311 | options = { 312 | { label = 'Male', value = 'male' }, 313 | { label = 'Female', value = 'female' }, 314 | { label = 'Other', value = 'other' } 315 | }, 316 | required = true, 317 | onSubmit = function(value) 318 | inputData.Gender = value 319 | end 320 | }, 321 | { 322 | label = 'Password', 323 | placeholder = 'Enter your password', 324 | type = 'password', 325 | required = true, 326 | onSubmit = function(value) 327 | inputData.Password = value 328 | end 329 | }, 330 | { 331 | label = 'Biography', 332 | placeholder = 'Tell us about yourself', 333 | type = 'textarea', 334 | required = false, 335 | onSubmit = function(value) 336 | inputData.Biography = value 337 | end 338 | }, 339 | { 340 | label = 'Vehicle Plate', 341 | placeholder = "23FF35B", 342 | type = 'text', 343 | disabledInput = true, 344 | onSubmit = function(value) 345 | local Plate = "DAWDWAD" 346 | inputData.VehiclePlate = Plate 347 | end 348 | } 349 | }, true, "label button") -- Input can Close? 350 | 351 | if INPUT_REGISTERED then 352 | print(json.encode(inputData, { indent = true, empty_table_as_array = true })) 353 | else 354 | print('Failed to register input form') 355 | end 356 | end 357 | 358 | 359 | RegisterCommand('mostrainput', function() 360 | registerInputForm() 361 | end) 362 | 363 | RegisterCommand('progress', function() 364 | PROGRESS:CreateProgress({ 365 | message = "Test Progress Bar", 366 | colorProgress = "#0ca678", 367 | position = "bottom", 368 | duration = 6000, 369 | disableBind = 38, 370 | disableKeyBind = { 24, 32, 33, 34, 30, 31, 36, 21, }, 371 | onFinish = function() 372 | print("Progress bar closed") 373 | end 374 | }) 375 | end) 376 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'cerulean' 2 | game 'gta5' 3 | version '1.0.9' 4 | lua54 'yes' 5 | use_fxv2_oal 'yes' 6 | author 'ENT510' 7 | description 'UI library for fivem' 8 | 9 | shared_scripts { 10 | "@ox_lib/init.lua", 11 | "init.lua", 12 | 'shared/*.lua', 13 | } 14 | 15 | client_scripts { 16 | 'client/**/*', 17 | 'game/client/*.lua', 18 | 'game/client/class/*.lua', 19 | 'game/framework/client.lua', 20 | } 21 | 22 | server_scripts { 23 | '@oxmysql/lib/MySQL.lua', 24 | 'server/**/*', 25 | 'game/server/*.lua', 26 | 'game/framework/server.lua', 27 | } 28 | 29 | files { 'web/build/index.html', 'web/build/**/*', } 30 | ui_page 'web/build/index.html' 31 | -------------------------------------------------------------------------------- /game/client/cb.lua: -------------------------------------------------------------------------------- 1 | local ClientCallback = {} 2 | local clientCallbacks = {} 3 | local currentRequestId = 0 4 | local serverCallbacks = {} 5 | local responses = {} 6 | 7 | function LGF:RegisterClientCallback(name, cb) 8 | clientCallbacks[name] = cb 9 | end 10 | 11 | function LGF:TriggerClientCallback(name, ...) 12 | local invoker = GetInvokingResource() 13 | if clientCallbacks[name] then 14 | local result = clientCallbacks[name](...) 15 | return result 16 | else 17 | LGF:logError("Client Callback Not Found. Name: %s , Invoker: %s", name, invoker) 18 | return nil 19 | end 20 | end 21 | 22 | function LGF:TriggerServerCallback(name, ...) 23 | currentRequestId = currentRequestId + 1 24 | local requestId = currentRequestId 25 | local timeout = 5000 26 | local startTime = GetGameTimer() 27 | local invoker = GetInvokingResource() 28 | 29 | serverCallbacks[requestId] = function(result) 30 | responses[requestId] = result 31 | end 32 | 33 | TriggerServerEvent("LGF_UI:server:Callback", name, requestId, invoker, ...) 34 | 35 | while responses[requestId] == nil do 36 | Citizen.Wait(0) 37 | if GetGameTimer() - startTime > timeout then 38 | LGF:logError(("Timeout for invoker Resource: %s, Callback Name: %s"):format(invoker, name)) 39 | responses[requestId] = nil 40 | break 41 | end 42 | end 43 | 44 | local result = responses[requestId] 45 | responses[requestId] = nil 46 | return result 47 | end 48 | 49 | RegisterNetEvent("LGF_UI:client:CallbackResponse") 50 | AddEventHandler("LGF_UI:client:CallbackResponse", function(requestId, result) 51 | -- LGF:DebugValue("Received callback response for RequestId: %s, Result: %s", requestId, tostring(result)) 52 | if serverCallbacks[requestId] then 53 | serverCallbacks[requestId](result) 54 | serverCallbacks[requestId] = nil 55 | else 56 | LGF:DebugValue("Callback function not found for requestId:", requestId) 57 | end 58 | end) 59 | 60 | 61 | exports('TriggerServerCallback', function(name, ...) 62 | return LGF:TriggerServerCallback(name, ...) 63 | end) 64 | 65 | exports('RegisterClientCallback', function(name, fun) 66 | return LGF:RegisterClientCallback(name, fun) 67 | end) 68 | 69 | exports('TriggerClientCallback', function(name, ...) 70 | return LGF:TriggerClientCallback(name, ...) 71 | end) 72 | 73 | 74 | return ClientCallback 75 | -------------------------------------------------------------------------------- /game/client/class/VehicleEvent.lua: -------------------------------------------------------------------------------- 1 | VehicleMonitor = {} 2 | VehicleMonitor.__index = VehicleMonitor 3 | 4 | function VehicleMonitor:new() 5 | local instance = setmetatable({}, VehicleMonitor) 6 | instance.previousVehicle = nil 7 | instance.previousSeat = nil 8 | instance.previousPlate = nil 9 | instance.isInVehicle = false 10 | self:startMonitoring(instance) 11 | return instance 12 | end 13 | 14 | function VehicleMonitor:startMonitoring(instance) 15 | CreateThread(function() 16 | while true do 17 | local isInVehicle, currentVehicle = instance:pedIsInVehicle() 18 | local currentSeat = -1 19 | 20 | if isInVehicle then 21 | if not instance.isInVehicle then 22 | instance.isInVehicle = true 23 | end 24 | 25 | for i = -1, GetVehicleMaxNumberOfPassengers(currentVehicle) - 1 do 26 | if GetPedInVehicleSeat(currentVehicle, i) == LGF.Player:Ped() then 27 | currentSeat = i 28 | break 29 | end 30 | end 31 | 32 | local currentPlate = GetVehicleNumberPlateText(currentVehicle) 33 | local currentNetId = NetworkGetNetworkIdFromEntity(currentVehicle) 34 | 35 | if currentVehicle ~= instance.previousVehicle then 36 | if instance.previousVehicle then 37 | TriggerEvent('LGF_Utility:Vehicle:Exit', instance.previousVehicle, instance.previousSeat,NetworkGetNetworkIdFromEntity(instance.previousVehicle)) 38 | end 39 | TriggerEvent('LGF_Utility:Vehicle:Enter', currentVehicle, currentSeat, currentNetId) 40 | end 41 | 42 | if currentSeat ~= instance.previousSeat then 43 | TriggerEvent('LGF_Utility:Vehicle:SeatChange', currentVehicle, currentSeat, currentNetId) 44 | end 45 | 46 | instance.previousVehicle = currentVehicle 47 | instance.previousSeat = currentSeat 48 | instance.previousPlate = currentPlate 49 | else 50 | if instance.previousVehicle then 51 | TriggerEvent('LGF_Utility:Vehicle:Exit', instance.previousVehicle, instance.previousSeat,NetworkGetNetworkIdFromEntity(instance.previousVehicle)) 52 | instance.previousVehicle = nil 53 | instance.previousSeat = nil 54 | instance.isInVehicle = false 55 | end 56 | end 57 | 58 | Wait(1000) 59 | end 60 | end) 61 | end 62 | 63 | function VehicleMonitor:pedIsInVehicle() 64 | local playerPed = LGF.Player:Ped() 65 | local currentVehicle = GetVehiclePedIsIn(playerPed, false) 66 | return currentVehicle ~= 0, currentVehicle 67 | end 68 | 69 | CreateThread(function() VehicleMonitor:new() end) 70 | 71 | 72 | exports("pedIsInVehicle", function() 73 | return VehicleMonitor:pedIsInVehicle() 74 | end) 75 | -------------------------------------------------------------------------------- /game/client/entity.lua: -------------------------------------------------------------------------------- 1 | local PED = {} 2 | local OBJECT = {} 3 | local VEHICLE = {} 4 | 5 | 6 | 7 | 8 | function LGF:CreateEntityPed(data) 9 | local model = data.model 10 | local position = data.position 11 | local scenario = data.scenario or nil 12 | local freeze = data.freeze or false 13 | local isNetworked = data.isNetworked or false 14 | local invincible = data.invincible or true 15 | local blockingEvents = data.blockingEvents or true 16 | 17 | local loaded, modelHash = self:RequestEntityModel(model, 3000) 18 | if not loaded then return end 19 | 20 | local createdPed = CreatePed(4, modelHash, position.x, position.y, position.z - 1, position.w or 0.0, isNetworked, true) 21 | if scenario then 22 | TaskStartScenarioInPlace(createdPed, scenario, -1, true) 23 | end 24 | 25 | SetEntityInvincible(createdPed, invincible) 26 | SetBlockingOfNonTemporaryEvents(createdPed, blockingEvents) 27 | 28 | NetworkRegisterEntityAsNetworked(createdPed) 29 | SetEntityAsMissionEntity(createdPed, true, true) 30 | local NETID = NetworkGetNetworkIdFromEntity(createdPed) 31 | 32 | if freeze then 33 | FreezeEntityPosition(createdPed, freeze) 34 | end 35 | 36 | PED[NETID] = { 37 | EntityID = createdPed, 38 | EntityHash = modelHash, 39 | netid = NETID, 40 | coords = position, 41 | created = true, 42 | } 43 | 44 | SetModelAsNoLongerNeeded(modelHash) 45 | 46 | return createdPed 47 | end 48 | 49 | function LGF:CreateEntityObject(data) 50 | local model = data.model 51 | local position = data.position 52 | local isNetworked = data.isNetworked or false 53 | local freeze = data.freeze or false 54 | local missionEntity = data.missionEntity or false 55 | 56 | local loaded, modelHash = self:RequestEntityModel(model, 3000) 57 | if not loaded then return end 58 | 59 | local createdObject = CreateObject(modelHash, position.x, position.y, position.z, isNetworked, missionEntity, false) 60 | SetEntityHeading(createdObject, position.w) 61 | PlaceObjectOnGroundProperly(createdObject) 62 | SetModelAsNoLongerNeeded(modelHash) 63 | NetworkRegisterEntityAsNetworked(createdObject) 64 | local NETID = NetworkGetNetworkIdFromEntity(createdObject) 65 | 66 | if freeze then 67 | FreezeEntityPosition(createdObject, freeze) 68 | end 69 | 70 | OBJECT[NETID] = { 71 | EntityID = createdObject, 72 | EntityHash = modelHash, 73 | netid = NETID, 74 | coords = position, 75 | created = true, 76 | } 77 | 78 | return createdObject 79 | end 80 | 81 | function LGF:CreateEntityVehicle(data) 82 | local PROM = promise.new() 83 | local model = data.model 84 | local position = data.position 85 | local isNetworked = data.isNetworked or false 86 | local seatPed = data.seatPed or false 87 | local seat = data.seat or -1 88 | local freeze = data.freeze or false 89 | 90 | local loaded, modelHash = self:RequestEntityModel(model, 3000) 91 | if not loaded then 92 | PROM:reject("Failed to load model") 93 | return Citizen.Await(PROM) 94 | end 95 | 96 | local createdVehicle = CreateVehicle(modelHash, position.x, position.y, position.z, position.w, isNetworked, false) 97 | SetVehicleOnGroundProperly(createdVehicle) 98 | SetModelAsNoLongerNeeded(modelHash) 99 | NetworkRegisterEntityAsNetworked(createdVehicle) 100 | SetVehicleHasBeenOwnedByPlayer(createdVehicle, true) 101 | local NETID = NetworkGetNetworkIdFromEntity(createdVehicle) 102 | SetNetworkIdCanMigrate(NETID, true) 103 | 104 | if seatPed then 105 | TaskWarpPedIntoVehicle(PlayerPedId(), createdVehicle, seat) 106 | end 107 | 108 | if freeze then 109 | FreezeEntityPosition(createdVehicle, freeze) 110 | end 111 | 112 | VEHICLE[NETID] = { 113 | EntityID = createdVehicle, 114 | EntityHash = modelHash, 115 | netid = NETID, 116 | coords = position, 117 | created = true, 118 | } 119 | 120 | PROM:resolve(createdVehicle) 121 | 122 | if data.onCreated then 123 | data.onCreated(createdVehicle) 124 | end 125 | 126 | return Citizen.Await(PROM) 127 | end 128 | 129 | AddEventHandler('onResourceStop', function(resourceName) 130 | if GetCurrentResourceName() == resourceName then 131 | for _, entity in pairs(PED) do 132 | if DoesEntityExist(entity.EntityID) then 133 | DeleteEntity(entity.EntityID) 134 | end 135 | end 136 | for _, entity in pairs(OBJECT) do 137 | if DoesEntityExist(entity.EntityID) then 138 | DeleteEntity(entity.EntityID) 139 | end 140 | end 141 | for _, entity in pairs(VEHICLE) do 142 | if DoesEntityExist(entity.EntityID) then 143 | DeleteEntity(entity.EntityID) 144 | end 145 | end 146 | end 147 | end) 148 | 149 | 150 | function LGF:GetAllEntityPed() 151 | local peds = {} 152 | for _, data in pairs(PED) do 153 | table.insert(peds, data) 154 | end 155 | return peds 156 | end 157 | 158 | function LGF:GetAllEntityObjects() 159 | local objects = {} 160 | for _, data in pairs(OBJECT) do 161 | table.insert(objects, data) 162 | end 163 | return objects 164 | end 165 | 166 | function LGF:GetAllEntityVehicles() 167 | local vehicles = {} 168 | for _, data in pairs(VEHICLE) do 169 | table.insert(vehicles, data) 170 | end 171 | return vehicles 172 | end 173 | 174 | exports("CreateEntityPed", function(data) return LGF:CreateEntityPed(data) end) 175 | exports("CreateEntityObject", function(data) return LGF:CreateEntityObject(data) end) 176 | exports("CreateEntityVehicle", function(data) return LGF:CreateEntityVehicle(data) end) 177 | exports("GetAllEntityPed", function() return LGF:GetAllEntityPed() end) 178 | exports("GetAllEntityObjects", function() return LGF:GetAllEntityObjects() end) 179 | exports("GetAllEntityVehicles", function() return LGF:GetAllEntityVehicles() end) 180 | -------------------------------------------------------------------------------- /game/client/raycast.lua: -------------------------------------------------------------------------------- 1 | LGF.RaycastHandler = {} 2 | 3 | function LGF.RaycastHandler:drawLine(startPosition, endPosition, lineColor) 4 | DrawLine(startPosition.x, startPosition.y, startPosition.z, endPosition.x, endPosition.y, endPosition.z, lineColor.r, 5 | lineColor.g, lineColor.b, lineColor.a) 6 | end 7 | 8 | local function getDirectionFromCameraRotation(cameraRotation) 9 | local radians = { x = math.rad(cameraRotation.x), y = math.rad(cameraRotation.y), z = math.rad(cameraRotation.z) } 10 | return { 11 | x = -math.sin(radians.z) * math.abs(math.cos(radians.x)), 12 | y = math.cos(radians.z) * 13 | math.abs(math.cos(radians.x)), 14 | z = math.sin(radians.x) 15 | } 16 | end 17 | 18 | function LGF.RaycastHandler:performRaycast(maxDistance, drawMarker, drawLine) 19 | local playerPed = PlayerPedId() 20 | local chestBoneIndex = GetPedBoneIndex(playerPed, 0x796e) 21 | local chestPosition = GetWorldPositionOfEntityBone(playerPed, chestBoneIndex) 22 | local direction = getDirectionFromCameraRotation(GetGameplayCamRot()) 23 | 24 | local raycastDestination = { 25 | x = chestPosition.x + direction.x * maxDistance, 26 | y = chestPosition.y + direction.y * maxDistance, 27 | z = chestPosition.z + direction.z * maxDistance 28 | } 29 | 30 | local rayHandle = StartShapeTestRay(chestPosition.x, chestPosition.y, chestPosition.z,raycastDestination.x, raycastDestination.y, raycastDestination.z, 511, playerPed, 0) 31 | 32 | local success, hit, hitCoordinates, surfaceNormal, entityHit = GetShapeTestResult(rayHandle) 33 | 34 | if hit then 35 | if IsEntityAVehicle(entityHit) or IsEntityAPed(entityHit) or IsEntityAnObject(entityHit) then 36 | if drawMarker and hitCoordinates then 37 | DrawMarker(28, hitCoordinates.x, hitCoordinates.y, hitCoordinates.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.1, 0.1, 255, 0, 0, 100, false, true, 2, nil, nil, false) 38 | end 39 | 40 | if drawLine and hitCoordinates then 41 | self:drawLine(chestPosition, hitCoordinates, { r = 255, g = 0, b = 0, a = 255 }) 42 | end 43 | return true, entityHit, hitCoordinates, surfaceNormal 44 | else 45 | if drawMarker and hitCoordinates then 46 | DrawMarker(28, hitCoordinates.x, hitCoordinates.y, hitCoordinates.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.1, 0.1, 0, 255, 0, 100, false, true, 2, nil, nil, false) 47 | end 48 | 49 | if drawLine and hitCoordinates then 50 | self:drawLine(chestPosition, hitCoordinates, { r = 0, g = 255, b = 0, a = 255 }) 51 | end 52 | 53 | return true, nil, hitCoordinates, surfaceNormal 54 | end 55 | end 56 | 57 | if drawLine then 58 | self:drawLine(chestPosition, raycastDestination, { r = 0, g = 255, b = 0, a = 255 }) 59 | end 60 | 61 | return false, nil, raycastDestination, nil 62 | end 63 | 64 | 65 | function LGF.RaycastHandler:performTargetPlayer(distanceMax, markerType, drawLine) 66 | local playerPed = LGF.Player:Ped() 67 | local players = GetActivePlayers() 68 | local closestPlayer, closestDistance = nil, 9999.0 69 | 70 | for _, player in ipairs(players) do 71 | if player ~= LGF.Player:PlayerId() then 72 | local targetPed = GetPlayerPed(player) 73 | local targetPosition = GetEntityCoords(targetPed) 74 | local distance = Vdist(GetEntityCoords(playerPed), targetPosition) 75 | 76 | if distance < closestDistance and distance < distanceMax then 77 | closestDistance = distance 78 | closestPlayer = targetPed 79 | end 80 | end 81 | end 82 | 83 | if closestPlayer then 84 | local hit, entityHit, hitCoordinates = LGF.RaycastHandler:performRaycast(distanceMax, false, drawLine) 85 | 86 | if hit and entityHit == closestPlayer then 87 | local markerPosition = { 88 | x = GetEntityCoords(closestPlayer).x, 89 | y = GetEntityCoords(closestPlayer).y, 90 | z = GetEntityCoords(closestPlayer).z + 1.5 91 | } 92 | DrawMarker(markerType, markerPosition.x, markerPosition.y, markerPosition.z, 0.0, 0.0, 0.0, 0.0, 180.0, 0.0, 93 | 0.5, 1.5, 0.5, 255, 128, 0, 50, true, true, 2, nil, nil, false) 94 | return GetEntityCoords(closestPlayer), closestPlayer, 95 | GetPlayerServerId(NetworkGetPlayerIndexFromPed(closestPlayer)) 96 | end 97 | end 98 | 99 | return nil, nil, nil 100 | end 101 | 102 | -------------------------------------------------------------------------------- /game/client/utility.lua: -------------------------------------------------------------------------------- 1 | function LGF:RequestEntityModel(model, timeout) 2 | timeout = timeout or 10000 3 | 4 | if not IsModelInCdimage(model) then 5 | error(("Model %s does not exist or Invalid GameBuild"):format(model), 1) 6 | return false 7 | end 8 | 9 | local success, err = pcall(function() 10 | RequestModel(model) 11 | local startTime = GetGameTimer() 12 | 13 | while not HasModelLoaded(model) do 14 | if GetGameTimer() - startTime > timeout then 15 | error(("Model loading timeout reached for model: %s"):format(model), 2) 16 | end 17 | Wait(500) 18 | end 19 | end) 20 | 21 | if not success then 22 | print(("Error loading model: %s"):format(err)) 23 | return false, err 24 | else 25 | return true, model 26 | end 27 | end 28 | 29 | function LGF:DrawText3D(data) 30 | local isOnScreen, screenX, screenY = World3dToScreen2d(data.position.x, data.position.y, data.position.z + 0.5) 31 | local camX, camY, camZ = table.unpack(GetGameplayCamCoords()) 32 | local distanceScale = (1 / #(vector3(camX, camY, camZ) - data.position)) * 2 33 | local fovScale = (1 / GetGameplayCamFov()) * 100 34 | distanceScale = distanceScale * fovScale 35 | 36 | if isOnScreen then 37 | SetTextScale(0.0, 0.35 * distanceScale) 38 | SetTextFont(0) 39 | SetTextProportional(true) 40 | SetTextColour(data.color[1], data.color[2], data.color[3], data.color[4]) 41 | SetTextDropshadow(0, 0, 0, 0, 255) 42 | SetTextEdge(2, 0, 0, 0, 150) 43 | SetTextDropShadow() 44 | SetTextOutline() 45 | SetTextEntry("STRING") 46 | SetTextCentre(true) 47 | AddTextComponentString(data.message) 48 | DrawText(screenX, screenY) 49 | end 50 | end 51 | 52 | exports("DrawText3D", function(data) return LGF:DrawText3D(data) end) 53 | 54 | exports("RequestEntityModel", function(model, timeout) return LGF:RequestEntityModel(model, timeout) end) 55 | -------------------------------------------------------------------------------- /game/framework/client.lua: -------------------------------------------------------------------------------- 1 | local obj, frameworkName = LGF:GetFramework() 2 | LGF.Core = {} 3 | 4 | local function ERR_CORE(res) 5 | return LGF:logError("Unsupported framework: %s", res) 6 | end 7 | 8 | function LGF.Core:GetPlayer() 9 | if frameworkName == "LEGACYCORE" then 10 | return obj.DATA:GetPlayerObject() or LocalPlayer.state.GetPlayerObject 11 | elseif frameworkName == "es_extended" then 12 | return obj.GetPlayerData() 13 | elseif frameworkName == "qb-core" then 14 | return obj.Functions.GetPlayerData() 15 | else 16 | ERR_CORE(frameworkName) 17 | end 18 | end 19 | 20 | function LGF.Core:GetJob() 21 | local playerData = LGF.Core:GetPlayer() 22 | if frameworkName == "LEGACYCORE" then 23 | return playerData.JobName 24 | elseif frameworkName == "es_extended" then 25 | return playerData.job and playerData.job.name 26 | elseif frameworkName == "qb-core" then 27 | return playerData.job and playerData.job.name 28 | else 29 | ERR_CORE(frameworkName) 30 | end 31 | end 32 | 33 | function LGF.Core:GetJobGrade() 34 | local playerData = LGF.Core:GetPlayer() 35 | if frameworkName == "LEGACYCORE" then 36 | return playerData.JobGrade 37 | elseif frameworkName == "es_extended" then 38 | return playerData.job and playerData.job.grade 39 | elseif frameworkName == "qb-core" then 40 | return playerData.job and playerData.job.grade 41 | else 42 | ERR_CORE(frameworkName) 43 | end 44 | end 45 | 46 | function LGF.Core:GetName() 47 | local playerData = LGF.Core:GetPlayer() 48 | if frameworkName == "LEGACYCORE" then 49 | return string.format("%s", playerData.playerName) 50 | elseif frameworkName == "es_extended" then 51 | return string.format("%s %s", playerData.firstName, playerData.lastName) 52 | elseif frameworkName == "qb-core" then 53 | return string.format("%s %s", playerData.charinfo.firstname, playerData.charinfo.lastname) 54 | else 55 | ERR_CORE(frameworkName) 56 | end 57 | end 58 | 59 | function LGF.Core:GetIdentifier() 60 | local PlayerData = LGF.Core:GetPlayer() 61 | if PlayerData then 62 | if frameworkName == "LEGACYCORE" then 63 | return PlayerData.identifier 64 | elseif frameworkName == "es_extended" then 65 | return PlayerData.identifier 66 | elseif frameworkName == "qb-core" then 67 | return PlayerData.license 68 | end 69 | end 70 | end 71 | 72 | function LGF.Core:GetGender() 73 | local PlayerData = LGF.Core:GetPlayer() 74 | if frameworkName == "LEGACYCORE" then 75 | return PlayerData.sex 76 | elseif frameworkName == "es_extended" then 77 | return PlayerData.sex 78 | elseif frameworkName == "qb-core" then 79 | return PlayerData.charinfo.gender 80 | end 81 | end 82 | 83 | function LGF.Core:GetGroup() 84 | local response = LGF:TriggerServerCallback("LGF_Utility:Bridge:GetPlayerGroup") 85 | return response 86 | end 87 | 88 | function LGF.Core:GetPlayerAccount() 89 | if frameworkName == "LEGACYCORE" then 90 | local promise = obj.DATA:GetPlayerMetadata('accounts') 91 | local Decoded = json.decode(promise) 92 | return { Bank = Decoded.Bank, Cash = Decoded.money } 93 | elseif frameworkName == "es_extended" then 94 | local accounts = obj.PlayerData.accounts 95 | local values = {} 96 | for i = 1, #accounts do 97 | values[accounts.name] = accounts.money 98 | end 99 | return values 100 | elseif frameworkName == "qb-core" then 101 | 102 | end 103 | end 104 | 105 | LocalPlayer.state.IsLoaded = false 106 | 107 | 108 | if frameworkName == "LEGACYCORE" then 109 | RegisterNetEvent('LegacyCore:PlayerLoaded') 110 | AddEventHandler('LegacyCore:PlayerLoaded', function(...) 111 | TriggerEvent("LGF_Utility:PlayerLoaded", ...) 112 | LocalPlayer.state.IsLoaded = true 113 | end) 114 | elseif frameworkName == "es_extended" then 115 | RegisterNetEvent('esx:playerLoaded') 116 | AddEventHandler('esx:playerLoaded', function(...) 117 | TriggerEvent("LGF_Utility:PlayerLoaded", ...) 118 | LocalPlayer.state.IsLoaded = true 119 | end) 120 | elseif frameworkName == "qbx_core" or frameworkName == "qb-core" then 121 | RegisterNetEvent('QBCore:Client:OnPlayerLoaded') 122 | AddEventHandler('QBCore:Client:OnPlayerLoaded', function(...) 123 | TriggerEvent("LGF_Utility:PlayerLoaded", ...) 124 | LocalPlayer.state.IsLoaded = true 125 | end) 126 | end 127 | 128 | if frameworkName == "LEGACYCORE" then 129 | AddEventHandler('LegacyCore:PlayerLogout', function(...) 130 | TriggerEvent("LGF_Utility:PlayerUnloaded", ...) 131 | LocalPlayer.state.IsLoaded = false 132 | end) 133 | elseif frameworkName == "es_extended" then 134 | RegisterNetEvent('esx:onPlayerLogout') 135 | AddEventHandler('esx:onPlayerLogout', function(...) 136 | TriggerEvent("LGF_Utility:PlayerUnloaded", ...) 137 | LocalPlayer.state.IsLoaded = false 138 | end) 139 | elseif frameworkName == "qbx_core" or frameworkName == "qb-core" then 140 | RegisterNetEvent('QBCore:Client:OnPlayerUnload') 141 | AddEventHandler('QBCore:Client:OnPlayerUnload', function(...) 142 | TriggerEvent("LGF_Utility:PlayerUnloaded", ...) 143 | LocalPlayer.state.IsLoaded = false 144 | end) 145 | end 146 | 147 | 148 | function LGF.Core:PlayerLoaded() 149 | return LocalPlayer.state.IsLoaded 150 | end 151 | 152 | 153 | 154 | return { 155 | IsLoaded = LGF.Core.PlayerLoaded, 156 | GetPlayer = LGF.Core.GetPlayer, 157 | GetPlayerJob = LGF.Core.GetJob, 158 | GetName = LGF.Core.GetName, 159 | GetIdentifier = LGF.Core.GetIdentifier, 160 | GetGender = LGF.Core.GetGender, 161 | GetGroup = LGF.Core.GetGroup, 162 | GetAccount = LGF.Core.GetPlayerAccount 163 | } 164 | -------------------------------------------------------------------------------- /game/framework/server.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: undefined-global, duplicate-set-field 2 | local obj, frameworkName = LGF:GetFramework() 3 | LGF.Core = {} 4 | 5 | local function ERR_CORE(res) 6 | return LGF:logError("Unsupported framework: %s", res) 7 | end 8 | 9 | function LGF.Core:GetPlayer(target) 10 | if frameworkName == "LEGACYCORE" then 11 | return obj.DATA:GetPlayerDataBySlot(target) 12 | elseif frameworkName == "es_extended" then 13 | return obj.GetPlayerFromId(target) 14 | elseif frameworkName == "qb-core" then 15 | return obj.Functions.GetPlayer(target).PlayerData 16 | else 17 | ERR_CORE(frameworkName) 18 | end 19 | end 20 | 21 | function LGF.Core:GetGroup(target) 22 | local PlayerData = LGF.Core:GetPlayer(target) 23 | local playerGroup = nil 24 | if not PlayerData then return end 25 | if frameworkName == "LEGACYCORE" then 26 | playerGroup = PlayerData.playerGroup 27 | elseif frameworkName == "es_extended" then 28 | playerGroup = PlayerData.getGroup() 29 | elseif frameworkName == "qb-core" then 30 | if IsPlayerAceAllowed(target, 'admin') then 31 | playerGroup = 'admin' 32 | elseif IsPlayerAceAllowed(target, 'god') then 33 | playerGroup = 'god' 34 | else 35 | return "User" 36 | end 37 | end 38 | return playerGroup 39 | end 40 | 41 | function LGF.Core:GetIdentifier(target) 42 | local PlayerData = LGF.Core:GetPlayer(target) 43 | if frameworkName == "LEGACYCORE" then 44 | return PlayerData.identifier 45 | elseif frameworkName == "es_extended" then 46 | return PlayerData.identifier 47 | elseif frameworkName == "qb-core" then 48 | return PlayerData.license 49 | end 50 | end 51 | 52 | function LGF.Core:GetName(target) 53 | local PlayerData = LGF.Core:GetPlayer(target) 54 | if frameworkName == "LEGACYCORE" then 55 | return PlayerData.playerName 56 | elseif frameworkName == "es_extended" then 57 | return string.format("%s %s", PlayerData.get("firstName"), PlayerData.get("lastName")) 58 | elseif frameworkName == "qb-core" then 59 | return string.format("%s %s", PlayerData.charinfo.firstname, PlayerData.charinfo.lastname) 60 | end 61 | end 62 | 63 | function LGF.Core:GetGender(target) 64 | local PlayerData = LGF.Core:GetPlayer(target) 65 | if frameworkName == "LEGACYCORE" then 66 | return PlayerData.sex 67 | elseif frameworkName == "es_extended" then 68 | return PlayerData.sex 69 | elseif frameworkName == "qb-core" then 70 | return PlayerData.charinfo.gender 71 | end 72 | end 73 | 74 | function LGF.Core:GetPlayerAccount(target) 75 | if frameworkName == "LEGACYCORE" then 76 | local promise = obj.DATA:GetPlayerAccount(target) 77 | return promise 78 | elseif frameworkName == "es_extended" then 79 | local player = self.GetPlayer(target) 80 | local values = {} 81 | for i =1 , #player.accounts do 82 | values[player.accounts[i].name] = player.accounts[i].money 83 | end 84 | return values 85 | elseif frameworkName == "qb-core" then 86 | 87 | end 88 | end 89 | 90 | LGF:RegisterServerCallback('LGF_Utility:Bridge:GetPlayerGroup', function(source) 91 | if not source or source <= 0 or type(source) ~= "number" then return ("Invalid source %S ?"):format(source) end 92 | if not GetPlayerName(source) then return "Player not found" end 93 | local Group = LGF.Core:GetGroup(source) 94 | if not Group then Group = "User" end 95 | return Group 96 | end) 97 | 98 | function LGF.Core:ManageAccount(target, amount, typetransition) 99 | if type(amount) == "string" then amount = tonumber(amount) end 100 | 101 | if frameworkName == "LEGACYCORE" then 102 | local PlayerSlot = LGF.Core:GetPlayer(target).charIdentifier 103 | local PlayerData = LGF.Core:GetPlayer(target) 104 | local accountsData = json.decode(PlayerData.accounts) 105 | 106 | if typetransition == "add" then 107 | accountsData.Bank = (accountsData.Bank or 0) + amount 108 | elseif typetransition == "remove" then 109 | accountsData.Bank = math.max(0, (accountsData.Bank or 0) - amount) 110 | end 111 | 112 | local updatedAccounts = json.encode(accountsData) 113 | local updatePromise, updateError = MySQL.update.await( 114 | 'UPDATE `users` SET `accounts` = ? WHERE `identifier` = ? AND `charIdentifier` = ?', 115 | { updatedAccounts, PlayerData.identifier, PlayerSlot }) 116 | elseif frameworkName == "es_extended" then 117 | local xPlayer = ESX.GetPlayerFromId(target) 118 | if xPlayer then 119 | if typetransition == "add" then 120 | xPlayer.addAccountMoney('bank', amount) 121 | elseif typetransition == "remove" then 122 | xPlayer.removeAccountMoney('bank', amount) 123 | end 124 | end 125 | elseif frameworkName == "qb-core" then 126 | local player = QBCore.Functions.GetPlayer(target) 127 | if player then 128 | if typetransition == "add" then 129 | player.Functions.AddMoney('bank', amount) 130 | elseif typetransition == "remove" then 131 | player.Functions.RemoveMoney('bank', amount) 132 | end 133 | end 134 | end 135 | end 136 | 137 | function LGF.Core:GetJob(target) 138 | local playerData = LGF.Core:GetPlayer(target) 139 | if frameworkName == "LEGACYCORE" then 140 | return playerData.JobName 141 | elseif frameworkName == "es_extended" then 142 | return playerData.job and playerData.job.name 143 | elseif frameworkName == "qb-core" then 144 | return playerData.job and playerData.job.label 145 | else 146 | ERR_CORE(frameworkName) 147 | end 148 | end 149 | 150 | function LGF.Core:generatePlate(maxLetters, pattern) 151 | local tableName = frameworkName == "es_extended" and "owned_vehicles" 152 | or frameworkName == "qb-core" and "player_vehicles" 153 | or frameworkName == "LEGACYCORE" and "owned_vehicles" 154 | 155 | if not tableName then 156 | print("Error: Unsupported framework or table not found.") 157 | return nil 158 | end 159 | 160 | local plate 161 | 162 | repeat 163 | plate = LGF.string:RandStr(maxLetters, pattern) 164 | 165 | local query = ('SELECT plate FROM %s WHERE plate = ? LIMIT 1'):format(tableName) 166 | local plateExists = MySQL.scalar.await(query, { plate }) 167 | until not plateExists 168 | 169 | return plate 170 | end 171 | 172 | function LGF.Core:giveVehicle(target, props, stored) 173 | local plate = props.plate 174 | local playerData = LGF.Core:GetPlayer(target) 175 | local identifier = LGF.Core:GetIdentifier(target) 176 | 177 | if frameworkName == "es_extended" then 178 | MySQL.insert('INSERT INTO `owned_vehicles` (owner, plate, vehicle, stored) VALUES (?, ?, ?, ?)', 179 | { identifier, plate, json.encode(props), stored }) 180 | elseif frameworkName == "qb-core" then 181 | MySQL.insert( 182 | 'INSERT INTO player_vehicles (license, citizenid, vehicle, hash, mods, plate, state, garage) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', 183 | { identifier, playerData.citizenid, props.model, GetHashKey(props.model), json.encode(props), plate, stored, 184 | "A" }) 185 | elseif frameworkName == "LEGACYCORE" then 186 | MySQL.insert('INSERT INTO owned_vehicles (owner, plate, vehicle, garage, stored) VALUES (?, ?, ?, ?,?)', 187 | { identifier, plate, json.encode(props), "A", stored }) 188 | else 189 | print("Error: Unsupported framework:", frameworkName) 190 | end 191 | end 192 | 193 | return { 194 | GiveVehicle = LGF.Core.giveVehicle, 195 | GeneratePlate = LGF.Core.generatePlate, 196 | GetName = LGF.Core.GetName, 197 | GetPlayer = LGF.Core.GetPlayer, 198 | GetGroup = LGF.Core.GetGroup, 199 | GetIdentifier = LGF.Core.GetIdentifier, 200 | GetPlayerAccount = LGF.Core.GetPlayerAccount, 201 | ManageAccount = LGF.Core.ManageAccount, 202 | } 203 | -------------------------------------------------------------------------------- /game/server/cb.lua: -------------------------------------------------------------------------------- 1 | ServerCallback = {} 2 | 3 | function LGF:RegisterServerCallback(name, cb) 4 | ServerCallback[name] = cb 5 | end 6 | 7 | RegisterNetEvent("LGF_UI:server:Callback") 8 | AddEventHandler("LGF_UI:server:Callback", function(name, requestId, invoker, ...) 9 | local source = source 10 | local invokingResource = invoker 11 | 12 | local result 13 | 14 | if ServerCallback[name] then 15 | result = ServerCallback[name](source, ...) 16 | else 17 | LGF:logError("Callback Not Found. Name: %s, RequestId: %s, Invoking Resource: %s", name, requestId,invokingResource) 18 | end 19 | 20 | TriggerClientEvent("LGF_UI:client:CallbackResponse", source, requestId, result) 21 | end) 22 | 23 | 24 | exports('RegisterServerCallback', function(name, cb) 25 | return LGF:RegisterServerCallback(name, cb) 26 | end) 27 | 28 | 29 | return ServerCallback 30 | -------------------------------------------------------------------------------- /game/server/event.lua: -------------------------------------------------------------------------------- 1 | function LGF:TriggerClientEvent(eventName, playerId, ...) 2 | 3 | if type(playerId) ~= "number" then 4 | return false, "Invalid player ID: must be a number" 5 | end 6 | 7 | if type(eventName) ~= "string" or eventName == "" then 8 | return false 9 | end 10 | 11 | local args = { ... } 12 | 13 | local success = TriggerClientEvent(eventName, playerId, table.unpack(args)) 14 | 15 | if not success then 16 | return false, ("Failed to trigger client event %s for player ID %d"):format(eventName, playerId) 17 | end 18 | 19 | return true 20 | end 21 | -------------------------------------------------------------------------------- /game/server/utility.lua: -------------------------------------------------------------------------------- 1 | function LGF:isPlayerOnline(source) 2 | local Players = GetPlayers() 3 | for I = 1, #Players do 4 | local target = Players[I] 5 | if tostring(target) == tostring(source) then 6 | return true 7 | end 8 | end 9 | return false 10 | end 11 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | LGF = {} 2 | LGF.Player = {} 3 | 4 | function LGF:GetContext() 5 | local SERVER_SIDE = IsDuplicityVersion() 6 | local CLIENT_SIDE = not SERVER_SIDE 7 | 8 | if SERVER_SIDE then 9 | return "server" 10 | elseif CLIENT_SIDE then 11 | return "client" 12 | else 13 | error("Unable to determine path: Not running on either server or client side") 14 | end 15 | end 16 | 17 | function LGF:LuaLoader(module_name, resource) 18 | local resource_name = resource or GetInvokingResource() 19 | local file_name = module_name .. ".lua" 20 | local file_content = LoadResourceFile(resource_name, file_name) 21 | 22 | if not file_content then 23 | error(string.format("Error loading file '%s' from resource '%s': File does not exist or cannot be read.", 24 | file_name, resource_name)) 25 | end 26 | 27 | local func, compile_err = load(file_content, "@" .. file_name) 28 | if not func then error(string.format("Error compiling module '%s': %s", module_name, compile_err), 2) end 29 | 30 | local success, result = pcall(func) 31 | if not success then error(string.format("Error executing module '%s': %s", module_name, result), 2) end 32 | 33 | return result 34 | end 35 | 36 | -- require = function(module_name) 37 | -- return LGF:LuaLoader(module_name) 38 | -- end 39 | 40 | 41 | local Framework = { 42 | { ResourceName = "LEGACYCORE", Object = "GetCoreData" }, 43 | { ResourceName = "es_extended", Object = "getSharedObject" }, 44 | { ResourceName = "qb-core", Object = "GetCoreObject" }, 45 | } 46 | 47 | 48 | function LGF:GetFramework() 49 | for I = 1, #Framework do 50 | local DATA = Framework[I] 51 | if GetResourceState(DATA.ResourceName):find("started") then 52 | local success, frame = pcall(function() 53 | return exports[DATA.ResourceName][DATA.Object]() 54 | end) 55 | if success then 56 | return frame, DATA.ResourceName 57 | else 58 | LGF:logError("Failed to Get Object from %s, Result %s", DATA.ResourceName, frame) 59 | end 60 | end 61 | end 62 | end 63 | 64 | if LGF:GetContext() == "client" then 65 | function LGF.Player:Ped() 66 | return PlayerPedId() 67 | end 68 | 69 | function LGF.Player:Index() 70 | return GetPlayerServerId(NetworkGetPlayerIndexFromPed(self:Ped())) 71 | end 72 | 73 | function LGF.Player:PlayerId() 74 | return PlayerId() 75 | end 76 | 77 | function LGF.Player:Coords() 78 | return GetEntityCoords(self:Ped()) 79 | end 80 | end 81 | 82 | 83 | exports('UtilityData', function() 84 | return _G.LGF 85 | end) 86 | -------------------------------------------------------------------------------- /readme.MD: -------------------------------------------------------------------------------- 1 | # LGF_Utility API 2 | 3 | ![GitHub Downloads](https://img.shields.io/github/downloads/ENT510/LGF_Utility/total?logo=github) 4 | ![GitHub Release](https://img.shields.io/github/v/release/ENT510/LGF_Utility?logo=github) 5 | 6 | **LGF_Utility** is a versatile library designed to facilitate the development of modules, functions, exports, and UI components within your project. It provides a unified framework for managing various aspects of your application, making it easier to build complex systems and interfaces. 7 | 8 | - [Documentation](https://lgf-docs.vercel.app) 9 | 10 | ## Features 11 | 12 | - **Modular Functionality**: Manage and utilize reusable modules. 13 | - **Exports System**: Share functions and data across different parts of your project. 14 | - **UI Components**: Create and manage user interfaces seamlessly. 15 | - **Bridge Framework**: Built-in bridge for various frameworks. 16 | 17 | ## Integration 18 | 19 | ### Importing via Export 20 | 21 | To access **LGF_Utility** functions and data, you can import it using the export system: 22 | 23 | ```lua 24 | local LGF = exports['LGF_Utility']:UtilityData() 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /server/main.lua: -------------------------------------------------------------------------------- 1 | local CurrentResourceName = "LGF_Utility" 2 | local CurrentResourceVersion = GetResourceMetadata(CurrentResourceName, "version", 0) or "unknown" 3 | 4 | function CheckVersion(repoName) 5 | local url = ('https://api.github.com/repos/%s/releases/latest'):format(repoName) 6 | 7 | PerformHttpRequest(url, function(errorCode, resultData, resultHeaders) 8 | if errorCode == 200 then 9 | local result = json.decode(resultData) 10 | 11 | if result and result.tag_name then 12 | local latestVersion = result.tag_name 13 | 14 | if CurrentResourceVersion ~= latestVersion then 15 | print(("^0[^3UPDATE^0] %s is outdated! Current version: ^1%s^0, Latest version: ^6%s^0"):format( 16 | CurrentResourceName, CurrentResourceVersion, latestVersion)) 17 | print(("^0[^3INFO^0] Please update to the latest version from the repository: ^6%s^0"):format(result 18 | .html_url)) 19 | else 20 | print(("^0[^6INFO^0] %s is up to date! (^6%s^0)"):format(CurrentResourceName, CurrentResourceVersion)) 21 | end 22 | else 23 | print("^0[^1ERROR^0] Unable to fetch version information.") 24 | end 25 | else 26 | print(("^0[^1ERROR^0] Failed to fetch the latest version information. HTTP error code: %d"):format(errorCode)) 27 | end 28 | end, "GET") 29 | end 30 | 31 | AddEventHandler('onServerResourceStart', function(resourceName) 32 | if resourceName == CurrentResourceName then 33 | local repoName = string.format('ENT510/%s', CurrentResourceName) 34 | CheckVersion(repoName) 35 | end 36 | end) 37 | 38 | -------------------------------------------------------------------------------- /shared/config.lua: -------------------------------------------------------------------------------- 1 | Config = {} 2 | 3 | 4 | Config.isPlayerDead = function() 5 | if GetResourceState('ars_ambulancejob'):find('start') then 6 | return LocalPlayer.state.dead 7 | else 8 | return IsEntityDead(cache.ped) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /shared/math.lua: -------------------------------------------------------------------------------- 1 | LGF.math = {} 2 | 3 | function LGF.math:findMin(...) 4 | local args = { ... } 5 | 6 | if #args == 0 then 7 | return nil, "No arguments provided" 8 | end 9 | 10 | local minValue = args[1] 11 | 12 | for i = 2, #args do 13 | if args[i] < minValue then 14 | minValue = args[i] 15 | end 16 | end 17 | 18 | return minValue 19 | end 20 | 21 | function LGF.math:findMax(...) 22 | local args = { ... } 23 | 24 | if #args == 0 then 25 | return nil, "No arguments provided" 26 | end 27 | 28 | local maxValue = args[1] 29 | 30 | for i = 2, #args do 31 | if args[i] > maxValue then 32 | maxValue = args[i] 33 | end 34 | end 35 | 36 | return maxValue 37 | end 38 | 39 | 40 | function LGF.math:round(value, decimalPlaces) 41 | assert(type(value) == "number", "Value must be a number") 42 | decimalPlaces = decimalPlaces or 0 43 | local multiplier = 10 ^ decimalPlaces 44 | return math.floor(value * multiplier + 0.5) / multiplier 45 | end 46 | 47 | -- print(LGF.math:round(23.4234324324, 4)) -------------------------------------------------------------------------------- /shared/shared.lua: -------------------------------------------------------------------------------- 1 | local GetConvarDebug = GetConvar('LGF_Utility:EnableDebug', "true") 2 | 3 | function LGF:DebugValue(format, ...) 4 | if GetConvarDebug == "false" then return end 5 | print(("[^3DEBUG^7] " .. format):format(...)) 6 | end 7 | 8 | function LGF:logError(message, ...) 9 | print(("^1[ERROR]^7 " .. message):format(...)) 10 | end 11 | 12 | local function parseVersion(version) 13 | local parts = {} 14 | for part in version:gmatch("%d+") do 15 | table.insert(parts, tonumber(part)) 16 | end 17 | return parts 18 | end 19 | 20 | local function isMinimumVersion(current_version, required_version) 21 | local current = parseVersion(current_version) 22 | local required = parseVersion(required_version) 23 | 24 | for i = 1, math.max(#current, #required) do 25 | local current_part = current[i] or 0 26 | local required_part = required[i] or 0 27 | 28 | if current_part > required_part then 29 | return true 30 | elseif current_part < required_part then 31 | return false 32 | end 33 | end 34 | 35 | return true 36 | end 37 | 38 | --[[GET DEPENDENCY]] 39 | function LGF:GetDependency(resource_name, required_version) 40 | local state = GetResourceState(resource_name) 41 | 42 | if state ~= "started" then 43 | return false, ("The resource ^1[%s]^7 is not started."):format(resource_name) 44 | end 45 | 46 | local current_version = GetResourceMetadata(resource_name, 'version', 0) 47 | 48 | if not isMinimumVersion(current_version, required_version) then 49 | return false,("^1[ERROR]^7 The version of resource ^1[%s]^7 does not meet the minimum required version. Required: ^3%s^7 or higher, Found: ^3%s^7") :format(resource_name, required_version, current_version) 50 | end 51 | 52 | return true 53 | end 54 | 55 | function LGF:SafeAsyncWait(delay, func) 56 | assert(type(delay) == "number" and delay > 0, "Delay must be a positive number.") 57 | 58 | local co = coroutine.running() 59 | 60 | Citizen.CreateThread(function() 61 | Citizen.Wait(delay) 62 | local success, result = pcall(func) 63 | 64 | if not success then 65 | LGF:logError("Async function failed %s:", result) 66 | end 67 | 68 | coroutine.resume(co, success, result) 69 | end) 70 | 71 | return coroutine.yield() 72 | end 73 | -------------------------------------------------------------------------------- /shared/string.lua: -------------------------------------------------------------------------------- 1 | LGF.string = {} 2 | 3 | _G.GENERATEDSTRING = {} 4 | 5 | function LGF.string:RandStr(len, patt, limit) 6 | limit = limit or 100 7 | assert(type(patt) == "string", "Pattern must be a string.") 8 | assert(len > 0 and len <= limit, ("Length must be a positive number and less than or equal to %d."):format(limit)) 9 | 10 | local charsets = { 11 | aln = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 12 | num = "0123456789", 13 | alp = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 14 | hex = "abcdef0123456789", 15 | upp = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 16 | low = "abcdefghijklmnopqrstuvwxyz", 17 | } 18 | 19 | local chars = charsets[patt] or charsets.aln 20 | 21 | local GenerateUniqueString = function() 22 | local str = {} 23 | 24 | for i = 1, len do 25 | local idx = math.random(#chars) 26 | str[i] = chars:sub(idx, idx) 27 | end 28 | 29 | return table.concat(str) 30 | end 31 | 32 | local success, result 33 | 34 | LGF:SafeAsyncWait(100, function() 35 | repeat 36 | result = GenerateUniqueString() 37 | until not GENERATEDSTRING[result] 38 | _G.GENERATEDSTRING[result] = true 39 | success = true 40 | end) 41 | 42 | if success then 43 | return result 44 | else 45 | return nil 46 | end 47 | end 48 | 49 | function LGF.string:ToLower(str) 50 | assert(type(str) == "string", "Input must be a string") 51 | return str:lower() 52 | end 53 | 54 | function LGF.string:TrimSpace(str) 55 | assert(type(str) == "string", "Input must be a string") 56 | return str:gsub("%s+", "") 57 | end 58 | 59 | function LGF.string:GetGeneratedString() 60 | return _G.GENERATEDSTRING 61 | end 62 | 63 | -------------------------------------------------------------------------------- /web/build/assets/index-BTKS3fo5.css: -------------------------------------------------------------------------------- 1 | @import"https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;700&display=swap";body{-webkit-user-select:none;user-select:none;color:#ccc;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;overflow:hidden}.menu{position:fixed;display:flex;flex-direction:column;padding:10px;bottom:40px;right:-500px;transform:translateY(0);width:375px;min-height:500px;max-height:500px;border-radius:10px;background-color:#141414;transition:right .7s ease-in-out,opacity .7s ease-in-out,height .7s ease-in-out;opacity:0;pointer-events:none;scrollbar-width:none;-ms-overflow-style:none}.menu::-webkit-scrollbar{display:none}.menu .context-menu-content{display:flex;flex-direction:column;height:100%;overflow-y:auto}.menu .context-menu-card{margin-bottom:10px}._container_11yfh_1{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;z-index:9999}._notification_11yfh_11{max-width:300px;min-width:300px;padding:12px;background-color:#141414cc;border-radius:9px;display:flex;align-items:center;pointer-events:auto;transition:opacity .3s ease,transform .3s ease;margin-bottom:10px;z-index:10000}._slide-in_11yfh_25{animation:_slideIn_11yfh_1 .5s ease-out forwards}._slide-out_11yfh_29{animation:_slideOut_11yfh_1 .5s ease-in forwards}@keyframes _slideIn_11yfh_1{0%{transform:translateY(-20px);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes _slideOut_11yfh_1{0%{transform:translateY(0);opacity:1}to{transform:translateY(-20px);opacity:0}}._position-top-left_11yfh_53{top:10px;left:10px}._position-top-right_11yfh_58{top:10px;right:10px}._position-bottom-left_11yfh_63{bottom:10px;left:10px}._position-bottom-right_11yfh_68{bottom:10px;right:10px}@keyframes slideOut{0%{transform:translateY(0);opacity:1}to{transform:translateY(-20px);opacity:0}}.text-ui{position:fixed;padding:8px 12px;background-color:#141414cc;color:#c6c6c6;border-radius:5px;box-shadow:0 4px 8px #0003;z-index:1000;opacity:0;transition:opacity .5s ease-in-out;max-width:300px;display:flex;flex-direction:column;align-items:flex-start}.text-ui.entering{animation:slideIn .5s forwards}.text-ui.exiting{animation:slideOut .5s forwards}.text-ui.center{left:50%;top:50%;transform:translate(-50%,-50%)}.text-ui.center-left{left:10px;top:50%;transform:translateY(-50%)}.text-ui.center-right{right:10px;top:50%;transform:translateY(-50%)}.text-ui .textui-message{display:flex;flex-direction:row;align-items:center;gap:7px;width:100%}.text-ui .textui-bind{display:flex;justify-content:center;align-items:center;width:34px;height:37px;border-radius:3px;color:#f2f2f2;background-color:#369c8161;border:.1vh solid #52938b;text-shadow:0 0 5px rgba(80,188,175,.2509803922)}.text-ui .textui-message-container{display:flex;align-items:center;flex:1;min-width:0}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{transform:translateY(-20px);opacity:0}to{transform:translateY(0);opacity:1}}.centered-container{display:flex;align-items:center;position:fixed;left:30px;top:50%;transform:translateY(-50%);width:100%;height:100%;z-index:1000;animation:fadeIn .3s ease-in-out}.card{max-width:500px;width:500px;min-height:500px;height:500px;max-height:500px;position:relative;background:#fff;border-radius:8px;animation:slideIn .5s ease-out}.card-header{font-size:1.25rem;font-weight:700;margin-bottom:1rem}.card-content{font-size:1rem;margin-bottom:1rem}.card-footer{display:flex;justify-content:space-between;margin-top:auto}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;height:100%}#root{height:100%}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace} 2 | -------------------------------------------------------------------------------- /web/build/assets/index-BiCOjNXi.css: -------------------------------------------------------------------------------- 1 | @import"https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;700&display=swap";.leaflet-pane,.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile-container,.leaflet-pane>svg,.leaflet-pane>canvas,.leaflet-zoom-box,.leaflet-image-layer,.leaflet-layer{position:absolute;left:0;top:0}.leaflet-container{overflow:hidden}.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow{-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-user-drag:none}.leaflet-tile::selection{background:transparent}.leaflet-safari .leaflet-tile{image-rendering:-webkit-optimize-contrast}.leaflet-safari .leaflet-tile-container{width:1600px;height:1600px;-webkit-transform-origin:0 0}.leaflet-marker-icon,.leaflet-marker-shadow{display:block}.leaflet-container .leaflet-overlay-pane svg{max-width:none!important;max-height:none!important}.leaflet-container .leaflet-marker-pane img,.leaflet-container .leaflet-shadow-pane img,.leaflet-container .leaflet-tile-pane img,.leaflet-container img.leaflet-image-layer,.leaflet-container .leaflet-tile{max-width:none!important;max-height:none!important;width:auto;padding:0}.leaflet-container img.leaflet-tile{mix-blend-mode:plus-lighter}.leaflet-container.leaflet-touch-zoom{-ms-touch-action:pan-x pan-y;touch-action:pan-x pan-y}.leaflet-container.leaflet-touch-drag{-ms-touch-action:pinch-zoom;touch-action:none;touch-action:pinch-zoom}.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom{-ms-touch-action:none;touch-action:none}.leaflet-container{-webkit-tap-highlight-color:transparent}.leaflet-container a{-webkit-tap-highlight-color:rgba(51,181,229,.4)}.leaflet-tile{filter:inherit;visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-box{width:0;height:0;-moz-box-sizing:border-box;box-sizing:border-box;z-index:800}.leaflet-overlay-pane svg{-moz-user-select:none}.leaflet-pane{z-index:400}.leaflet-tile-pane{z-index:200}.leaflet-overlay-pane{z-index:400}.leaflet-shadow-pane{z-index:500}.leaflet-marker-pane{z-index:600}.leaflet-tooltip-pane{z-index:650}.leaflet-popup-pane{z-index:700}.leaflet-map-pane canvas{z-index:100}.leaflet-map-pane svg{z-index:200}.leaflet-vml-shape{width:1px;height:1px}.lvml{behavior:url(#default#VML);display:inline-block;position:absolute}.leaflet-control{position:relative;z-index:800;pointer-events:visiblePainted;pointer-events:auto}.leaflet-top,.leaflet-bottom{position:absolute;z-index:1000;pointer-events:none}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-control{float:left;clear:both}.leaflet-right .leaflet-control{float:right}.leaflet-top .leaflet-control{margin-top:10px}.leaflet-bottom .leaflet-control{margin-bottom:10px}.leaflet-left .leaflet-control{margin-left:10px}.leaflet-right .leaflet-control{margin-right:10px}.leaflet-fade-anim .leaflet-popup{opacity:0;-webkit-transition:opacity .2s linear;-moz-transition:opacity .2s linear;transition:opacity .2s linear}.leaflet-fade-anim .leaflet-map-pane .leaflet-popup{opacity:1}.leaflet-zoom-animated{-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}svg.leaflet-zoom-animated{will-change:transform}.leaflet-zoom-anim .leaflet-zoom-animated{-webkit-transition:-webkit-transform .25s cubic-bezier(0,0,.25,1);-moz-transition:-moz-transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1)}.leaflet-zoom-anim .leaflet-tile,.leaflet-pan-anim .leaflet-tile{-webkit-transition:none;-moz-transition:none;transition:none}.leaflet-zoom-anim .leaflet-zoom-hide{visibility:hidden}.leaflet-interactive{cursor:pointer}.leaflet-grab{cursor:-webkit-grab;cursor:-moz-grab;cursor:grab}.leaflet-crosshair,.leaflet-crosshair .leaflet-interactive{cursor:crosshair}.leaflet-popup-pane,.leaflet-control{cursor:auto}.leaflet-dragging .leaflet-grab,.leaflet-dragging .leaflet-grab .leaflet-interactive,.leaflet-dragging .leaflet-marker-draggable{cursor:move;cursor:-webkit-grabbing;cursor:-moz-grabbing;cursor:grabbing}.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-image-layer,.leaflet-pane>svg path,.leaflet-tile-container{pointer-events:none}.leaflet-marker-icon.leaflet-interactive,.leaflet-image-layer.leaflet-interactive,.leaflet-pane>svg path.leaflet-interactive,svg.leaflet-image-layer.leaflet-interactive path{pointer-events:visiblePainted;pointer-events:auto}.leaflet-container{background:#ddd;outline-offset:1px}.leaflet-container a{color:#0078a8}.leaflet-zoom-box{border:2px dotted #38f;background:#ffffff80}.leaflet-container{font-family:Helvetica Neue,Arial,Helvetica,sans-serif;font-size:12px;font-size:.75rem;line-height:1.5}.leaflet-bar{box-shadow:0 1px 5px #000000a6;border-radius:4px}.leaflet-bar a{background-color:#fff;border-bottom:1px solid #ccc;width:26px;height:26px;line-height:26px;display:block;text-align:center;text-decoration:none;color:#000}.leaflet-bar a,.leaflet-control-layers-toggle{background-position:50% 50%;background-repeat:no-repeat;display:block}.leaflet-bar a:hover,.leaflet-bar a:focus{background-color:#f4f4f4}.leaflet-bar a:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-bar a:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom:none}.leaflet-bar a.leaflet-disabled{cursor:default;background-color:#f4f4f4;color:#bbb}.leaflet-touch .leaflet-bar a{width:30px;height:30px;line-height:30px}.leaflet-touch .leaflet-bar a:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.leaflet-touch .leaflet-bar a:last-child{border-bottom-left-radius:2px;border-bottom-right-radius:2px}.leaflet-control-zoom-in,.leaflet-control-zoom-out{font:700 18px Lucida Console,Monaco,monospace;text-indent:1px}.leaflet-touch .leaflet-control-zoom-in,.leaflet-touch .leaflet-control-zoom-out{font-size:22px}.leaflet-control-layers{box-shadow:0 1px 5px #0006;background:#fff;border-radius:5px}.leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAQAAAADQ4RFAAACf0lEQVR4AY1UM3gkARTePdvdoTxXKc+qTl3aU5U6b2Kbkz3Gtq3Zw6ziLGNPzrYx7946Tr6/ee/XeCQ4D3ykPtL5tHno4n0d/h3+xfuWHGLX81cn7r0iTNzjr7LrlxCqPtkbTQEHeqOrTy4Yyt3VCi/IOB0v7rVC7q45Q3Gr5K6jt+3Gl5nCoDD4MtO+j96Wu8atmhGqcNGHObuf8OM/x3AMx38+4Z2sPqzCxRFK2aF2e5Jol56XTLyggAMTL56XOMoS1W4pOyjUcGGQdZxU6qRh7B9Zp+PfpOFlqt0zyDZckPi1ttmIp03jX8gyJ8a/PG2yutpS/Vol7peZIbZcKBAEEheEIAgFbDkz5H6Zrkm2hVWGiXKiF4Ycw0RWKdtC16Q7qe3X4iOMxruonzegJzWaXFrU9utOSsLUmrc0YjeWYjCW4PDMADElpJSSQ0vQvA1Tm6/JlKnqFs1EGyZiFCqnRZTEJJJiKRYzVYzJck2Rm6P4iH+cmSY0YzimYa8l0EtTODFWhcMIMVqdsI2uiTvKmTisIDHJ3od5GILVhBCarCfVRmo4uTjkhrhzkiBV7SsaqS+TzrzM1qpGGUFt28pIySQHR6h7F6KSwGWm97ay+Z+ZqMcEjEWebE7wxCSQwpkhJqoZA5ivCdZDjJepuJ9IQjGGUmuXJdBFUygxVqVsxFsLMbDe8ZbDYVCGKxs+W080max1hFCarCfV+C1KATwcnvE9gRRuMP2prdbWGowm1KB1y+zwMMENkM755cJ2yPDtqhTI6ED1M/82yIDtC/4j4BijjeObflpO9I9MwXTCsSX8jWAFeHr05WoLTJ5G8IQVS/7vwR6ohirYM7f6HzYpogfS3R2OAAAAAElFTkSuQmCC);width:36px;height:36px}.leaflet-retina .leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAQAAABvcdNgAAAEsklEQVR4AWL4TydIhpZK1kpWOlg0w3ZXP6D2soBtG42jeI6ZmQTHzAxiTbSJsYLjO9HhP+WOmcuhciVnmHVQcJnp7DFvScowZorad/+V/fVzMdMT2g9Cv9guXGv/7pYOrXh2U+RRR3dSd9JRx6bIFc/ekqHI29JC6pJ5ZEh1yWkhkbcFeSjxgx3L2m1cb1C7bceyxA+CNjT/Ifff+/kDk2u/w/33/IeCMOSaWZ4glosqT3DNnNZQ7Cs58/3Ce5HL78iZH/vKVIaYlqzfdLu8Vi7dnvUbEza5Idt36tquZFldl6N5Z/POLof0XLK61mZCmJSWjVF9tEjUluu74IUXvgttuVIHE7YxSkaYhJZam7yiM9Pv82JYfl9nptxZaxMJE4YSPty+vF0+Y2up9d3wwijfjZbabqm/3bZ9ecKHsiGmRflnn1MW4pjHf9oLufyn2z3y1D6n8g8TZhxyzipLNPnAUpsOiuWimg52psrTZYnOWYNDTMuWBWa0tJb4rgq1UvmutpaYEbZlwU3CLJm/ayYjHW5/h7xWLn9Hh1vepDkyf7dE7MtT5LR4e7yYpHrkhOUpEfssBLq2pPhAqoSWKUkk7EDqkmK6RrCEzqDjhNDWNE+XSMvkJRDWlZTmCW0l0PHQGRZY5t1L83kT0Y3l2SItk5JAWHl2dCOBm+fPu3fo5/3v61RMCO9Jx2EEYYhb0rmNQMX/vm7gqOEJLcXTGw3CAuRNeyaPWwjR8PRqKQ1PDA/dpv+on9Shox52WFnx0KY8onHayrJzm87i5h9xGw/tfkev0jGsQizqezUKjk12hBMKJ4kbCqGPVNXudyyrShovGw5CgxsRICxF6aRmSjlBnHRzg7Gx8fKqEubI2rahQYdR1YgDIRQO7JvQyD52hoIQx0mxa0ODtW2Iozn1le2iIRdzwWewedyZzewidueOGqlsn1MvcnQpuVwLGG3/IR1hIKxCjelIDZ8ldqWz25jWAsnldEnK0Zxro19TGVb2ffIZEsIO89EIEDvKMPrzmBOQcKQ+rroye6NgRRxqR4U8EAkz0CL6uSGOm6KQCdWjvjRiSP1BPalCRS5iQYiEIvxuBMJEWgzSoHADcVMuN7IuqqTeyUPq22qFimFtxDyBBJEwNyt6TM88blFHao/6tWWhuuOM4SAK4EI4QmFHA+SEyWlp4EQoJ13cYGzMu7yszEIBOm2rVmHUNqwAIQabISNMRstmdhNWcFLsSm+0tjJH1MdRxO5Nx0WDMhCtgD6OKgZeljJqJKc9po8juskR9XN0Y1lZ3mWjLR9JCO1jRDMd0fpYC2VnvjBSEFg7wBENc0R9HFlb0xvF1+TBEpF68d+DHR6IOWVv2BECtxo46hOFUBd/APU57WIoEwJhIi2CdpyZX0m93BZicktMj1AS9dClteUFAUNUIEygRZCtik5zSxI9MubTBH1GOiHsiLJ3OCoSZkILa9PxiN0EbvhsAo8tdAf9Seepd36lGWHmtNANTv5Jd0z4QYyeo/UEJqxKRpg5LZx6btLPsOaEmdMyxYdlc8LMaJnikDlhclqmPiQnTEpLUIZEwkRagjYkEibQErwhkTAKCLQEbUgkzJQWc/0PstHHcfEdQ+UAAAAASUVORK5CYII=);background-size:26px 26px}.leaflet-touch .leaflet-control-layers-toggle{width:44px;height:44px}.leaflet-control-layers .leaflet-control-layers-list,.leaflet-control-layers-expanded .leaflet-control-layers-toggle{display:none}.leaflet-control-layers-expanded .leaflet-control-layers-list{display:block;position:relative}.leaflet-control-layers-expanded{padding:6px 10px 6px 6px;color:#333;background:#fff}.leaflet-control-layers-scrollbar{overflow-y:scroll;overflow-x:hidden;padding-right:5px}.leaflet-control-layers-selector{margin-top:2px;position:relative;top:1px}.leaflet-control-layers label{display:block;font-size:13px;font-size:1.08333em}.leaflet-control-layers-separator{height:0;border-top:1px solid #ddd;margin:5px -10px 5px -6px}.leaflet-default-icon-path{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YaZtr2962HUzbDNpjszW24mRt28p47v7zq/bXZtrp/lWnXr337j3nPCe85NcypgSFdugCpW5YoDAMRaIMqRi6aKq5E3YqDQO3qAwjVWrD8Ncq/RBpykd8oZUb/kaJutow8r1aP9II0WmLKLIsJyv1w/kqw9Ch2MYdB++12Onxee/QMwvf4/Dk/Lfp/i4nxTXtOoQ4pW5Aj7wpici1A9erdAN2OH64x8OSP9j3Ft3b7aWkTg/Fm91siTra0f9on5sQr9INejH6CUUUpavjFNq1B+Oadhxmnfa8RfEmN8VNAsQhPqF55xHkMzz3jSmChWU6f7/XZKNH+9+hBLOHYozuKQPxyMPUKkrX/K0uWnfFaJGS1QPRtZsOPtr3NsW0uyh6NNCOkU3Yz+bXbT3I8G3xE5EXLXtCXbbqwCO9zPQYPRTZ5vIDXD7U+w7rFDEoUUf7ibHIR4y6bLVPXrz8JVZEql13trxwue/uDivd3fkWRbS6/IA2bID4uk0UpF1N8qLlbBlXs4Ee7HLTfV1j54APvODnSfOWBqtKVvjgLKzF5YdEk5ewRkGlK0i33Eofffc7HT56jD7/6U+qH3Cx7SBLNntH5YIPvODnyfIXZYRVDPqgHtLs5ABHD3YzLuespb7t79FY34DjMwrVrcTuwlT55YMPvOBnRrJ4VXTdNnYug5ucHLBjEpt30701A3Ts+HEa73u6dT3FNWwflY86eMHPk+Yu+i6pzUpRrW7SNDg5JHR4KapmM5Wv2E8Tfcb1HoqqHMHU+uWDD7zg54mz5/2BSnizi9T1Dg4QQXLToGNCkb6tb1NU+QAlGr1++eADrzhn/u8Q2YZhQVlZ5+CAOtqfbhmaUCS1ezNFVm2imDbPmPng5wmz+gwh+oHDce0eUtQ6OGDIyR0uUhUsoO3vfDmmgOezH0mZN59x7MBi++WDL1g/eEiU3avlidO671bkLfwbw5XV2P8Pzo0ydy4t2/0eu33xYSOMOD8hTf4CrBtGMSoXfPLchX+J0ruSePw3LZeK0juPJbYzrhkH0io7B3k164hiGvawhOKMLkrQLyVpZg8rHFW7E2uHOL888IBPlNZ1FPzstSJM694fWr6RwpvcJK60+0HCILTBzZLFNdtAzJaohze60T8qBzyh5ZuOg5e7uwQppofEmf2++DYvmySqGBuKaicF1blQjhuHdvCIMvp8whTTfZzI7RldpwtSzL+F1+wkdZ2TBOW2gIF88PBTzD/gpeREAMEbxnJcaJHNHrpzji0gQCS6hdkEeYt9DF/2qPcEC8RM28Hwmr3sdNyht00byAut2k3gufWNtgtOEOFGUwcXWNDbdNbpgBGxEvKkOQsxivJx33iow0Vw5S6SVTrpVq11ysA2Rp7gTfPfktc6zhtXBBC+adRLshf6sG2RfHPZ5EAc4sVZ83yCN00Fk/4kggu40ZTvIEm5g24qtU4KjBrx/BTTH8ifVASAG7gKrnWxJDcU7x8X6Ecczhm3o6YicvsLXWfh3Ch1W0k8x0nXF+0fFxgt4phz8QvypiwCCFKMqXCnqXExjq10beH+UUA7+nG6mdG/Pu0f3LgFcGrl2s0kNNjpmoJ9o4B29CMO8dMT4Q5ox8uitF6fqsrJOr8qnwNbRzv6hSnG5wP+64C7h9lp30hKNtKdWjtdkbuPA19nJ7Tz3zR/ibgARbhb4AlhavcBebmTHcFl2fvYEnW0ox9xMxKBS8btJ+KiEbq9zA4RthQXDhPa0T9TEe69gWupwc6uBUphquXgf+/FrIjweHQS4/pduMe5ERUMHUd9xv8ZR98CxkS4F2n3EUrUZ10EYNw7BWm9x1GiPssi3GgiGRDKWRYZfXlON+dfNbM+GgIwYdwAAAAASUVORK5CYII=)}.leaflet-container .leaflet-control-attribution{background:#fff;background:#fffc;margin:0}.leaflet-control-attribution,.leaflet-control-scale-line{padding:0 5px;color:#333;line-height:1.4}.leaflet-control-attribution a{text-decoration:none}.leaflet-control-attribution a:hover,.leaflet-control-attribution a:focus{text-decoration:underline}.leaflet-attribution-flag{display:inline!important;vertical-align:baseline!important;width:1em;height:.6669em}.leaflet-left .leaflet-control-scale{margin-left:5px}.leaflet-bottom .leaflet-control-scale{margin-bottom:5px}.leaflet-control-scale-line{border:2px solid #777;border-top:none;line-height:1.1;padding:2px 5px 1px;white-space:nowrap;-moz-box-sizing:border-box;box-sizing:border-box;background:#fffc;text-shadow:1px 1px #fff}.leaflet-control-scale-line:not(:first-child){border-top:2px solid #777;border-bottom:none;margin-top:-2px}.leaflet-control-scale-line:not(:first-child):not(:last-child){border-bottom:2px solid #777}.leaflet-touch .leaflet-control-attribution,.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{box-shadow:none}.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{border:2px solid rgba(0,0,0,.2);background-clip:padding-box}.leaflet-popup{position:absolute;text-align:center;margin-bottom:20px}.leaflet-popup-content-wrapper{padding:1px;text-align:left;border-radius:12px}.leaflet-popup-content{margin:13px 24px 13px 20px;line-height:1.3;font-size:13px;font-size:1.08333em;min-height:1px}.leaflet-popup-content p{margin:1.3em 0}.leaflet-popup-tip-container{width:40px;height:20px;position:absolute;left:50%;margin-top:-1px;margin-left:-20px;overflow:hidden;pointer-events:none}.leaflet-popup-tip{width:17px;height:17px;padding:1px;margin:-10px auto 0;pointer-events:auto;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.leaflet-popup-content-wrapper,.leaflet-popup-tip{background:#fff;color:#333;box-shadow:0 3px 14px #0006}.leaflet-container a.leaflet-popup-close-button{position:absolute;top:0;right:0;border:none;text-align:center;width:24px;height:24px;font:16px/24px Tahoma,Verdana,sans-serif;color:#757575;text-decoration:none;background:transparent}.leaflet-container a.leaflet-popup-close-button:hover,.leaflet-container a.leaflet-popup-close-button:focus{color:#585858}.leaflet-popup-scrolled{overflow:auto}.leaflet-oldie .leaflet-popup-content-wrapper{-ms-zoom:1}.leaflet-oldie .leaflet-popup-tip{width:24px;margin:0 auto;-ms-filter:"progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";filter:progid:DXImageTransform.Microsoft.Matrix(M11=.70710678,M12=.70710678,M21=-.70710678,M22=.70710678)}.leaflet-oldie .leaflet-control-zoom,.leaflet-oldie .leaflet-control-layers,.leaflet-oldie .leaflet-popup-content-wrapper,.leaflet-oldie .leaflet-popup-tip{border:1px solid #999}.leaflet-div-icon{background:#fff;border:1px solid #666}.leaflet-tooltip{position:absolute;padding:6px;background-color:#fff;border:1px solid #fff;border-radius:3px;color:#222;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;box-shadow:0 1px 3px #0006}.leaflet-tooltip.leaflet-interactive{cursor:pointer;pointer-events:auto}.leaflet-tooltip-top:before,.leaflet-tooltip-bottom:before,.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{position:absolute;pointer-events:none;border:6px solid transparent;background:transparent;content:""}.leaflet-tooltip-bottom{margin-top:6px}.leaflet-tooltip-top{margin-top:-6px}.leaflet-tooltip-bottom:before,.leaflet-tooltip-top:before{left:50%;margin-left:-6px}.leaflet-tooltip-top:before{bottom:0;margin-bottom:-12px;border-top-color:#fff}.leaflet-tooltip-bottom:before{top:0;margin-top:-12px;margin-left:-6px;border-bottom-color:#fff}.leaflet-tooltip-left{margin-left:-6px}.leaflet-tooltip-right{margin-left:6px}.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{top:50%;margin-top:-6px}.leaflet-tooltip-left:before{right:0;margin-right:-12px;border-left-color:#fff}.leaflet-tooltip-right:before{left:0;margin-left:-12px;border-right-color:#fff}@media print{.leaflet-control{-webkit-print-color-adjust:exact;print-color-adjust:exact}}._container_11yfh_1{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;z-index:9999}._notification_11yfh_11{max-width:300px;min-width:300px;padding:12px;background-color:#141414cc;border-radius:9px;display:flex;align-items:center;pointer-events:auto;transition:opacity .3s ease,transform .3s ease;margin-bottom:10px;z-index:10000}._slide-in_11yfh_25{animation:_slideIn_11yfh_1 .5s ease-out forwards}._slide-out_11yfh_29{animation:_slideOut_11yfh_1 .5s ease-in forwards}@keyframes _slideIn_11yfh_1{0%{transform:translateY(-20px);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes _slideOut_11yfh_1{0%{transform:translateY(0);opacity:1}to{transform:translateY(-20px);opacity:0}}._position-top-left_11yfh_53{top:10px;left:10px}._position-top-right_11yfh_58{top:10px;right:10px}._position-bottom-left_11yfh_63{bottom:10px;left:10px}._position-bottom-right_11yfh_68{bottom:10px;right:10px}@keyframes slideOut{0%{transform:translateY(0);opacity:1}to{transform:translateY(-20px);opacity:0}}.text-ui{position:fixed;padding:8px 12px;background-color:#141414cc;color:#c6c6c6;border-radius:5px;box-shadow:0 4px 8px #0003;z-index:1000;opacity:0;transition:opacity .5s ease-in-out;max-width:300px;display:flex;flex-direction:column;align-items:flex-start;font-family:Poppins,sans-serif}.text-ui.entering{animation:slideIn .5s forwards}.text-ui.exiting{animation:slideOut .5s forwards}.text-ui.center{left:50%;top:50%;transform:translate(-50%,-50%)}.text-ui.center-left{left:10px;top:50%;transform:translateY(-50%)}.text-ui.center-right{right:10px;top:50%;transform:translateY(-50%)}.text-ui .textui-message{display:flex;flex-direction:row;align-items:center;width:100%;gap:10px}.text-ui .textui-bind{display:flex;justify-content:center;align-items:center;width:34px;height:37px;border-radius:3px;color:#f2f2f2;background-color:#369c8161;border:.1vh solid #52938b;text-shadow:0 0 5px rgba(80,188,175,.2509803922)}.text-ui .textui-message-container{display:flex;align-items:center;flex:1;min-width:0}.Instructional{position:fixed;padding:10px 14px;background-color:#141414e6;color:#c6c6c6;border-radius:10px;box-shadow:0 4px 15px #0000004d;z-index:1000;opacity:1;display:flex;flex-direction:row;align-items:center;gap:15px;width:auto;max-width:90%}.Instructional-message{display:flex;align-items:center;width:100%}.Instructional-bind{width:43px;height:40px;background-color:#369c81b3;color:#fff;border-radius:6px;display:flex;justify-content:center;align-items:center;font-family:Poppins,sans-serif;transition:background-color .3s ease}.Instructional-bind.active{background-color:#369c81;box-shadow:0 0 10px #369c81cc}.mouse-icon{width:40px;height:40px;background-color:#369c8180;border-radius:6px;display:flex;justify-content:center;align-items:center}.Instructional-message-container{margin-left:12px;font-family:Poppins,sans-serif;font-size:14px;color:#e0e0e0}.Instructional-description{font-size:12px;color:#b0b0b0;margin-top:2px;width:200px;max-width:200px}body{-webkit-user-select:none;user-select:none;color:#ccc;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;overflow:hidden}.menu{position:fixed;display:flex;flex-direction:column;padding:10px;bottom:40px;right:-500px;transform:translateY(-50%);width:375px;min-height:500px;max-height:500px;border-radius:10px;background-color:#141414;transition:right .7s ease-in-out,opacity .7s ease-in-out,height .7s ease-in-out;opacity:0;pointer-events:none;scrollbar-width:none;-ms-overflow-style:none}.menu::-webkit-scrollbar{display:none}.menu .context-menu-content{display:flex;flex-direction:column;height:100%;overflow-y:auto}.menu .context-menu-card{margin-bottom:10px}.slide-in{right:5%;opacity:1}.slide-out{right:-500px;opacity:0;pointer-events:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{transform:translateY(-20px);opacity:0}to{transform:translateY(0);opacity:1}}.centered-container{display:flex;align-items:center;position:fixed;left:30px;top:50%;transform:translateY(-50%);width:100%;height:100%;z-index:1000;animation:fadeIn .3s ease-in-out}.card{max-width:500px;width:500px;min-height:500px;height:500px;max-height:500px;position:relative;background:#fff;border-radius:8px;animation:slideIn .5s ease-out}.card-header{font-size:1.25rem;font-weight:700;margin-bottom:1rem}.card-content{font-size:1rem;margin-bottom:1rem}.card-footer{display:flex;justify-content:space-between;margin-top:auto}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;height:100%}#root{height:100%}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace} 2 | -------------------------------------------------------------------------------- /web/build/assets/index-C01FryLe.css: -------------------------------------------------------------------------------- 1 | @import"https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;700&display=swap";.leaflet-pane,.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile-container,.leaflet-pane>svg,.leaflet-pane>canvas,.leaflet-zoom-box,.leaflet-image-layer,.leaflet-layer{position:absolute;left:0;top:0}.leaflet-container{overflow:hidden}.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow{-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-user-drag:none}.leaflet-tile::selection{background:transparent}.leaflet-safari .leaflet-tile{image-rendering:-webkit-optimize-contrast}.leaflet-safari .leaflet-tile-container{width:1600px;height:1600px;-webkit-transform-origin:0 0}.leaflet-marker-icon,.leaflet-marker-shadow{display:block}.leaflet-container .leaflet-overlay-pane svg{max-width:none!important;max-height:none!important}.leaflet-container .leaflet-marker-pane img,.leaflet-container .leaflet-shadow-pane img,.leaflet-container .leaflet-tile-pane img,.leaflet-container img.leaflet-image-layer,.leaflet-container .leaflet-tile{max-width:none!important;max-height:none!important;width:auto;padding:0}.leaflet-container img.leaflet-tile{mix-blend-mode:plus-lighter}.leaflet-container.leaflet-touch-zoom{-ms-touch-action:pan-x pan-y;touch-action:pan-x pan-y}.leaflet-container.leaflet-touch-drag{-ms-touch-action:pinch-zoom;touch-action:none;touch-action:pinch-zoom}.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom{-ms-touch-action:none;touch-action:none}.leaflet-container{-webkit-tap-highlight-color:transparent}.leaflet-container a{-webkit-tap-highlight-color:rgba(51,181,229,.4)}.leaflet-tile{filter:inherit;visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-box{width:0;height:0;-moz-box-sizing:border-box;box-sizing:border-box;z-index:800}.leaflet-overlay-pane svg{-moz-user-select:none}.leaflet-pane{z-index:400}.leaflet-tile-pane{z-index:200}.leaflet-overlay-pane{z-index:400}.leaflet-shadow-pane{z-index:500}.leaflet-marker-pane{z-index:600}.leaflet-tooltip-pane{z-index:650}.leaflet-popup-pane{z-index:700}.leaflet-map-pane canvas{z-index:100}.leaflet-map-pane svg{z-index:200}.leaflet-vml-shape{width:1px;height:1px}.lvml{behavior:url(#default#VML);display:inline-block;position:absolute}.leaflet-control{position:relative;z-index:800;pointer-events:visiblePainted;pointer-events:auto}.leaflet-top,.leaflet-bottom{position:absolute;z-index:1000;pointer-events:none}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-control{float:left;clear:both}.leaflet-right .leaflet-control{float:right}.leaflet-top .leaflet-control{margin-top:10px}.leaflet-bottom .leaflet-control{margin-bottom:10px}.leaflet-left .leaflet-control{margin-left:10px}.leaflet-right .leaflet-control{margin-right:10px}.leaflet-fade-anim .leaflet-popup{opacity:0;-webkit-transition:opacity .2s linear;-moz-transition:opacity .2s linear;transition:opacity .2s linear}.leaflet-fade-anim .leaflet-map-pane .leaflet-popup{opacity:1}.leaflet-zoom-animated{-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}svg.leaflet-zoom-animated{will-change:transform}.leaflet-zoom-anim .leaflet-zoom-animated{-webkit-transition:-webkit-transform .25s cubic-bezier(0,0,.25,1);-moz-transition:-moz-transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1)}.leaflet-zoom-anim .leaflet-tile,.leaflet-pan-anim .leaflet-tile{-webkit-transition:none;-moz-transition:none;transition:none}.leaflet-zoom-anim .leaflet-zoom-hide{visibility:hidden}.leaflet-interactive{cursor:pointer}.leaflet-grab{cursor:-webkit-grab;cursor:-moz-grab;cursor:grab}.leaflet-crosshair,.leaflet-crosshair .leaflet-interactive{cursor:crosshair}.leaflet-popup-pane,.leaflet-control{cursor:auto}.leaflet-dragging .leaflet-grab,.leaflet-dragging .leaflet-grab .leaflet-interactive,.leaflet-dragging .leaflet-marker-draggable{cursor:move;cursor:-webkit-grabbing;cursor:-moz-grabbing;cursor:grabbing}.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-image-layer,.leaflet-pane>svg path,.leaflet-tile-container{pointer-events:none}.leaflet-marker-icon.leaflet-interactive,.leaflet-image-layer.leaflet-interactive,.leaflet-pane>svg path.leaflet-interactive,svg.leaflet-image-layer.leaflet-interactive path{pointer-events:visiblePainted;pointer-events:auto}.leaflet-container{background:#ddd;outline-offset:1px}.leaflet-container a{color:#0078a8}.leaflet-zoom-box{border:2px dotted #38f;background:#ffffff80}.leaflet-container{font-family:Helvetica Neue,Arial,Helvetica,sans-serif;font-size:12px;font-size:.75rem;line-height:1.5}.leaflet-bar{box-shadow:0 1px 5px #000000a6;border-radius:4px}.leaflet-bar a{background-color:#fff;border-bottom:1px solid #ccc;width:26px;height:26px;line-height:26px;display:block;text-align:center;text-decoration:none;color:#000}.leaflet-bar a,.leaflet-control-layers-toggle{background-position:50% 50%;background-repeat:no-repeat;display:block}.leaflet-bar a:hover,.leaflet-bar a:focus{background-color:#f4f4f4}.leaflet-bar a:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-bar a:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom:none}.leaflet-bar a.leaflet-disabled{cursor:default;background-color:#f4f4f4;color:#bbb}.leaflet-touch .leaflet-bar a{width:30px;height:30px;line-height:30px}.leaflet-touch .leaflet-bar a:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.leaflet-touch .leaflet-bar a:last-child{border-bottom-left-radius:2px;border-bottom-right-radius:2px}.leaflet-control-zoom-in,.leaflet-control-zoom-out{font:700 18px Lucida Console,Monaco,monospace;text-indent:1px}.leaflet-touch .leaflet-control-zoom-in,.leaflet-touch .leaflet-control-zoom-out{font-size:22px}.leaflet-control-layers{box-shadow:0 1px 5px #0006;background:#fff;border-radius:5px}.leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAQAAAADQ4RFAAACf0lEQVR4AY1UM3gkARTePdvdoTxXKc+qTl3aU5U6b2Kbkz3Gtq3Zw6ziLGNPzrYx7946Tr6/ee/XeCQ4D3ykPtL5tHno4n0d/h3+xfuWHGLX81cn7r0iTNzjr7LrlxCqPtkbTQEHeqOrTy4Yyt3VCi/IOB0v7rVC7q45Q3Gr5K6jt+3Gl5nCoDD4MtO+j96Wu8atmhGqcNGHObuf8OM/x3AMx38+4Z2sPqzCxRFK2aF2e5Jol56XTLyggAMTL56XOMoS1W4pOyjUcGGQdZxU6qRh7B9Zp+PfpOFlqt0zyDZckPi1ttmIp03jX8gyJ8a/PG2yutpS/Vol7peZIbZcKBAEEheEIAgFbDkz5H6Zrkm2hVWGiXKiF4Ycw0RWKdtC16Q7qe3X4iOMxruonzegJzWaXFrU9utOSsLUmrc0YjeWYjCW4PDMADElpJSSQ0vQvA1Tm6/JlKnqFs1EGyZiFCqnRZTEJJJiKRYzVYzJck2Rm6P4iH+cmSY0YzimYa8l0EtTODFWhcMIMVqdsI2uiTvKmTisIDHJ3od5GILVhBCarCfVRmo4uTjkhrhzkiBV7SsaqS+TzrzM1qpGGUFt28pIySQHR6h7F6KSwGWm97ay+Z+ZqMcEjEWebE7wxCSQwpkhJqoZA5ivCdZDjJepuJ9IQjGGUmuXJdBFUygxVqVsxFsLMbDe8ZbDYVCGKxs+W080max1hFCarCfV+C1KATwcnvE9gRRuMP2prdbWGowm1KB1y+zwMMENkM755cJ2yPDtqhTI6ED1M/82yIDtC/4j4BijjeObflpO9I9MwXTCsSX8jWAFeHr05WoLTJ5G8IQVS/7vwR6ohirYM7f6HzYpogfS3R2OAAAAAElFTkSuQmCC);width:36px;height:36px}.leaflet-retina .leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAQAAABvcdNgAAAEsklEQVR4AWL4TydIhpZK1kpWOlg0w3ZXP6D2soBtG42jeI6ZmQTHzAxiTbSJsYLjO9HhP+WOmcuhciVnmHVQcJnp7DFvScowZorad/+V/fVzMdMT2g9Cv9guXGv/7pYOrXh2U+RRR3dSd9JRx6bIFc/ekqHI29JC6pJ5ZEh1yWkhkbcFeSjxgx3L2m1cb1C7bceyxA+CNjT/Ifff+/kDk2u/w/33/IeCMOSaWZ4glosqT3DNnNZQ7Cs58/3Ce5HL78iZH/vKVIaYlqzfdLu8Vi7dnvUbEza5Idt36tquZFldl6N5Z/POLof0XLK61mZCmJSWjVF9tEjUluu74IUXvgttuVIHE7YxSkaYhJZam7yiM9Pv82JYfl9nptxZaxMJE4YSPty+vF0+Y2up9d3wwijfjZbabqm/3bZ9ecKHsiGmRflnn1MW4pjHf9oLufyn2z3y1D6n8g8TZhxyzipLNPnAUpsOiuWimg52psrTZYnOWYNDTMuWBWa0tJb4rgq1UvmutpaYEbZlwU3CLJm/ayYjHW5/h7xWLn9Hh1vepDkyf7dE7MtT5LR4e7yYpHrkhOUpEfssBLq2pPhAqoSWKUkk7EDqkmK6RrCEzqDjhNDWNE+XSMvkJRDWlZTmCW0l0PHQGRZY5t1L83kT0Y3l2SItk5JAWHl2dCOBm+fPu3fo5/3v61RMCO9Jx2EEYYhb0rmNQMX/vm7gqOEJLcXTGw3CAuRNeyaPWwjR8PRqKQ1PDA/dpv+on9Shox52WFnx0KY8onHayrJzm87i5h9xGw/tfkev0jGsQizqezUKjk12hBMKJ4kbCqGPVNXudyyrShovGw5CgxsRICxF6aRmSjlBnHRzg7Gx8fKqEubI2rahQYdR1YgDIRQO7JvQyD52hoIQx0mxa0ODtW2Iozn1le2iIRdzwWewedyZzewidueOGqlsn1MvcnQpuVwLGG3/IR1hIKxCjelIDZ8ldqWz25jWAsnldEnK0Zxro19TGVb2ffIZEsIO89EIEDvKMPrzmBOQcKQ+rroye6NgRRxqR4U8EAkz0CL6uSGOm6KQCdWjvjRiSP1BPalCRS5iQYiEIvxuBMJEWgzSoHADcVMuN7IuqqTeyUPq22qFimFtxDyBBJEwNyt6TM88blFHao/6tWWhuuOM4SAK4EI4QmFHA+SEyWlp4EQoJ13cYGzMu7yszEIBOm2rVmHUNqwAIQabISNMRstmdhNWcFLsSm+0tjJH1MdRxO5Nx0WDMhCtgD6OKgZeljJqJKc9po8juskR9XN0Y1lZ3mWjLR9JCO1jRDMd0fpYC2VnvjBSEFg7wBENc0R9HFlb0xvF1+TBEpF68d+DHR6IOWVv2BECtxo46hOFUBd/APU57WIoEwJhIi2CdpyZX0m93BZicktMj1AS9dClteUFAUNUIEygRZCtik5zSxI9MubTBH1GOiHsiLJ3OCoSZkILa9PxiN0EbvhsAo8tdAf9Seepd36lGWHmtNANTv5Jd0z4QYyeo/UEJqxKRpg5LZx6btLPsOaEmdMyxYdlc8LMaJnikDlhclqmPiQnTEpLUIZEwkRagjYkEibQErwhkTAKCLQEbUgkzJQWc/0PstHHcfEdQ+UAAAAASUVORK5CYII=);background-size:26px 26px}.leaflet-touch .leaflet-control-layers-toggle{width:44px;height:44px}.leaflet-control-layers .leaflet-control-layers-list,.leaflet-control-layers-expanded .leaflet-control-layers-toggle{display:none}.leaflet-control-layers-expanded .leaflet-control-layers-list{display:block;position:relative}.leaflet-control-layers-expanded{padding:6px 10px 6px 6px;color:#333;background:#fff}.leaflet-control-layers-scrollbar{overflow-y:scroll;overflow-x:hidden;padding-right:5px}.leaflet-control-layers-selector{margin-top:2px;position:relative;top:1px}.leaflet-control-layers label{display:block;font-size:13px;font-size:1.08333em}.leaflet-control-layers-separator{height:0;border-top:1px solid #ddd;margin:5px -10px 5px -6px}.leaflet-default-icon-path{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YaZtr2962HUzbDNpjszW24mRt28p47v7zq/bXZtrp/lWnXr337j3nPCe85NcypgSFdugCpW5YoDAMRaIMqRi6aKq5E3YqDQO3qAwjVWrD8Ncq/RBpykd8oZUb/kaJutow8r1aP9II0WmLKLIsJyv1w/kqw9Ch2MYdB++12Onxee/QMwvf4/Dk/Lfp/i4nxTXtOoQ4pW5Aj7wpici1A9erdAN2OH64x8OSP9j3Ft3b7aWkTg/Fm91siTra0f9on5sQr9INejH6CUUUpavjFNq1B+Oadhxmnfa8RfEmN8VNAsQhPqF55xHkMzz3jSmChWU6f7/XZKNH+9+hBLOHYozuKQPxyMPUKkrX/K0uWnfFaJGS1QPRtZsOPtr3NsW0uyh6NNCOkU3Yz+bXbT3I8G3xE5EXLXtCXbbqwCO9zPQYPRTZ5vIDXD7U+w7rFDEoUUf7ibHIR4y6bLVPXrz8JVZEql13trxwue/uDivd3fkWRbS6/IA2bID4uk0UpF1N8qLlbBlXs4Ee7HLTfV1j54APvODnSfOWBqtKVvjgLKzF5YdEk5ewRkGlK0i33Eofffc7HT56jD7/6U+qH3Cx7SBLNntH5YIPvODnyfIXZYRVDPqgHtLs5ABHD3YzLuespb7t79FY34DjMwrVrcTuwlT55YMPvOBnRrJ4VXTdNnYug5ucHLBjEpt30701A3Ts+HEa73u6dT3FNWwflY86eMHPk+Yu+i6pzUpRrW7SNDg5JHR4KapmM5Wv2E8Tfcb1HoqqHMHU+uWDD7zg54mz5/2BSnizi9T1Dg4QQXLToGNCkb6tb1NU+QAlGr1++eADrzhn/u8Q2YZhQVlZ5+CAOtqfbhmaUCS1ezNFVm2imDbPmPng5wmz+gwh+oHDce0eUtQ6OGDIyR0uUhUsoO3vfDmmgOezH0mZN59x7MBi++WDL1g/eEiU3avlidO671bkLfwbw5XV2P8Pzo0ydy4t2/0eu33xYSOMOD8hTf4CrBtGMSoXfPLchX+J0ruSePw3LZeK0juPJbYzrhkH0io7B3k164hiGvawhOKMLkrQLyVpZg8rHFW7E2uHOL888IBPlNZ1FPzstSJM694fWr6RwpvcJK60+0HCILTBzZLFNdtAzJaohze60T8qBzyh5ZuOg5e7uwQppofEmf2++DYvmySqGBuKaicF1blQjhuHdvCIMvp8whTTfZzI7RldpwtSzL+F1+wkdZ2TBOW2gIF88PBTzD/gpeREAMEbxnJcaJHNHrpzji0gQCS6hdkEeYt9DF/2qPcEC8RM28Hwmr3sdNyht00byAut2k3gufWNtgtOEOFGUwcXWNDbdNbpgBGxEvKkOQsxivJx33iow0Vw5S6SVTrpVq11ysA2Rp7gTfPfktc6zhtXBBC+adRLshf6sG2RfHPZ5EAc4sVZ83yCN00Fk/4kggu40ZTvIEm5g24qtU4KjBrx/BTTH8ifVASAG7gKrnWxJDcU7x8X6Ecczhm3o6YicvsLXWfh3Ch1W0k8x0nXF+0fFxgt4phz8QvypiwCCFKMqXCnqXExjq10beH+UUA7+nG6mdG/Pu0f3LgFcGrl2s0kNNjpmoJ9o4B29CMO8dMT4Q5ox8uitF6fqsrJOr8qnwNbRzv6hSnG5wP+64C7h9lp30hKNtKdWjtdkbuPA19nJ7Tz3zR/ibgARbhb4AlhavcBebmTHcFl2fvYEnW0ox9xMxKBS8btJ+KiEbq9zA4RthQXDhPa0T9TEe69gWupwc6uBUphquXgf+/FrIjweHQS4/pduMe5ERUMHUd9xv8ZR98CxkS4F2n3EUrUZ10EYNw7BWm9x1GiPssi3GgiGRDKWRYZfXlON+dfNbM+GgIwYdwAAAAASUVORK5CYII=)}.leaflet-container .leaflet-control-attribution{background:#fff;background:#fffc;margin:0}.leaflet-control-attribution,.leaflet-control-scale-line{padding:0 5px;color:#333;line-height:1.4}.leaflet-control-attribution a{text-decoration:none}.leaflet-control-attribution a:hover,.leaflet-control-attribution a:focus{text-decoration:underline}.leaflet-attribution-flag{display:inline!important;vertical-align:baseline!important;width:1em;height:.6669em}.leaflet-left .leaflet-control-scale{margin-left:5px}.leaflet-bottom .leaflet-control-scale{margin-bottom:5px}.leaflet-control-scale-line{border:2px solid #777;border-top:none;line-height:1.1;padding:2px 5px 1px;white-space:nowrap;-moz-box-sizing:border-box;box-sizing:border-box;background:#fffc;text-shadow:1px 1px #fff}.leaflet-control-scale-line:not(:first-child){border-top:2px solid #777;border-bottom:none;margin-top:-2px}.leaflet-control-scale-line:not(:first-child):not(:last-child){border-bottom:2px solid #777}.leaflet-touch .leaflet-control-attribution,.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{box-shadow:none}.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{border:2px solid rgba(0,0,0,.2);background-clip:padding-box}.leaflet-popup{position:absolute;text-align:center;margin-bottom:20px}.leaflet-popup-content-wrapper{padding:1px;text-align:left;border-radius:12px}.leaflet-popup-content{margin:13px 24px 13px 20px;line-height:1.3;font-size:13px;font-size:1.08333em;min-height:1px}.leaflet-popup-content p{margin:1.3em 0}.leaflet-popup-tip-container{width:40px;height:20px;position:absolute;left:50%;margin-top:-1px;margin-left:-20px;overflow:hidden;pointer-events:none}.leaflet-popup-tip{width:17px;height:17px;padding:1px;margin:-10px auto 0;pointer-events:auto;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.leaflet-popup-content-wrapper,.leaflet-popup-tip{background:#fff;color:#333;box-shadow:0 3px 14px #0006}.leaflet-container a.leaflet-popup-close-button{position:absolute;top:0;right:0;border:none;text-align:center;width:24px;height:24px;font:16px/24px Tahoma,Verdana,sans-serif;color:#757575;text-decoration:none;background:transparent}.leaflet-container a.leaflet-popup-close-button:hover,.leaflet-container a.leaflet-popup-close-button:focus{color:#585858}.leaflet-popup-scrolled{overflow:auto}.leaflet-oldie .leaflet-popup-content-wrapper{-ms-zoom:1}.leaflet-oldie .leaflet-popup-tip{width:24px;margin:0 auto;-ms-filter:"progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";filter:progid:DXImageTransform.Microsoft.Matrix(M11=.70710678,M12=.70710678,M21=-.70710678,M22=.70710678)}.leaflet-oldie .leaflet-control-zoom,.leaflet-oldie .leaflet-control-layers,.leaflet-oldie .leaflet-popup-content-wrapper,.leaflet-oldie .leaflet-popup-tip{border:1px solid #999}.leaflet-div-icon{background:#fff;border:1px solid #666}.leaflet-tooltip{position:absolute;padding:6px;background-color:#fff;border:1px solid #fff;border-radius:3px;color:#222;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;box-shadow:0 1px 3px #0006}.leaflet-tooltip.leaflet-interactive{cursor:pointer;pointer-events:auto}.leaflet-tooltip-top:before,.leaflet-tooltip-bottom:before,.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{position:absolute;pointer-events:none;border:6px solid transparent;background:transparent;content:""}.leaflet-tooltip-bottom{margin-top:6px}.leaflet-tooltip-top{margin-top:-6px}.leaflet-tooltip-bottom:before,.leaflet-tooltip-top:before{left:50%;margin-left:-6px}.leaflet-tooltip-top:before{bottom:0;margin-bottom:-12px;border-top-color:#fff}.leaflet-tooltip-bottom:before{top:0;margin-top:-12px;margin-left:-6px;border-bottom-color:#fff}.leaflet-tooltip-left{margin-left:-6px}.leaflet-tooltip-right{margin-left:6px}.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{top:50%;margin-top:-6px}.leaflet-tooltip-left:before{right:0;margin-right:-12px;border-left-color:#fff}.leaflet-tooltip-right:before{left:0;margin-left:-12px;border-right-color:#fff}@media print{.leaflet-control{-webkit-print-color-adjust:exact;print-color-adjust:exact}}._container_11yfh_1{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;z-index:9999}._notification_11yfh_11{max-width:300px;min-width:300px;padding:12px;background-color:#141414cc;border-radius:9px;display:flex;align-items:center;pointer-events:auto;transition:opacity .3s ease,transform .3s ease;margin-bottom:10px;z-index:10000}._slide-in_11yfh_25{animation:_slideIn_11yfh_1 .5s ease-out forwards}._slide-out_11yfh_29{animation:_slideOut_11yfh_1 .5s ease-in forwards}@keyframes _slideIn_11yfh_1{0%{transform:translateY(-20px);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes _slideOut_11yfh_1{0%{transform:translateY(0);opacity:1}to{transform:translateY(-20px);opacity:0}}._position-top-left_11yfh_53{top:10px;left:10px}._position-top-right_11yfh_58{top:10px;right:10px}._position-bottom-left_11yfh_63{bottom:10px;left:10px}._position-bottom-right_11yfh_68{bottom:10px;right:10px}@keyframes slideOut{0%{transform:translateY(0);opacity:1}to{transform:translateY(-20px);opacity:0}}.text-ui{position:fixed;padding:8px 12px;background-color:#141414cc;color:#c6c6c6;border-radius:5px;box-shadow:0 4px 8px #0003;z-index:1000;opacity:0;transition:opacity .5s ease-in-out;max-width:300px;display:flex;flex-direction:column;align-items:flex-start}.text-ui.entering{animation:slideIn .5s forwards}.text-ui.exiting{animation:slideOut .5s forwards}.text-ui.center{left:50%;top:50%;transform:translate(-50%,-50%)}.text-ui.center-left{left:10px;top:50%;transform:translateY(-50%)}.text-ui.center-right{right:10px;top:50%;transform:translateY(-50%)}.text-ui .textui-message{display:flex;flex-direction:row;align-items:center;gap:7px;width:100%}.text-ui .textui-bind{display:flex;justify-content:center;align-items:center;width:34px;height:37px;border-radius:3px;color:#f2f2f2;background-color:#369c8161;border:.1vh solid #52938b;text-shadow:0 0 5px rgba(80,188,175,.2509803922)}.text-ui .textui-message-container{display:flex;align-items:center;flex:1;min-width:0}.Instructional{position:fixed;padding:10px 14px;background-color:#141414e6;color:#c6c6c6;border-radius:10px;box-shadow:0 4px 15px #0000004d;z-index:1000;opacity:1;display:flex;flex-direction:row;align-items:center;gap:15px;width:auto;max-width:90%}.Instructional-message{display:flex;align-items:center;width:100%}.Instructional-bind{width:43px;height:40px;background-color:#369c81b3;color:#fff;border-radius:6px;display:flex;justify-content:center;align-items:center;font-family:Poppins,sans-serif;transition:background-color .3s ease}.Instructional-bind.active{background-color:#369c81;box-shadow:0 0 10px #369c81cc}.mouse-icon{width:40px;height:40px;background-color:#369c8180;border-radius:6px;display:flex;justify-content:center;align-items:center}.Instructional-message-container{margin-left:12px;font-family:Poppins,sans-serif;font-size:14px;color:#e0e0e0}.Instructional-description{font-size:12px;color:#b0b0b0;margin-top:2px;width:200px;max-width:200px}body{-webkit-user-select:none;user-select:none;color:#ccc;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;overflow:hidden}.menu{position:fixed;display:flex;flex-direction:column;padding:10px;bottom:40px;right:-500px;transform:translateY(-50%);width:375px;min-height:500px;max-height:500px;border-radius:10px;background-color:#141414;transition:right .7s ease-in-out,opacity .7s ease-in-out,height .7s ease-in-out;opacity:0;pointer-events:none;scrollbar-width:none;-ms-overflow-style:none}.menu::-webkit-scrollbar{display:none}.menu .context-menu-content{display:flex;flex-direction:column;height:100%;overflow-y:auto}.menu .context-menu-card{margin-bottom:10px}.slide-in{right:5%;opacity:1}.slide-out{right:-500px;opacity:0;pointer-events:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{transform:translateY(-20px);opacity:0}to{transform:translateY(0);opacity:1}}.centered-container{display:flex;align-items:center;position:fixed;left:30px;top:50%;transform:translateY(-50%);width:100%;height:100%;z-index:1000;animation:fadeIn .3s ease-in-out}.card{max-width:500px;width:500px;min-height:500px;height:500px;max-height:500px;position:relative;background:#fff;border-radius:8px;animation:slideIn .5s ease-out}.card-header{font-size:1.25rem;font-weight:700;margin-bottom:1rem}.card-content{font-size:1rem;margin-bottom:1rem}.card-footer{display:flex;justify-content:space-between;margin-top:auto}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;height:100%}#root{height:100%}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace} 2 | -------------------------------------------------------------------------------- /web/build/assets/index-CcFNomvE.css: -------------------------------------------------------------------------------- 1 | @import"https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;700&display=swap";.leaflet-pane,.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile-container,.leaflet-pane>svg,.leaflet-pane>canvas,.leaflet-zoom-box,.leaflet-image-layer,.leaflet-layer{position:absolute;left:0;top:0}.leaflet-container{overflow:hidden}.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow{-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-user-drag:none}.leaflet-tile::selection{background:transparent}.leaflet-safari .leaflet-tile{image-rendering:-webkit-optimize-contrast}.leaflet-safari .leaflet-tile-container{width:1600px;height:1600px;-webkit-transform-origin:0 0}.leaflet-marker-icon,.leaflet-marker-shadow{display:block}.leaflet-container .leaflet-overlay-pane svg{max-width:none!important;max-height:none!important}.leaflet-container .leaflet-marker-pane img,.leaflet-container .leaflet-shadow-pane img,.leaflet-container .leaflet-tile-pane img,.leaflet-container img.leaflet-image-layer,.leaflet-container .leaflet-tile{max-width:none!important;max-height:none!important;width:auto;padding:0}.leaflet-container img.leaflet-tile{mix-blend-mode:plus-lighter}.leaflet-container.leaflet-touch-zoom{-ms-touch-action:pan-x pan-y;touch-action:pan-x pan-y}.leaflet-container.leaflet-touch-drag{-ms-touch-action:pinch-zoom;touch-action:none;touch-action:pinch-zoom}.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom{-ms-touch-action:none;touch-action:none}.leaflet-container{-webkit-tap-highlight-color:transparent}.leaflet-container a{-webkit-tap-highlight-color:rgba(51,181,229,.4)}.leaflet-tile{filter:inherit;visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-box{width:0;height:0;-moz-box-sizing:border-box;box-sizing:border-box;z-index:800}.leaflet-overlay-pane svg{-moz-user-select:none}.leaflet-pane{z-index:400}.leaflet-tile-pane{z-index:200}.leaflet-overlay-pane{z-index:400}.leaflet-shadow-pane{z-index:500}.leaflet-marker-pane{z-index:600}.leaflet-tooltip-pane{z-index:650}.leaflet-popup-pane{z-index:700}.leaflet-map-pane canvas{z-index:100}.leaflet-map-pane svg{z-index:200}.leaflet-vml-shape{width:1px;height:1px}.lvml{behavior:url(#default#VML);display:inline-block;position:absolute}.leaflet-control{position:relative;z-index:800;pointer-events:visiblePainted;pointer-events:auto}.leaflet-top,.leaflet-bottom{position:absolute;z-index:1000;pointer-events:none}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-control{float:left;clear:both}.leaflet-right .leaflet-control{float:right}.leaflet-top .leaflet-control{margin-top:10px}.leaflet-bottom .leaflet-control{margin-bottom:10px}.leaflet-left .leaflet-control{margin-left:10px}.leaflet-right .leaflet-control{margin-right:10px}.leaflet-fade-anim .leaflet-popup{opacity:0;-webkit-transition:opacity .2s linear;-moz-transition:opacity .2s linear;transition:opacity .2s linear}.leaflet-fade-anim .leaflet-map-pane .leaflet-popup{opacity:1}.leaflet-zoom-animated{-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}svg.leaflet-zoom-animated{will-change:transform}.leaflet-zoom-anim .leaflet-zoom-animated{-webkit-transition:-webkit-transform .25s cubic-bezier(0,0,.25,1);-moz-transition:-moz-transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1)}.leaflet-zoom-anim .leaflet-tile,.leaflet-pan-anim .leaflet-tile{-webkit-transition:none;-moz-transition:none;transition:none}.leaflet-zoom-anim .leaflet-zoom-hide{visibility:hidden}.leaflet-interactive{cursor:pointer}.leaflet-grab{cursor:-webkit-grab;cursor:-moz-grab;cursor:grab}.leaflet-crosshair,.leaflet-crosshair .leaflet-interactive{cursor:crosshair}.leaflet-popup-pane,.leaflet-control{cursor:auto}.leaflet-dragging .leaflet-grab,.leaflet-dragging .leaflet-grab .leaflet-interactive,.leaflet-dragging .leaflet-marker-draggable{cursor:move;cursor:-webkit-grabbing;cursor:-moz-grabbing;cursor:grabbing}.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-image-layer,.leaflet-pane>svg path,.leaflet-tile-container{pointer-events:none}.leaflet-marker-icon.leaflet-interactive,.leaflet-image-layer.leaflet-interactive,.leaflet-pane>svg path.leaflet-interactive,svg.leaflet-image-layer.leaflet-interactive path{pointer-events:visiblePainted;pointer-events:auto}.leaflet-container{background:#ddd;outline-offset:1px}.leaflet-container a{color:#0078a8}.leaflet-zoom-box{border:2px dotted #38f;background:#ffffff80}.leaflet-container{font-family:Helvetica Neue,Arial,Helvetica,sans-serif;font-size:12px;font-size:.75rem;line-height:1.5}.leaflet-bar{box-shadow:0 1px 5px #000000a6;border-radius:4px}.leaflet-bar a{background-color:#fff;border-bottom:1px solid #ccc;width:26px;height:26px;line-height:26px;display:block;text-align:center;text-decoration:none;color:#000}.leaflet-bar a,.leaflet-control-layers-toggle{background-position:50% 50%;background-repeat:no-repeat;display:block}.leaflet-bar a:hover,.leaflet-bar a:focus{background-color:#f4f4f4}.leaflet-bar a:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-bar a:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom:none}.leaflet-bar a.leaflet-disabled{cursor:default;background-color:#f4f4f4;color:#bbb}.leaflet-touch .leaflet-bar a{width:30px;height:30px;line-height:30px}.leaflet-touch .leaflet-bar a:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.leaflet-touch .leaflet-bar a:last-child{border-bottom-left-radius:2px;border-bottom-right-radius:2px}.leaflet-control-zoom-in,.leaflet-control-zoom-out{font:700 18px Lucida Console,Monaco,monospace;text-indent:1px}.leaflet-touch .leaflet-control-zoom-in,.leaflet-touch .leaflet-control-zoom-out{font-size:22px}.leaflet-control-layers{box-shadow:0 1px 5px #0006;background:#fff;border-radius:5px}.leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAQAAAADQ4RFAAACf0lEQVR4AY1UM3gkARTePdvdoTxXKc+qTl3aU5U6b2Kbkz3Gtq3Zw6ziLGNPzrYx7946Tr6/ee/XeCQ4D3ykPtL5tHno4n0d/h3+xfuWHGLX81cn7r0iTNzjr7LrlxCqPtkbTQEHeqOrTy4Yyt3VCi/IOB0v7rVC7q45Q3Gr5K6jt+3Gl5nCoDD4MtO+j96Wu8atmhGqcNGHObuf8OM/x3AMx38+4Z2sPqzCxRFK2aF2e5Jol56XTLyggAMTL56XOMoS1W4pOyjUcGGQdZxU6qRh7B9Zp+PfpOFlqt0zyDZckPi1ttmIp03jX8gyJ8a/PG2yutpS/Vol7peZIbZcKBAEEheEIAgFbDkz5H6Zrkm2hVWGiXKiF4Ycw0RWKdtC16Q7qe3X4iOMxruonzegJzWaXFrU9utOSsLUmrc0YjeWYjCW4PDMADElpJSSQ0vQvA1Tm6/JlKnqFs1EGyZiFCqnRZTEJJJiKRYzVYzJck2Rm6P4iH+cmSY0YzimYa8l0EtTODFWhcMIMVqdsI2uiTvKmTisIDHJ3od5GILVhBCarCfVRmo4uTjkhrhzkiBV7SsaqS+TzrzM1qpGGUFt28pIySQHR6h7F6KSwGWm97ay+Z+ZqMcEjEWebE7wxCSQwpkhJqoZA5ivCdZDjJepuJ9IQjGGUmuXJdBFUygxVqVsxFsLMbDe8ZbDYVCGKxs+W080max1hFCarCfV+C1KATwcnvE9gRRuMP2prdbWGowm1KB1y+zwMMENkM755cJ2yPDtqhTI6ED1M/82yIDtC/4j4BijjeObflpO9I9MwXTCsSX8jWAFeHr05WoLTJ5G8IQVS/7vwR6ohirYM7f6HzYpogfS3R2OAAAAAElFTkSuQmCC);width:36px;height:36px}.leaflet-retina .leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAQAAABvcdNgAAAEsklEQVR4AWL4TydIhpZK1kpWOlg0w3ZXP6D2soBtG42jeI6ZmQTHzAxiTbSJsYLjO9HhP+WOmcuhciVnmHVQcJnp7DFvScowZorad/+V/fVzMdMT2g9Cv9guXGv/7pYOrXh2U+RRR3dSd9JRx6bIFc/ekqHI29JC6pJ5ZEh1yWkhkbcFeSjxgx3L2m1cb1C7bceyxA+CNjT/Ifff+/kDk2u/w/33/IeCMOSaWZ4glosqT3DNnNZQ7Cs58/3Ce5HL78iZH/vKVIaYlqzfdLu8Vi7dnvUbEza5Idt36tquZFldl6N5Z/POLof0XLK61mZCmJSWjVF9tEjUluu74IUXvgttuVIHE7YxSkaYhJZam7yiM9Pv82JYfl9nptxZaxMJE4YSPty+vF0+Y2up9d3wwijfjZbabqm/3bZ9ecKHsiGmRflnn1MW4pjHf9oLufyn2z3y1D6n8g8TZhxyzipLNPnAUpsOiuWimg52psrTZYnOWYNDTMuWBWa0tJb4rgq1UvmutpaYEbZlwU3CLJm/ayYjHW5/h7xWLn9Hh1vepDkyf7dE7MtT5LR4e7yYpHrkhOUpEfssBLq2pPhAqoSWKUkk7EDqkmK6RrCEzqDjhNDWNE+XSMvkJRDWlZTmCW0l0PHQGRZY5t1L83kT0Y3l2SItk5JAWHl2dCOBm+fPu3fo5/3v61RMCO9Jx2EEYYhb0rmNQMX/vm7gqOEJLcXTGw3CAuRNeyaPWwjR8PRqKQ1PDA/dpv+on9Shox52WFnx0KY8onHayrJzm87i5h9xGw/tfkev0jGsQizqezUKjk12hBMKJ4kbCqGPVNXudyyrShovGw5CgxsRICxF6aRmSjlBnHRzg7Gx8fKqEubI2rahQYdR1YgDIRQO7JvQyD52hoIQx0mxa0ODtW2Iozn1le2iIRdzwWewedyZzewidueOGqlsn1MvcnQpuVwLGG3/IR1hIKxCjelIDZ8ldqWz25jWAsnldEnK0Zxro19TGVb2ffIZEsIO89EIEDvKMPrzmBOQcKQ+rroye6NgRRxqR4U8EAkz0CL6uSGOm6KQCdWjvjRiSP1BPalCRS5iQYiEIvxuBMJEWgzSoHADcVMuN7IuqqTeyUPq22qFimFtxDyBBJEwNyt6TM88blFHao/6tWWhuuOM4SAK4EI4QmFHA+SEyWlp4EQoJ13cYGzMu7yszEIBOm2rVmHUNqwAIQabISNMRstmdhNWcFLsSm+0tjJH1MdRxO5Nx0WDMhCtgD6OKgZeljJqJKc9po8juskR9XN0Y1lZ3mWjLR9JCO1jRDMd0fpYC2VnvjBSEFg7wBENc0R9HFlb0xvF1+TBEpF68d+DHR6IOWVv2BECtxo46hOFUBd/APU57WIoEwJhIi2CdpyZX0m93BZicktMj1AS9dClteUFAUNUIEygRZCtik5zSxI9MubTBH1GOiHsiLJ3OCoSZkILa9PxiN0EbvhsAo8tdAf9Seepd36lGWHmtNANTv5Jd0z4QYyeo/UEJqxKRpg5LZx6btLPsOaEmdMyxYdlc8LMaJnikDlhclqmPiQnTEpLUIZEwkRagjYkEibQErwhkTAKCLQEbUgkzJQWc/0PstHHcfEdQ+UAAAAASUVORK5CYII=);background-size:26px 26px}.leaflet-touch .leaflet-control-layers-toggle{width:44px;height:44px}.leaflet-control-layers .leaflet-control-layers-list,.leaflet-control-layers-expanded .leaflet-control-layers-toggle{display:none}.leaflet-control-layers-expanded .leaflet-control-layers-list{display:block;position:relative}.leaflet-control-layers-expanded{padding:6px 10px 6px 6px;color:#333;background:#fff}.leaflet-control-layers-scrollbar{overflow-y:scroll;overflow-x:hidden;padding-right:5px}.leaflet-control-layers-selector{margin-top:2px;position:relative;top:1px}.leaflet-control-layers label{display:block;font-size:13px;font-size:1.08333em}.leaflet-control-layers-separator{height:0;border-top:1px solid #ddd;margin:5px -10px 5px -6px}.leaflet-default-icon-path{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YaZtr2962HUzbDNpjszW24mRt28p47v7zq/bXZtrp/lWnXr337j3nPCe85NcypgSFdugCpW5YoDAMRaIMqRi6aKq5E3YqDQO3qAwjVWrD8Ncq/RBpykd8oZUb/kaJutow8r1aP9II0WmLKLIsJyv1w/kqw9Ch2MYdB++12Onxee/QMwvf4/Dk/Lfp/i4nxTXtOoQ4pW5Aj7wpici1A9erdAN2OH64x8OSP9j3Ft3b7aWkTg/Fm91siTra0f9on5sQr9INejH6CUUUpavjFNq1B+Oadhxmnfa8RfEmN8VNAsQhPqF55xHkMzz3jSmChWU6f7/XZKNH+9+hBLOHYozuKQPxyMPUKkrX/K0uWnfFaJGS1QPRtZsOPtr3NsW0uyh6NNCOkU3Yz+bXbT3I8G3xE5EXLXtCXbbqwCO9zPQYPRTZ5vIDXD7U+w7rFDEoUUf7ibHIR4y6bLVPXrz8JVZEql13trxwue/uDivd3fkWRbS6/IA2bID4uk0UpF1N8qLlbBlXs4Ee7HLTfV1j54APvODnSfOWBqtKVvjgLKzF5YdEk5ewRkGlK0i33Eofffc7HT56jD7/6U+qH3Cx7SBLNntH5YIPvODnyfIXZYRVDPqgHtLs5ABHD3YzLuespb7t79FY34DjMwrVrcTuwlT55YMPvOBnRrJ4VXTdNnYug5ucHLBjEpt30701A3Ts+HEa73u6dT3FNWwflY86eMHPk+Yu+i6pzUpRrW7SNDg5JHR4KapmM5Wv2E8Tfcb1HoqqHMHU+uWDD7zg54mz5/2BSnizi9T1Dg4QQXLToGNCkb6tb1NU+QAlGr1++eADrzhn/u8Q2YZhQVlZ5+CAOtqfbhmaUCS1ezNFVm2imDbPmPng5wmz+gwh+oHDce0eUtQ6OGDIyR0uUhUsoO3vfDmmgOezH0mZN59x7MBi++WDL1g/eEiU3avlidO671bkLfwbw5XV2P8Pzo0ydy4t2/0eu33xYSOMOD8hTf4CrBtGMSoXfPLchX+J0ruSePw3LZeK0juPJbYzrhkH0io7B3k164hiGvawhOKMLkrQLyVpZg8rHFW7E2uHOL888IBPlNZ1FPzstSJM694fWr6RwpvcJK60+0HCILTBzZLFNdtAzJaohze60T8qBzyh5ZuOg5e7uwQppofEmf2++DYvmySqGBuKaicF1blQjhuHdvCIMvp8whTTfZzI7RldpwtSzL+F1+wkdZ2TBOW2gIF88PBTzD/gpeREAMEbxnJcaJHNHrpzji0gQCS6hdkEeYt9DF/2qPcEC8RM28Hwmr3sdNyht00byAut2k3gufWNtgtOEOFGUwcXWNDbdNbpgBGxEvKkOQsxivJx33iow0Vw5S6SVTrpVq11ysA2Rp7gTfPfktc6zhtXBBC+adRLshf6sG2RfHPZ5EAc4sVZ83yCN00Fk/4kggu40ZTvIEm5g24qtU4KjBrx/BTTH8ifVASAG7gKrnWxJDcU7x8X6Ecczhm3o6YicvsLXWfh3Ch1W0k8x0nXF+0fFxgt4phz8QvypiwCCFKMqXCnqXExjq10beH+UUA7+nG6mdG/Pu0f3LgFcGrl2s0kNNjpmoJ9o4B29CMO8dMT4Q5ox8uitF6fqsrJOr8qnwNbRzv6hSnG5wP+64C7h9lp30hKNtKdWjtdkbuPA19nJ7Tz3zR/ibgARbhb4AlhavcBebmTHcFl2fvYEnW0ox9xMxKBS8btJ+KiEbq9zA4RthQXDhPa0T9TEe69gWupwc6uBUphquXgf+/FrIjweHQS4/pduMe5ERUMHUd9xv8ZR98CxkS4F2n3EUrUZ10EYNw7BWm9x1GiPssi3GgiGRDKWRYZfXlON+dfNbM+GgIwYdwAAAAASUVORK5CYII=)}.leaflet-container .leaflet-control-attribution{background:#fff;background:#fffc;margin:0}.leaflet-control-attribution,.leaflet-control-scale-line{padding:0 5px;color:#333;line-height:1.4}.leaflet-control-attribution a{text-decoration:none}.leaflet-control-attribution a:hover,.leaflet-control-attribution a:focus{text-decoration:underline}.leaflet-attribution-flag{display:inline!important;vertical-align:baseline!important;width:1em;height:.6669em}.leaflet-left .leaflet-control-scale{margin-left:5px}.leaflet-bottom .leaflet-control-scale{margin-bottom:5px}.leaflet-control-scale-line{border:2px solid #777;border-top:none;line-height:1.1;padding:2px 5px 1px;white-space:nowrap;-moz-box-sizing:border-box;box-sizing:border-box;background:#fffc;text-shadow:1px 1px #fff}.leaflet-control-scale-line:not(:first-child){border-top:2px solid #777;border-bottom:none;margin-top:-2px}.leaflet-control-scale-line:not(:first-child):not(:last-child){border-bottom:2px solid #777}.leaflet-touch .leaflet-control-attribution,.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{box-shadow:none}.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{border:2px solid rgba(0,0,0,.2);background-clip:padding-box}.leaflet-popup{position:absolute;text-align:center;margin-bottom:20px}.leaflet-popup-content-wrapper{padding:1px;text-align:left;border-radius:12px}.leaflet-popup-content{margin:13px 24px 13px 20px;line-height:1.3;font-size:13px;font-size:1.08333em;min-height:1px}.leaflet-popup-content p{margin:1.3em 0}.leaflet-popup-tip-container{width:40px;height:20px;position:absolute;left:50%;margin-top:-1px;margin-left:-20px;overflow:hidden;pointer-events:none}.leaflet-popup-tip{width:17px;height:17px;padding:1px;margin:-10px auto 0;pointer-events:auto;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.leaflet-popup-content-wrapper,.leaflet-popup-tip{background:#fff;color:#333;box-shadow:0 3px 14px #0006}.leaflet-container a.leaflet-popup-close-button{position:absolute;top:0;right:0;border:none;text-align:center;width:24px;height:24px;font:16px/24px Tahoma,Verdana,sans-serif;color:#757575;text-decoration:none;background:transparent}.leaflet-container a.leaflet-popup-close-button:hover,.leaflet-container a.leaflet-popup-close-button:focus{color:#585858}.leaflet-popup-scrolled{overflow:auto}.leaflet-oldie .leaflet-popup-content-wrapper{-ms-zoom:1}.leaflet-oldie .leaflet-popup-tip{width:24px;margin:0 auto;-ms-filter:"progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";filter:progid:DXImageTransform.Microsoft.Matrix(M11=.70710678,M12=.70710678,M21=-.70710678,M22=.70710678)}.leaflet-oldie .leaflet-control-zoom,.leaflet-oldie .leaflet-control-layers,.leaflet-oldie .leaflet-popup-content-wrapper,.leaflet-oldie .leaflet-popup-tip{border:1px solid #999}.leaflet-div-icon{background:#fff;border:1px solid #666}.leaflet-tooltip{position:absolute;padding:6px;background-color:#fff;border:1px solid #fff;border-radius:3px;color:#222;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;box-shadow:0 1px 3px #0006}.leaflet-tooltip.leaflet-interactive{cursor:pointer;pointer-events:auto}.leaflet-tooltip-top:before,.leaflet-tooltip-bottom:before,.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{position:absolute;pointer-events:none;border:6px solid transparent;background:transparent;content:""}.leaflet-tooltip-bottom{margin-top:6px}.leaflet-tooltip-top{margin-top:-6px}.leaflet-tooltip-bottom:before,.leaflet-tooltip-top:before{left:50%;margin-left:-6px}.leaflet-tooltip-top:before{bottom:0;margin-bottom:-12px;border-top-color:#fff}.leaflet-tooltip-bottom:before{top:0;margin-top:-12px;margin-left:-6px;border-bottom-color:#fff}.leaflet-tooltip-left{margin-left:-6px}.leaflet-tooltip-right{margin-left:6px}.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{top:50%;margin-top:-6px}.leaflet-tooltip-left:before{right:0;margin-right:-12px;border-left-color:#fff}.leaflet-tooltip-right:before{left:0;margin-left:-12px;border-right-color:#fff}@media print{.leaflet-control{-webkit-print-color-adjust:exact;print-color-adjust:exact}}._container_11yfh_1{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;z-index:9999}._notification_11yfh_11{max-width:300px;min-width:300px;padding:12px;background-color:#141414cc;border-radius:9px;display:flex;align-items:center;pointer-events:auto;transition:opacity .3s ease,transform .3s ease;margin-bottom:10px;z-index:10000}._slide-in_11yfh_25{animation:_slideIn_11yfh_1 .5s ease-out forwards}._slide-out_11yfh_29{animation:_slideOut_11yfh_1 .5s ease-in forwards}@keyframes _slideIn_11yfh_1{0%{transform:translateY(-20px);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes _slideOut_11yfh_1{0%{transform:translateY(0);opacity:1}to{transform:translateY(-20px);opacity:0}}._position-top-left_11yfh_53{top:10px;left:10px}._position-top-right_11yfh_58{top:10px;right:10px}._position-bottom-left_11yfh_63{bottom:10px;left:10px}._position-bottom-right_11yfh_68{bottom:10px;right:10px}@keyframes slideOut{0%{transform:translateY(0);opacity:1}to{transform:translateY(-20px);opacity:0}}.text-ui{position:fixed;padding:8px 12px;background-color:#141414cc;color:#c6c6c6;border-radius:5px;box-shadow:0 4px 8px #0003;z-index:1000;opacity:0;transition:opacity .5s ease-in-out;max-width:300px;display:flex;flex-direction:column;align-items:flex-start}.text-ui.entering{animation:slideIn .5s forwards}.text-ui.exiting{animation:slideOut .5s forwards}.text-ui.center{left:50%;top:50%;transform:translate(-50%,-50%)}.text-ui.center-left{left:10px;top:50%;transform:translateY(-50%)}.text-ui.center-right{right:10px;top:50%;transform:translateY(-50%)}.text-ui .textui-message{display:flex;flex-direction:row;align-items:center;gap:7px;width:100%}.text-ui .textui-bind{display:flex;justify-content:center;align-items:center;width:34px;height:37px;border-radius:3px;color:#f2f2f2;background-color:#369c8161;border:.1vh solid #52938b;text-shadow:0 0 5px rgba(80,188,175,.2509803922)}.text-ui .textui-message-container{display:flex;align-items:center;flex:1;min-width:0}body{-webkit-user-select:none;user-select:none;color:#ccc;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;overflow:hidden}.menu{position:fixed;display:flex;flex-direction:column;padding:10px;bottom:40px;right:-500px;transform:translateY(-50%);width:375px;min-height:500px;max-height:500px;border-radius:10px;background-color:#141414;transition:right .7s ease-in-out,opacity .7s ease-in-out,height .7s ease-in-out;opacity:0;pointer-events:none;scrollbar-width:none;-ms-overflow-style:none}.menu::-webkit-scrollbar{display:none}.menu .context-menu-content{display:flex;flex-direction:column;height:100%;overflow-y:auto}.menu .context-menu-card{margin-bottom:10px}.slide-in{right:5%;opacity:1}.slide-out{right:-500px;opacity:0;pointer-events:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{transform:translateY(-20px);opacity:0}to{transform:translateY(0);opacity:1}}.centered-container{display:flex;align-items:center;position:fixed;left:30px;top:50%;transform:translateY(-50%);width:100%;height:100%;z-index:1000;animation:fadeIn .3s ease-in-out}.card{max-width:500px;width:500px;min-height:500px;height:500px;max-height:500px;position:relative;background:#fff;border-radius:8px;animation:slideIn .5s ease-out}.card-header{font-size:1.25rem;font-weight:700;margin-bottom:1rem}.card-content{font-size:1rem;margin-bottom:1rem}.card-footer{display:flex;justify-content:space-between;margin-top:auto}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;height:100%}#root{height:100%}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace} 2 | -------------------------------------------------------------------------------- /web/build/assets/index-Do78jPx0.css: -------------------------------------------------------------------------------- 1 | body{-webkit-user-select:none;user-select:none;color:#ccc;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;overflow:hidden}.menu{position:fixed;display:flex;flex-direction:column;padding:10px;bottom:40px;right:-500px;transform:translateY(0);width:375px;min-height:500px;max-height:500px;border-radius:10px;background-color:#141414;transition:right .7s ease-in-out,opacity .7s ease-in-out,height .7s ease-in-out;opacity:0;pointer-events:none;scrollbar-width:none;-ms-overflow-style:none}.menu::-webkit-scrollbar{display:none}.menu .context-menu-content{display:flex;flex-direction:column;height:100%;overflow-y:auto}.menu .context-menu-card{margin-bottom:10px}._container_1kzia_2{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;z-index:9999}._notification_1kzia_12{max-width:300px;margin:10px;padding:12px;background-color:#141414cc;border-radius:9px;display:flex;width:300px;align-items:center;position:absolute;pointer-events:auto;transition:opacity .3s ease,transform .3s ease;z-index:10000}._notification_1kzia_12._top-left_1kzia_27{top:10px;left:10px}._notification_1kzia_12._top-right_1kzia_32{top:10px;right:10px}._notification_1kzia_12._bottom-left_1kzia_37{bottom:10px;left:10px}._notification_1kzia_12._bottom-right_1kzia_42{bottom:10px;right:10px}._slide-in_1kzia_47{animation:_slideIn_1kzia_1 .5s ease-out forwards}._slide-out_1kzia_51{animation:_slideOut_1kzia_1 .5s ease-in forwards}@keyframes _slideIn_1kzia_1{0%{transform:translateY(-20px);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes _slideOut_1kzia_1{0%{transform:translateY(0);opacity:1}to{transform:translateY(-20px);opacity:0}}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;height:100%}#root{height:100%}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace} 2 | -------------------------------------------------------------------------------- /web/build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Template lgf 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Template lgf 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "homepage": "web/build", 4 | "private": true, 5 | "type": "module", 6 | "version": "0.1.0", 7 | "scripts": { 8 | "start": "vite", 9 | "start:game": "vite build --watch", 10 | "build": "tsc && vite build", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@emotion/react": "^11.11.1", 15 | "@fortawesome/fontawesome-svg-core": "^6.5.2", 16 | "@fortawesome/free-solid-svg-icons": "^6.5.2", 17 | "@fortawesome/react-fontawesome": "^0.2.0", 18 | "@headlessui/react": "^2.1.2", 19 | "@mantine/core": "^6.0.15", 20 | "@mantine/dates": "^6.0.15", 21 | "@mantine/form": "^7.9.0", 22 | "@mantine/hooks": "^6.0.15", 23 | "@mantine/styles": "^6.0.21", 24 | "@tabler/icons-react": "^2.23.0", 25 | "bootstrap": "^5.3.3", 26 | "html2canvas": "^1.4.1", 27 | "leaflet": "^1.9.4", 28 | "react": "^18.2.0", 29 | "react-bootstrap": "^2.10.4", 30 | "react-dom": "^18.2.0", 31 | "react-draggable": "^4.4.6", 32 | "react-icons": "^5.2.0", 33 | "react-leaflet": "^4.2.1", 34 | "react-scroll-into-view": "^2.1.2", 35 | "react-select": "^5.8.0", 36 | "sass": "^1.72.0", 37 | "styled-components": "^6.1.8" 38 | }, 39 | "devDependencies": { 40 | "@types/leaflet": "^1.9.12", 41 | "@types/node": "^20.11.30", 42 | "@types/react": "^18.2.37", 43 | "@types/react-dom": "^18.2.15", 44 | "@types/react-leaflet": "^3.0.0", 45 | "@typescript-eslint/eslint-plugin": "^6.11.0", 46 | "@typescript-eslint/parser": "^6.11.0", 47 | "@vitejs/plugin-react": "^4.2.0", 48 | "eslint": "^8.54.0", 49 | "eslint-plugin-react-hooks": "^4.6.0", 50 | "eslint-plugin-react-refresh": "^0.4.4", 51 | "postcss": "^8.4.38", 52 | "postcss-preset-mantine": "^1.15.0", 53 | "postcss-simple-vars": "^7.0.1", 54 | "typescript": "^5.2.2", 55 | "vite": "^5.0.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /web/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { fetchNui } from "../utils/fetchNui"; 3 | import { useNuiEvent } from "../hooks/useNuiEvent"; 4 | import { isEnvBrowser } from "../utils/misc"; 5 | import ContextMenu from "./ContextMenu"; 6 | import NotificationComponent from "./Notification"; 7 | import TextUIComponent from "./TextUI"; 8 | import DialogComponent from "./Dialog"; 9 | import ProgressBar from "./ProgressBar"; 10 | import InputComponent from "./Input"; 11 | import Instructional from "./Instructional"; 12 | import "./ContextMenu.scss"; 13 | import "./TextUI.scss"; 14 | import "./Dialog.scss"; 15 | 16 | const App: React.FC = () => { 17 | const [contextVisible, setContextVisible] = useState(false); 18 | const [currentMenuID, setCurrentMenuID] = useState(null); 19 | const [buttonVisible, setButtonVisible] = useState(false); 20 | const [binderControls, setBinderControls] = useState({}); 21 | const [schema, setSchema] = useState({}); 22 | 23 | useNuiEvent<{ visible: boolean; menuID: string }>( 24 | "CreateMenuContext", 25 | ({ visible, menuID }) => { 26 | setContextVisible(visible); 27 | setCurrentMenuID(menuID); 28 | } 29 | ); 30 | 31 | useNuiEvent("openInstructionalButt", (data) => { 32 | setButtonVisible(data.visible); 33 | setBinderControls(data.controls || {}); 34 | setSchema(data.schema || {}); 35 | }); 36 | 37 | const handleCloseContextMenu = () => { 38 | if (!isEnvBrowser()) { 39 | fetchNui("UI:CloseContext", { 40 | name: "CreateMenuContext", 41 | menuID: currentMenuID, 42 | }) 43 | .then(() => {}) 44 | .catch((error) => { 45 | console.error("Failed to close context menu:", error); 46 | }); 47 | } 48 | }; 49 | 50 | useEffect(() => { 51 | const keyHandler = (e: KeyboardEvent) => { 52 | if (contextVisible && e.code === "Escape") { 53 | if (!isEnvBrowser()) { 54 | handleCloseContextMenu(); 55 | } 56 | } 57 | }; 58 | 59 | window.addEventListener("keydown", keyHandler); 60 | 61 | return () => { 62 | window.removeEventListener("keydown", keyHandler); 63 | }; 64 | }, [contextVisible]); 65 | 66 | return ( 67 | <> 68 | 69 | 70 | 71 | 72 | 73 | 74 | 79 | 80 | ); 81 | }; 82 | 83 | export default App; 84 | -------------------------------------------------------------------------------- /web/src/components/ContextMenu.scss: -------------------------------------------------------------------------------- 1 | $color1: #1A1B1E; 2 | $color2: #2e3036; 3 | $Border: 10px; 4 | 5 | body { 6 | user-select: none; 7 | color: #ccc; 8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 9 | overflow: hidden; 10 | } 11 | 12 | 13 | 14 | .menu { 15 | position: fixed; 16 | display: flex; 17 | flex-direction: column; 18 | padding: 10px; 19 | bottom: 40px; 20 | right: -500px; 21 | transform: translateY(-50%); 22 | width: 375px; 23 | min-height: 500px; 24 | max-height: 500px; 25 | border-radius: $Border; 26 | background-color: #141414; 27 | transition: right 0.7s ease-in-out, opacity 0.7s ease-in-out, height 0.7s ease-in-out; 28 | opacity: 0; 29 | pointer-events: none; 30 | scrollbar-width: none; 31 | -ms-overflow-style: none; 32 | 33 | &::-webkit-scrollbar { 34 | display: none; 35 | } 36 | 37 | 38 | .context-menu-content { 39 | display: flex; 40 | flex-direction: column; 41 | height: 100%; 42 | overflow-y: auto; 43 | } 44 | 45 | .context-menu-card { 46 | margin-bottom: 10px; 47 | } 48 | 49 | 50 | } 51 | 52 | .slide-in { 53 | right: 5%; 54 | opacity: 1; 55 | } 56 | 57 | .slide-out { 58 | right: -500px; 59 | opacity: 0; 60 | pointer-events: none; 61 | } 62 | 63 | -------------------------------------------------------------------------------- /web/src/components/Dialog.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;700&display=swap'); 2 | 3 | @keyframes fadeIn { 4 | from { 5 | opacity: 0; 6 | } 7 | to { 8 | opacity: 1; 9 | } 10 | } 11 | 12 | @keyframes slideIn { 13 | from { 14 | transform: translateY(-20px); 15 | opacity: 0; 16 | } 17 | to { 18 | transform: translateY(0); 19 | opacity: 1; 20 | } 21 | } 22 | 23 | .centered-container { 24 | display: flex; 25 | 26 | align-items: center; 27 | position: fixed; 28 | left: 30px; 29 | top: 50%; 30 | transform: translateY(-50%); 31 | width: 100%; 32 | height: 100%; 33 | z-index: 1000; 34 | animation: fadeIn 0.3s ease-in-out; 35 | } 36 | .card { 37 | max-width: 500px; 38 | width: 500px; 39 | min-height: 500px; 40 | height: 500px; 41 | max-height: 500px; 42 | position: relative; 43 | background: #fff; 44 | border-radius: 8px; 45 | animation: slideIn 0.5s ease-out; 46 | } 47 | 48 | .card-header { 49 | font-size: 1.25rem; 50 | font-weight: 700; 51 | margin-bottom: 1rem; 52 | } 53 | 54 | .card-content { 55 | font-size: 1rem; 56 | margin-bottom: 1rem; 57 | } 58 | 59 | .card-footer { 60 | display: flex; 61 | justify-content: space-between; 62 | margin-top: auto; 63 | } 64 | -------------------------------------------------------------------------------- /web/src/components/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect } from "react"; 2 | import { 3 | Button, 4 | Text, 5 | Card, 6 | Center, 7 | useMantineTheme, 8 | Loader, 9 | Image, 10 | } from "@mantine/core"; 11 | import { fetchNui } from "../utils/fetchNui"; 12 | import { MdNavigateNext } from "react-icons/md"; 13 | import { IoIosArrowBack, IoIosClose } from "react-icons/io"; 14 | 15 | 16 | interface CardData { 17 | title: string; 18 | message: string; 19 | actionLabel?: string; 20 | actionCloseLabel?: string; 21 | hasOnAction?: boolean; 22 | hasOnClose?: boolean; 23 | image?: string; 24 | } 25 | 26 | interface DialogData { 27 | id: string; 28 | title: string; 29 | cards: CardData[]; 30 | } 31 | 32 | const CenteredContainer: React.FC = () => { 33 | const theme = useMantineTheme(); 34 | const [dialog, setDialog] = useState(null); 35 | const [isVisible, setIsVisible] = useState(false); 36 | const [currentCardIndex, setCurrentCardIndex] = useState(0); 37 | 38 | const hideDialog = useCallback(() => { 39 | setIsVisible(false); 40 | setDialog(null); 41 | setCurrentCardIndex(0); 42 | }, []); 43 | 44 | const showDialog = useCallback((data: DialogData) => { 45 | setDialog(data); 46 | setCurrentCardIndex(0); 47 | setIsVisible(true); 48 | }, []); 49 | 50 | const handleAction = async () => { 51 | try { 52 | const cardIndex = currentCardIndex + 1; 53 | const card = dialog?.cards[currentCardIndex]; 54 | if (!card) { 55 | return; 56 | } 57 | if (card.hasOnAction) { 58 | await fetchNui("dialogAction", { id: dialog!.id, cardIndex }); 59 | } 60 | if (currentCardIndex < dialog!.cards.length - 1) { 61 | setCurrentCardIndex((prevIndex) => prevIndex + 1); 62 | } else { 63 | await fetchNui("dialogClose", { 64 | id: dialog!.id, 65 | cardIndex: currentCardIndex + 1, 66 | }); 67 | } 68 | } catch (error) { 69 | console.error("Error handling action:", error); 70 | } 71 | }; 72 | 73 | const handleClose = async () => { 74 | try { 75 | const cardIndex = currentCardIndex + 1; 76 | const card = dialog?.cards[currentCardIndex]; 77 | await fetchNui("dialogClose", { 78 | id: dialog!.id, 79 | cardIndex: cardIndex, 80 | }); 81 | hideDialog(); 82 | } catch (error) { 83 | console.error("Error sending close to NUI:", error); 84 | } 85 | }; 86 | 87 | useEffect(() => { 88 | const handleMessage = (event: MessageEvent) => { 89 | const data = event.data; 90 | if (data.action === "showDialog") { 91 | showDialog(data); 92 | } else if (data.action === "hideDialog") { 93 | hideDialog(); 94 | } 95 | }; 96 | 97 | 98 | 99 | 100 | 101 | window.addEventListener("message", handleMessage); 102 | return () => { 103 | window.removeEventListener("message", handleMessage); 104 | }; 105 | }, [showDialog, hideDialog]); 106 | 107 | 108 | 109 | if (!isVisible || !dialog) { 110 | return null; 111 | } 112 | 113 | const currentCard = dialog.cards[currentCardIndex]; 114 | 115 | if (!currentCard) { 116 | return null; 117 | } 118 | 119 | return ( 120 |
121 | 137 |
145 | 146 |
147 | 148 | {dialog.title} 149 | 150 |
151 | {currentCard.image && ( 152 | {currentCard.title} 159 | )} 160 | 161 | {currentCard.title} 162 | 163 | 164 | {currentCard.message} 165 | 166 |
167 |
174 | 185 | {currentCard.hasOnAction && ( 186 | 189 | )} 190 | {currentCardIndex === dialog.cards.length - 1 ? ( 191 | 199 | ) : ( 200 | 208 | )} 209 |
210 |
211 |
212 | ); 213 | }; 214 | 215 | export default CenteredContainer; 216 | -------------------------------------------------------------------------------- /web/src/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect } from "react"; 2 | import { 3 | TextInput, 4 | Select, 5 | NumberInput, 6 | PasswordInput, 7 | Textarea, 8 | Button, 9 | Grid, 10 | Col, 11 | Text, 12 | useMantineTheme, 13 | Group, 14 | Box, 15 | Badge, 16 | Title, 17 | Transition, 18 | Divider, 19 | } from "@mantine/core"; 20 | import { fetchNui } from "../utils/fetchNui"; 21 | import styled from "styled-components"; 22 | 23 | const ScrollBarData = styled.div` 24 | overflow-y: auto; 25 | overflow-x: hidden; 26 | flex-grow: 1; 27 | padding-bottom: 1rem; 28 | scrollbar-width: none; 29 | -ms-overflow-style: none; 30 | 31 | &::-webkit-scrollbar { 32 | display: none; 33 | } 34 | `; 35 | 36 | interface FieldOption { 37 | label: string; 38 | value: string; 39 | } 40 | 41 | interface Field { 42 | label: string; 43 | placeholder?: string; 44 | type?: "text" | "number" | "select" | "password" | "textarea"; 45 | options?: FieldOption[]; 46 | description?: string; 47 | min?: number; 48 | max?: number; 49 | required?: boolean; 50 | disabledInput?: boolean; 51 | } 52 | 53 | interface InputData { 54 | id: string; 55 | title: string; 56 | fields: Field[]; 57 | canClose?: boolean; 58 | titleButton?: string; 59 | } 60 | 61 | const InputComponent = () => { 62 | const [inputData, setInputData] = useState(null); 63 | const [formValues, setFormValues] = useState<{ [key: number]: any }>({}); 64 | const [modalOpen, setModalOpen] = useState(false); 65 | const [submitting, setSubmitting] = useState(false); 66 | const [closing, setClosing] = useState(false); 67 | const theme = useMantineTheme(); 68 | 69 | const handleMessage = useCallback((event: MessageEvent) => { 70 | const data = event.data; 71 | if (data.action === "showInputForm") { 72 | const { fields, canClose } = data.data; 73 | if (Array.isArray(fields) && typeof canClose === "boolean") { 74 | setInputData(data.data); 75 | setModalOpen(true); 76 | } else { 77 | console.error("Invalid data format:", data.data); 78 | } 79 | } else if (data.action === "closeInputForm") { 80 | setInputData(null); 81 | setModalOpen(false); 82 | } 83 | }, []); 84 | 85 | useEffect(() => { 86 | window.addEventListener("message", handleMessage); 87 | return () => { 88 | window.removeEventListener("message", handleMessage); 89 | }; 90 | }, [handleMessage]); 91 | 92 | const handleSubmit = async () => { 93 | const allFieldsFilled = inputData?.fields.every((field, index) => { 94 | if (field.required) { 95 | const value = formValues[index]; 96 | return value !== undefined && value !== null && value !== ""; 97 | } 98 | return true; 99 | }); 100 | 101 | if (!allFieldsFilled) { 102 | console.error("Please fill in all required fields."); 103 | return; 104 | } 105 | 106 | setSubmitting(true); 107 | setClosing(true); 108 | 109 | setTimeout(async () => { 110 | try { 111 | const formattedValues = Object.values(formValues); 112 | await fetchNui("input:Submit", { 113 | inputID: inputData?.id, 114 | fields: formattedValues, 115 | }); 116 | } catch (error) { 117 | console.error("Error submitting data:", error); 118 | } finally { 119 | setSubmitting(false); 120 | } 121 | }, 1500); 122 | }; 123 | 124 | const handleFieldChange = (index: number, value: any) => { 125 | setFormValues((prevValues) => ({ ...prevValues, [index]: value })); 126 | }; 127 | 128 | const handleClose = async () => { 129 | if (inputData?.canClose) { 130 | setClosing(true); 131 | try { 132 | await fetchNui("input:Close", { inputID: inputData.id }); 133 | } catch (error) { 134 | console.error("Error closing the modal:", error); 135 | } finally { 136 | setModalOpen(false); 137 | setClosing(false); 138 | setInputData(null); 139 | setFormValues({}); 140 | } 141 | } else { 142 | console.log("Modal cannot be closed without submitting."); 143 | } 144 | }; 145 | 146 | useEffect(() => { 147 | if (closing) { 148 | const timer = setTimeout(() => { 149 | setModalOpen(false); 150 | setClosing(false); 151 | setInputData(null); 152 | setFormValues({}); 153 | }, 2000); 154 | 155 | return () => clearTimeout(timer); 156 | } 157 | }, [closing]); 158 | 159 | const renderInputField = (field: Field, index: number) => { 160 | const inputStyles = { marginBottom: "0px" }; 161 | 162 | const isDisabled = field.disabledInput || false; 163 | 164 | switch (field.type) { 165 | case "text": 166 | return ( 167 | handleFieldChange(index, event.target.value)} 170 | style={inputStyles} 171 | required={field.required} 172 | size="xs" 173 | disabled={isDisabled} 174 | /> 175 | ); 176 | case "number": 177 | return ( 178 | handleFieldChange(index, value)} 183 | style={inputStyles} 184 | required={field.required} 185 | size="xs" 186 | disabled={isDisabled} 187 | /> 188 | ); 189 | case "select": 190 | return ( 191 |