├── LICENSE ├── README.md ├── client ├── alerts.lua ├── eventhandlers.lua ├── main.lua └── utils.lua ├── fxmanifest.lua ├── html ├── index.css ├── index.html └── index.js ├── locales ├── cs.json ├── de.json ├── en.json ├── es.json ├── fr.json ├── nl.json ├── pt-br.json └── tr.json ├── server └── main.lua ├── shared └── config.lua ├── sounds ├── dispatch.ogg ├── panicbutton.ogg └── robberysound.ogg └── ui ├── .gitignore ├── .prettierrc ├── README.md ├── app.js ├── index.html ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── src ├── App.svelte ├── Tailwind.css ├── components │ ├── Main.svelte │ └── Menu.svelte ├── main.ts ├── providers │ ├── AlwaysListener.svelte │ ├── BackdropFix.svelte │ ├── DebugBrowser.svelte │ └── VisibilityProvider.svelte ├── store │ └── stores.ts ├── typings │ └── type.ts ├── utils │ ├── ReceiveNUI.ts │ ├── SendNUI.ts │ ├── debugData.ts │ ├── misc.ts │ └── timeAgo.ts └── vite-env.d.ts ├── style.css ├── svelte.config.js ├── tailwind.config.cjs ├── tsconfig.json └── vite.config.js /README.md: -------------------------------------------------------------------------------- 1 | # PS Dispatch 2 | 3 | Integrated with [ps-mdt](https://github.com/Project-Sloth/ps-mdt) 4 | 5 | For all support questions, ask in our [Discord](https://www.discord.gg/projectsloth) support chat. 6 | Do not create issues on GitHub if you need help. Issues are for bug reporting and new features only. 7 | 8 | # Depedency 9 | 1. [qb-core](https://github.com/qbcore-framework/qb-core) 10 | 2. [ox_lib](https://github.com/overextended/ox_lib) 11 | 3. [ps-mdt](https://github.com/Project-Sloth/ps-mdt) - Optional but highly recommended. 12 | 13 | # Installation 14 | * Download ZIP 15 | * Make sure your [qb-core](https://github.com/qbcore-framework/qb-core) is fully updated to the latest version. 16 | * Drag and drop resource into your server files 17 | * Start resource through server.cfg 18 | * Drag and drop sounds folder into interact-sound\client\html\sounds 19 | * Configure your [language](https://github.com/Project-Sloth/ps-dispatch#change-language) 20 | * Restart your server. 21 | 22 | # Preview 23 | ## Short Notifications 24 | Dispatch notifications are sent containing only the alert name, omitting additional details to help over populated servers. For more information, the dispatch menu can be accessed. Can be configured on [Config.ShortCalls](https://github.com/Project-Sloth/ps-dispatch/blob/40ffc466ec7ffa14faaf40a68e8b3a9a92c72db6/shared/config.lua#L3C1-L3C18), false by default. 25 | 26 | 27 | 28 | ## Long Notifications 29 | 30 | 31 | 32 | 33 | 34 | ## Dispatch Menu 35 | 36 | 37 | # Change Language. 38 | 39 | - Place this `setr ox:locale en` inside your `server.cfg` 40 | - Change the `en` to your desired language! 41 | 42 | **Supported Languages:** 43 | | **Alias** | **Language Names** | 44 | |--------------|---------------| 45 | |en |English | 46 | |de |German | 47 | |nl |Dutch | 48 | |cs |Czech | 49 | |pt-br |Brazilian Portuguese | 50 | |es |Spanish | 51 | 52 | # Preset Alert Exports. 53 | 54 | ```lua 55 | - exports['ps-dispatch']:ArtGalleryRobbery() 56 | - exports['ps-dispatch']:CarBoosting(vehicle) 57 | - exports['ps-dispatch']:CarJacking(vehicle) 58 | - exports['ps-dispatch']:CustomAlert() 59 | - exports['ps-dispatch']:DeceasedPerson() 60 | - exports['ps-dispatch']:DrugBoatRobbery() 61 | - exports['ps-dispatch']:DrugSale() 62 | - exports['ps-dispatch']:EmsDown() 63 | - exports['ps-dispatch']:Explosion() 64 | - exports['ps-dispatch']:Fight() 65 | - exports['ps-dispatch']:FleecaBankRobbery(camId) 66 | - exports['ps-dispatch']:HouseRobbery() 67 | - exports['ps-dispatch']:HumaneRobbery() 68 | - exports['ps-dispatch']:Hunting() 69 | - exports['ps-dispatch']:InjuriedPerson() 70 | - exports['ps-dispatch']:OfficerDown() 71 | - exports['ps-dispatch']:OfficerBackup() 72 | - exports['ps-dispatch']:OfficerInDistress() 73 | - exports['ps-dispatch']:PacificBankRobbery(camId) 74 | - exports['ps-dispatch']:PaletoBankRobbery(camId) 75 | - exports['ps-dispatch']:PrisonBreak() 76 | - exports['ps-dispatch']:Shooting() 77 | - exports['ps-dispatch']:SignRobbery() 78 | - exports['ps-dispatch']:SpeedingVehicle(vehicle) 79 | - exports['ps-dispatch']:StoreRobbery(camId) 80 | - exports['ps-dispatch']:SuspiciousActivity() 81 | - exports['ps-dispatch']:TrainRobbery() 82 | - exports['ps-dispatch']:UndergroundRobbery() 83 | - exports['ps-dispatch']:UnionRobbery() 84 | - exports['ps-dispatch']:VangelicoRobbery(camId) 85 | - exports['ps-dispatch']:VanRobbery() 86 | - exports['ps-dispatch']:VehicleShooting(vehicle) 87 | - exports['ps-dispatch']:VehicleTheft(vehicle) 88 | - exports['ps-dispatch']:YachtHeist() 89 | - exports['ps-dispatch']:BobcatSecurityHeist() 90 | ``` 91 | # Steps to Create New Alert 92 | Add the following into your `alerts.lua` and change to your liking: 93 | ``` 94 | local function TestAlert() 95 | local coords = GetEntityCoords(cache.ped) 96 | local vehicle = GetVehicleData(cache.vehicle) 97 | 98 | local dispatchData = { 99 | message = locale('testalert'), -- add this into your locale 100 | codeName = 'testalert', -- this should be the same as in config.lua 101 | code = '10-35', 102 | icon = 'fas fa-car-burst', 103 | priority = 2, 104 | coords = coords, 105 | street = GetStreetAndZone(coords), 106 | heading = GetPlayerHeading(), 107 | vehicle = vehicle.name, 108 | plate = vehicle.plate, 109 | color = vehicle.color, 110 | class = vehicle.class, 111 | doors = vehicle.doors, 112 | jobs = { 'leo' } 113 | } 114 | 115 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 116 | end 117 | exports('TestAlert', TestAlert) 118 | ``` 119 | Add codeName in `config.lua` for the particular robbery to display the blip 120 | ["testalert"] is the codename you passed with the TriggerServerEvent in step 1 121 | ``` 122 | ['testalert'] = { -- Need to match the codeName in alerts.lua 123 | radius = 0, 124 | sprite = 119, 125 | color = 1, 126 | scale = 1.5, 127 | length = 2, 128 | sound = 'Lose_1st', 129 | sound2 = 'GTAO_FM_Events_Soundset', 130 | offset = false, 131 | flash = false 132 | }, 133 | ``` 134 | Information about each parameter is in the `alerts.lua` file. 135 | 136 | # FAQ 137 | * There are no calls showing on dispatch or mdt list. 138 | - Make sure you have a job type specified in your qbcore/shared/jobs.lua like: 139 | 140 | ![image](https://github.com/Project-Sloth/ps-dispatch/assets/9503151/7834e878-5020-4fcc-8864-03d44120c160) 141 | 142 | - Make sure that you're using the correct job type as leo and make sure your [qb-core](https://github.com/qbcore-framework/qb-core) is fully updated to the latest version. 143 | - On shared/config.lua make set Config.Debug = true to test calls as police officer.(ONLY to be used as testing, make sure to disable on live production) 144 | 145 | * How to change colors of the calls? 146 | - Priority 1 is red and priority 2 is normal on the config. 147 | 148 | * To increase the time that calls are shown on the screen, do the following: 149 | - Find the "alerts.lua" file in the client folder. 150 | - Open this file with a text editor or a development tool like Visual Studio Code. 151 | - Look for the code "alertTime = nil". 152 | - Replace "nil" with the number of seconds you want the calls to display. For example, setting "alertTime = 25" means calls will be shown for 25 seconds. 153 | 154 | # Credits 155 | * [OK1ez](https://github.com/OK1ez) 156 | * [Candrex](https://github.com/CandrexDev) 157 | * [Lenzh](https://github.com/Lenzh) 158 | * [LeSiiN](https://github.com/LeSiiN) 159 | * Project Sloth Team 160 | -------------------------------------------------------------------------------- /client/alerts.lua: -------------------------------------------------------------------------------- 1 | local function CustomAlert(data) 2 | local coords = data.coords or vec3(0.0, 0.0, 0.0) 3 | local gender = GetPlayerGender() 4 | if not data.gender then gender = nil end 5 | 6 | 7 | local dispatchData = { 8 | message = data.message or "", -- Title of the alert 9 | codeName = data.dispatchCode or "NONE", -- Unique name for each alert 10 | code = data.code or '10-80', -- Code that is displayed before the title 11 | icon = data.icon or 'fas fa-question', -- Icon that is displaed after the title 12 | priority = data.priority or 2, -- Changes color of the alert ( 1 = red, 2 = default ) 13 | coords = coords, -- Coords of the player 14 | gender = gender, -- Gender of the player 15 | street = GetStreetAndZone(coords), -- Street of the player 16 | camId = data.camId or nil, -- Cam ID ( for heists ) 17 | color = data.firstColor or nil, -- Color of the vehicle 18 | callsign = data.callsign or nil, -- Callsigns 19 | name = data.name or nil, -- Name of either officer/ems or a player 20 | vehicle = data.model or nil, -- Vehicle name 21 | plate = data.plate or nil, -- Vehicle plate 22 | alertTime = data.alertTime or nil, -- How long it stays on the screen in seconds 23 | doorCount = data.doorCount or nil, -- How many doors on vehicle 24 | automaticGunfire = data.automaticGunfire or false, -- Automatic Gun or not 25 | alert = { 26 | radius = data.radius or 0, -- Radius around the blip 27 | sprite = data.sprite or 1, -- Sprite of the blip 28 | color = data.color or 1, -- Color of the blip 29 | scale = data.scale or 0.5, -- Scale of the blip 30 | length = data.length or 2, -- How long it stays on the map 31 | sound = data.sound or "Lose_1st", -- Alert sound 32 | sound2 = data.sound2 or "GTAO_FM_Events_Soundset", -- Alert sound 33 | offset = data.offset or false, -- Blip / radius offset 34 | flash = data.flash or false -- Blip flash 35 | }, 36 | jobs = data.jobs or { 'leo' }, 37 | } 38 | 39 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 40 | end 41 | exports('CustomAlert', CustomAlert) 42 | 43 | local function VehicleTheft() 44 | local coords = GetEntityCoords(cache.ped) 45 | local vehicle = GetVehicleData(cache.vehicle) 46 | 47 | local dispatchData = { 48 | message = locale('vehicletheft'), 49 | codeName = 'vehicletheft', 50 | code = '10-35', 51 | icon = 'fas fa-car-burst', 52 | priority = 2, 53 | coords = coords, 54 | street = GetStreetAndZone(coords), 55 | heading = GetPlayerHeading(), 56 | vehicle = vehicle.name, 57 | plate = vehicle.plate, 58 | color = vehicle.color, 59 | class = vehicle.class, 60 | doors = vehicle.doors, 61 | alertTime = nil, 62 | jobs = { 'leo' } 63 | } 64 | 65 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 66 | end 67 | exports('VehicleTheft', VehicleTheft) 68 | 69 | local function Shooting() 70 | local coords = GetEntityCoords(cache.ped) 71 | 72 | local dispatchData = { 73 | message = locale('shooting'), 74 | codeName = 'shooting', 75 | code = '10-11', 76 | icon = 'fas fa-gun', 77 | priority = 2, 78 | coords = coords, 79 | street = GetStreetAndZone(coords), 80 | gender = GetPlayerGender(), 81 | weapon = GetWeaponName(), 82 | alertTime = nil, 83 | jobs = { 'leo' } 84 | } 85 | 86 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 87 | end 88 | exports('Shooting', Shooting) 89 | 90 | local function Hunting() 91 | local coords = GetEntityCoords(cache.ped) 92 | 93 | local dispatchData = { 94 | message = locale('hunting'), 95 | codeName = 'hunting', 96 | code = '10-13', 97 | icon = 'fas fa-gun', 98 | priority = 2, 99 | weapon = GetWeaponName(), 100 | coords = coords, 101 | gender = GetPlayerGender(), 102 | street = GetStreetAndZone(coords), 103 | alertTime = nil, 104 | jobs = { 'leo' } 105 | } 106 | 107 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 108 | end 109 | exports('Hunting', Hunting) 110 | 111 | local function VehicleShooting() 112 | local coords = GetEntityCoords(cache.ped) 113 | local vehicle = GetVehicleData(cache.vehicle) 114 | 115 | local dispatchData = { 116 | message = locale('vehicleshots'), 117 | codeName = 'vehicleshots', 118 | code = '10-60', 119 | icon = 'fas fa-gun', 120 | priority = 2, 121 | coords = coords, 122 | weapon = GetWeaponName(), 123 | street = GetStreetAndZone(coords), 124 | heading = GetPlayerHeading(), 125 | vehicle = vehicle.name, 126 | plate = vehicle.plate, 127 | color = vehicle.color, 128 | class = vehicle.class, 129 | doors = vehicle.doors, 130 | alertTime = nil, 131 | jobs = { 'leo' } 132 | } 133 | 134 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 135 | end 136 | exports('VehicleShooting', VehicleShooting) 137 | 138 | local function SpeedingVehicle() 139 | local coords = GetEntityCoords(cache.ped) 140 | local vehicle = GetVehicleData(cache.vehicle) 141 | 142 | local dispatchData = { 143 | message = locale('speeding'), 144 | codeName = 'speeding', 145 | code = '10-11', 146 | icon = 'fas fa-car', 147 | priority = 2, 148 | coords = coords, 149 | street = GetStreetAndZone(coords), 150 | heading = GetPlayerHeading(), 151 | vehicle = vehicle.name, 152 | plate = vehicle.plate, 153 | color = vehicle.color, 154 | class = vehicle.class, 155 | doors = vehicle.doors, 156 | alertTime = nil, 157 | jobs = { 'leo' } 158 | } 159 | 160 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 161 | end 162 | exports('SpeedingVehicle', SpeedingVehicle) 163 | 164 | local function Fight() 165 | local coords = GetEntityCoords(cache.ped) 166 | 167 | local dispatchData = { 168 | message = locale('melee'), 169 | codeName = 'fight', 170 | code = '10-10', 171 | icon = 'fas fa-hand-fist', 172 | priority = 2, 173 | coords = coords, 174 | gender = GetPlayerGender(), 175 | street = GetStreetAndZone(coords), 176 | alertTime = nil, 177 | jobs = { 'leo' } 178 | } 179 | 180 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 181 | end 182 | exports('Fight', Fight) 183 | 184 | local function PrisonBreak() 185 | local coords = GetEntityCoords(cache.ped) 186 | 187 | local dispatchData = { 188 | message = locale('prisonbreak'), 189 | codeName = 'prisonbreak', 190 | code = '10-90', 191 | icon = 'fas fa-vault', 192 | priority = 2, 193 | coords = coords, 194 | gender = GetPlayerGender(), 195 | street = GetStreetAndZone(coords), 196 | alertTime = nil, 197 | jobs = { 'leo' } 198 | } 199 | 200 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 201 | end 202 | exports('PrisonBreak', PrisonBreak) 203 | 204 | local function StoreRobbery(camId) 205 | local coords = GetEntityCoords(cache.ped) 206 | 207 | local dispatchData = { 208 | message = locale('storerobbery'), 209 | codeName = 'storerobbery', 210 | code = '10-90', 211 | icon = 'fas fa-store', 212 | priority = 2, 213 | coords = coords, 214 | gender = GetPlayerGender(), 215 | street = GetStreetAndZone(coords), 216 | camId = camId, 217 | alertTime = nil, 218 | jobs = { 'leo' } 219 | } 220 | 221 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 222 | end 223 | exports('StoreRobbery', StoreRobbery) 224 | 225 | local function FleecaBankRobbery(camId) 226 | local coords = GetEntityCoords(cache.ped) 227 | 228 | local dispatchData = { 229 | message = locale('fleecabank'), 230 | codeName = 'bankrobbery', 231 | code = '10-90', 232 | icon = 'fas fa-vault', 233 | priority = 2, 234 | coords = coords, 235 | gender = GetPlayerGender(), 236 | street = GetStreetAndZone(coords), 237 | camId = camId, 238 | alertTime = nil, 239 | jobs = { 'leo' } 240 | } 241 | 242 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 243 | end 244 | exports('FleecaBankRobbery', FleecaBankRobbery) 245 | 246 | local function PaletoBankRobbery(camId) 247 | local coords = GetEntityCoords(cache.ped) 248 | 249 | local dispatchData = { 250 | message = locale('paletobank'), 251 | codeName = 'paletobankrobbery', 252 | code = '10-90', 253 | icon = 'fas fa-vault', 254 | priority = 2, 255 | coords = coords, 256 | gender = GetPlayerGender(), 257 | street = GetStreetAndZone(coords), 258 | camId = camId, 259 | alertTime = nil, 260 | jobs = { 'leo' } 261 | } 262 | 263 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 264 | end 265 | exports('PaletoBankRobbery', PaletoBankRobbery) 266 | 267 | local function PacificBankRobbery(camId) 268 | local coords = GetEntityCoords(cache.ped) 269 | 270 | local dispatchData = { 271 | message = locale('pacificbank'), 272 | codeName = 'pacificbankrobbery', 273 | code = '10-90', 274 | icon = 'fas fa-vault', 275 | priority = 2, 276 | coords = coords, 277 | gender = GetPlayerGender(), 278 | street = GetStreetAndZone(coords), 279 | camId = camId, 280 | alertTime = nil, 281 | jobs = { 'leo' } 282 | } 283 | 284 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 285 | end 286 | exports('PacificBankRobbery', PacificBankRobbery) 287 | 288 | local function VangelicoRobbery(camId) 289 | local coords = GetEntityCoords(cache.ped) 290 | 291 | local dispatchData = { 292 | message = locale('vangelico'), 293 | codeName = 'vangelicorobbery', 294 | code = '10-90', 295 | icon = 'fas fa-gem', 296 | priority = 2, 297 | coords = coords, 298 | gender = GetPlayerGender(), 299 | street = GetStreetAndZone(coords), 300 | camId = camId, 301 | alertTime = nil, 302 | jobs = { 'leo' } 303 | } 304 | 305 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 306 | end 307 | exports('VangelicoRobbery', VangelicoRobbery) 308 | 309 | local function HouseRobbery() 310 | local coords = GetEntityCoords(cache.ped) 311 | 312 | local dispatchData = { 313 | message = locale('houserobbery'), 314 | codeName = 'houserobbery', 315 | code = '10-90', 316 | icon = 'fas fa-house', 317 | priority = 2, 318 | coords = coords, 319 | gender = GetPlayerGender(), 320 | street = GetStreetAndZone(coords), 321 | alertTime = nil, 322 | jobs = { 'leo' } 323 | } 324 | 325 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 326 | end 327 | exports('HouseRobbery', HouseRobbery) 328 | 329 | local function YachtHeist() 330 | local coords = GetEntityCoords(cache.ped) 331 | 332 | local dispatchData = { 333 | message = locale('yachtheist'), 334 | codeName = 'yachtheist', 335 | code = '10-65', 336 | icon = 'fas fa-house', 337 | priority = 2, 338 | coords = coords, 339 | gender = GetPlayerGender(), 340 | street = GetStreetAndZone(coords), 341 | alertTime = nil, 342 | jobs = { 'leo' } 343 | } 344 | 345 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 346 | end 347 | exports('YachtHeist', YachtHeist) 348 | 349 | local function DrugSale() 350 | local coords = GetEntityCoords(cache.ped) 351 | 352 | local dispatchData = { 353 | message = locale('drugsell'), 354 | codeName = 'suspicioushandoff', 355 | code = '10-13', 356 | icon = 'fas fa-tablets', 357 | priority = 2, 358 | coords = coords, 359 | gender = GetPlayerGender(), 360 | street = GetStreetAndZone(coords), 361 | alertTime = nil, 362 | jobs = { 'leo' } 363 | } 364 | 365 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 366 | end 367 | exports('DrugSale', DrugSale) 368 | 369 | local function SuspiciousActivity() 370 | local coords = GetEntityCoords(cache.ped) 371 | 372 | local dispatchData = { 373 | message = locale('susactivity'), 374 | codeName = 'susactivity', 375 | code = '10-66', 376 | icon = 'fas fa-tablets', 377 | priority = 2, 378 | coords = coords, 379 | gender = GetPlayerGender(), 380 | street = GetStreetAndZone(coords), 381 | alertTime = nil, 382 | jobs = { 'leo' } 383 | } 384 | 385 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 386 | end 387 | exports('SuspiciousActivity', SuspiciousActivity) 388 | 389 | local function CarJacking(vehicle) 390 | local coords = GetEntityCoords(cache.ped) 391 | local vehicle = GetVehicleData(vehicle) 392 | 393 | local dispatchData = { 394 | message = locale('carjacking'), 395 | codeName = 'carjack', 396 | code = '10-35', 397 | icon = 'fas fa-car', 398 | priority = 2, 399 | coords = coords, 400 | street = GetStreetAndZone(coords), 401 | heading = GetPlayerHeading(), 402 | vehicle = vehicle.name, 403 | plate = vehicle.plate, 404 | color = vehicle.color, 405 | class = vehicle.class, 406 | doors = vehicle.doors, 407 | alertTime = nil, 408 | jobs = { 'leo' } 409 | } 410 | 411 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 412 | end 413 | exports('CarJacking', CarJacking) 414 | 415 | local function InjuriedPerson() 416 | local coords = GetEntityCoords(cache.ped) 417 | 418 | local dispatchData = { 419 | message = locale('persondown'), 420 | codeName = 'civdown', 421 | code = '10-69', 422 | icon = 'fas fa-face-dizzy', 423 | priority = 1, 424 | coords = coords, 425 | gender = GetPlayerGender(), 426 | street = GetStreetAndZone(coords), 427 | alertTime = 10, 428 | jobs = { 'ems' } 429 | } 430 | 431 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 432 | end 433 | exports('InjuriedPerson', InjuriedPerson) 434 | 435 | local function DeceasedPerson() 436 | local coords = GetEntityCoords(cache.ped) 437 | 438 | local dispatchData = { 439 | message = locale('civbled'), 440 | codeName = 'civdead', 441 | code = '10-69', 442 | icon = 'fas fa-skull', 443 | priority = 1, 444 | coords = coords, 445 | gender = GetPlayerGender(), 446 | street = GetStreetAndZone(coords), 447 | alertTime = 10, 448 | jobs = { 'ems' } 449 | } 450 | 451 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 452 | end 453 | exports('DeceasedPerson', DeceasedPerson) 454 | 455 | local function OfficerDown() 456 | local coords = GetEntityCoords(cache.ped) 457 | 458 | local dispatchData = { 459 | message = locale('officerdown'), 460 | codeName = 'officerdown', 461 | code = '10-99', 462 | icon = 'fas fa-skull', 463 | priority = 1, 464 | coords = coords, 465 | gender = GetPlayerGender(), 466 | street = GetStreetAndZone(coords), 467 | name = PlayerData.charinfo.firstname .. " " .. PlayerData.charinfo.lastname, 468 | callsign = PlayerData.metadata["callsign"], 469 | alertTime = 10, 470 | jobs = { 'ems', 'leo' } 471 | } 472 | 473 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 474 | end 475 | exports('OfficerDown', OfficerDown) 476 | 477 | RegisterNetEvent("ps-dispatch:client:officerdown", function() OfficerDown() end) 478 | 479 | local function OfficerBackup() 480 | local coords = GetEntityCoords(cache.ped) 481 | 482 | local dispatchData = { 483 | message = locale('officerbackup'), 484 | codeName = 'officerbackup', 485 | code = '10-32', 486 | icon = 'fas fa-skull', 487 | priority = 1, 488 | coords = coords, 489 | gender = GetPlayerGender(), 490 | street = GetStreetAndZone(coords), 491 | name = PlayerData.charinfo.firstname .. " " .. PlayerData.charinfo.lastname, 492 | callsign = PlayerData.metadata["callsign"], 493 | alertTime = 10, 494 | jobs = { 'ems', 'leo' } 495 | } 496 | 497 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 498 | end 499 | exports('OfficerBackup', OfficerBackup) 500 | 501 | RegisterNetEvent("ps-dispatch:client:officerbackup", function() OfficerBackup() end) 502 | 503 | local function OfficerInDistress() 504 | local coords = GetEntityCoords(cache.ped) 505 | 506 | local dispatchData = { 507 | message = locale('officerdistress'), 508 | codeName = 'officerdistress', 509 | code = '10-99', 510 | icon = 'fas fa-skull', 511 | priority = 1, 512 | coords = coords, 513 | gender = GetPlayerGender(), 514 | street = GetStreetAndZone(coords), 515 | name = PlayerData.charinfo.firstname .. " " .. PlayerData.charinfo.lastname, 516 | callsign = PlayerData.metadata["callsign"], 517 | alertTime = 10, 518 | jobs = { 'ems', 'leo' } 519 | } 520 | 521 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 522 | end 523 | exports('OfficerInDistress', OfficerInDistress) 524 | 525 | local function EmsDown() 526 | local coords = GetEntityCoords(cache.ped) 527 | 528 | local dispatchData = { 529 | message = locale('emsdown'), 530 | codeName = 'emsdown', 531 | code = '10-99', 532 | icon = 'fas fa-skull', 533 | priority = 1, 534 | coords = coords, 535 | gender = GetPlayerGender(), 536 | street = GetStreetAndZone(coords), 537 | name = PlayerData.charinfo.firstname .. " " .. PlayerData.charinfo.lastname, 538 | callsign = PlayerData.metadata["callsign"], 539 | alertTime = 10, 540 | jobs = { 'ems', 'leo' } 541 | } 542 | 543 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 544 | end 545 | exports('EmsDown', EmsDown) 546 | 547 | RegisterNetEvent("ps-dispatch:client:emsdown", function() EmsDown() end) 548 | 549 | local function Explosion() 550 | local coords = GetEntityCoords(cache.ped) 551 | 552 | local dispatchData = { 553 | message = locale('explosion'), 554 | codeName = 'explosion', 555 | code = '10-80', 556 | icon = 'fas fa-fire', 557 | priority = 2, 558 | coords = coords, 559 | gender = GetPlayerGender(), 560 | street = GetStreetAndZone(coords), 561 | alertTime = nil, 562 | jobs = { 'leo' } 563 | } 564 | 565 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 566 | end 567 | exports('Explosion', Explosion) 568 | 569 | local function PhoneCall(message, anonymous, job, type) 570 | local coords = GetEntityCoords(cache.ped) 571 | 572 | if IsCallAllowed(message) then 573 | PhoneAnimation() 574 | 575 | local dispatchData = { 576 | message = anonymous and locale('anon_call') or locale('call'), 577 | codeName = type == '311' and '311call' or '911call', 578 | code = type, 579 | icon = 'fas fa-phone', 580 | priority = 2, 581 | coords = coords, 582 | name = anonymous and locale('anon') or (PlayerData.charinfo.firstname .. " " .. PlayerData.charinfo.lastname), 583 | number = anonymous and locale('hidden_number') or PlayerData.charinfo.phone, 584 | information = message, 585 | street = GetStreetAndZone(coords), 586 | alertTime = nil, 587 | jobs = job 588 | } 589 | 590 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 591 | end 592 | end 593 | 594 | --- @param data string -- Message 595 | --- @param type string -- What type of emergency 596 | --- @param anonymous boolean -- Is the call anonymous 597 | local pslastaction = 0 598 | RegisterNetEvent('ps-dispatch:client:sendEmergencyMsg', function(data, type, anonymous) 599 | local year, month , day , hour, minute, second = GetUtcTime() 600 | local idtrack = tonumber(hour..minute..second) 601 | local spamdetek = idtrack - pslastaction 602 | if spamdetek < 0 then spamdetek = Config.AlertCommandCooldown end 603 | if spamdetek <= Config.AlertCommandCooldown and pslastaction > 0 then 604 | pslastaction = idtrack 605 | QBCore.Functions.Notify("Command on cooldown", "error") 606 | else 607 | pslastaction = idtrack 608 | local jobs = { ['911'] = { 'leo' }, ['311'] = { 'ems' } } 609 | PhoneCall(data, anonymous, jobs[type], type) 610 | end 611 | end) 612 | 613 | 614 | local function ArtGalleryRobbery() 615 | local coords = GetEntityCoords(cache.ped) 616 | 617 | local dispatchData = { 618 | message = locale('artgalleryrobbery'), 619 | codeName = 'artgalleryrobbery', 620 | code = '10-90', 621 | icon = 'fas fa-brush', 622 | priority = 2, 623 | coords = coords, 624 | gender = GetPlayerGender(), 625 | street = GetStreetAndZone(coords), 626 | alertTime = nil, 627 | jobs = { 'leo' } 628 | } 629 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 630 | end 631 | exports('ArtGalleryRobbery', ArtGalleryRobbery) 632 | 633 | local function HumaneRobbery() 634 | local coords = GetEntityCoords(cache.ped) 635 | 636 | local dispatchData = { 637 | message = locale('humanelabsrobbery'), 638 | codeName = 'humanelabsrobbery', 639 | code = '10-90', 640 | icon = 'fas fa-flask-vial', 641 | priority = 2, 642 | coords = coords, 643 | gender = GetPlayerGender(), 644 | street = GetStreetAndZone(coords), 645 | alertTime = nil, 646 | jobs = { 'leo' } 647 | } 648 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 649 | 650 | end 651 | exports('HumaneRobbery', HumaneRobbery) 652 | 653 | local function TrainRobbery() 654 | local coords = GetEntityCoords(cache.ped) 655 | 656 | local dispatchData = { 657 | message = locale('trainrobbery'), 658 | codeName = 'trainrobbery', 659 | code = '10-90', 660 | icon = 'fas fa-train', 661 | priority = 2, 662 | coords = coords, 663 | gender = GetPlayerGender(), 664 | street = GetStreetAndZone(coords), 665 | alertTime = nil, 666 | jobs = { 'leo' } 667 | } 668 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 669 | 670 | end 671 | exports('TrainRobbery', TrainRobbery) 672 | 673 | local function VanRobbery() 674 | local coords = GetEntityCoords(cache.ped) 675 | 676 | local dispatchData = { 677 | message = locale('vanrobbery'), 678 | codeName = 'vanrobbery', 679 | code = '10-90', 680 | icon = 'fas fa-van-shuttle', 681 | priority = 2, 682 | coords = coords, 683 | gender = GetPlayerGender(), 684 | street = GetStreetAndZone(coords), 685 | alertTime = nil, 686 | jobs = { 'leo' } 687 | } 688 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 689 | 690 | end 691 | exports('VanRobbery', VanRobbery) 692 | 693 | local function UndergroundRobbery() 694 | local coords = GetEntityCoords(cache.ped) 695 | 696 | local dispatchData = { 697 | message = locale('undergroundrobbery'), 698 | codeName = 'undergroundrobbery', 699 | code = '10-90', 700 | icon = 'fas fa-person-rays', 701 | priority = 2, 702 | coords = coords, 703 | gender = GetPlayerGender(), 704 | street = GetStreetAndZone(coords), 705 | alertTime = nil, 706 | jobs = { 'leo' } 707 | } 708 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 709 | end 710 | exports('UndergroundRobbery', UndergroundRobbery) 711 | 712 | local function DrugBoatRobbery() 713 | local coords = GetEntityCoords(cache.ped) 714 | 715 | local dispatchData = { 716 | message = locale('drugboatrobbery'), 717 | codeName = 'drugboatrobbery', 718 | code = '10-65', 719 | icon = 'fas fa-ship', 720 | priority = 2, 721 | coords = coords, 722 | gender = GetPlayerGender(), 723 | street = GetStreetAndZone(coords), 724 | alertTime = nil, 725 | jobs = { 'leo' } 726 | } 727 | 728 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 729 | end 730 | exports('DrugBoatRobbery', DrugBoatRobbery) 731 | 732 | local function UnionRobbery() 733 | local coords = GetEntityCoords(cache.ped) 734 | 735 | local dispatchData = { 736 | message = locale('unionrobbery'), 737 | codeName = 'unionrobbery', 738 | code = '10-90', 739 | icon = 'fas fa-truck-field', 740 | priority = 2, 741 | coords = coords, 742 | gender = GetPlayerGender(), 743 | street = GetStreetAndZone(coords), 744 | alertTime = nil, 745 | jobs = { 'leo' } 746 | } 747 | 748 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 749 | end 750 | exports('UnionRobbery', UnionRobbery) 751 | 752 | local function CarBoosting(vehicle) 753 | local coords = GetEntityCoords(cache.ped) 754 | local vehicle = GetVehicleData(vehicle or cache.vehicle) 755 | 756 | local dispatchData = { 757 | message = locale('carboosting'), 758 | codeName = 'carboosting', 759 | code = '10-50', 760 | icon = 'fas fa-car', 761 | priority = 2, 762 | coords = coords, 763 | street = GetStreetAndZone(coords), 764 | heading = GetPlayerHeading(), 765 | vehicle = vehicle.name, 766 | plate = vehicle.plate, 767 | color = vehicle.color, 768 | class = vehicle.class, 769 | doors = vehicle.doors, 770 | alertTime = nil, 771 | jobs = { 'leo' } 772 | } 773 | 774 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 775 | end 776 | exports('CarBoosting', CarBoosting) 777 | 778 | local function SignRobbery() 779 | local coords = GetEntityCoords(cache.ped) 780 | 781 | local dispatchData = { 782 | message = locale('signrobbery'), 783 | codeName = 'signrobbery', 784 | code = '10-10', 785 | icon = 'fab fa-artstation', 786 | priority = 2, 787 | coords = coords, 788 | gender = GetPlayerGender(), 789 | street = GetStreetAndZone(coords), 790 | alertTime = nil, 791 | jobs = { 'leo'} 792 | } 793 | 794 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 795 | end 796 | exports('SignRobbery', SignRobbery) 797 | 798 | local function BobcatSecurityHeist() 799 | local coords = GetEntityCoords(cache.ped) 800 | 801 | local dispatchData = { 802 | message = locale('bobcatsecurity'), 803 | codeName = 'bobcatsecurityheist', 804 | code = '10-90', 805 | icon = 'fa-solid fa-building-shield', 806 | priority = 2, 807 | coords = coords, 808 | gender = GetPlayerGender(), 809 | street = GetStreetAndZone(coords), 810 | alertTime = nil, 811 | jobs = { 'leo'} 812 | } 813 | 814 | TriggerServerEvent('ps-dispatch:server:notify', dispatchData) 815 | end 816 | exports('BobcatSecurityHeist', BobcatSecurityHeist) -------------------------------------------------------------------------------- /client/eventhandlers.lua: -------------------------------------------------------------------------------- 1 | local timer = {} 2 | 3 | ---@param name string -- The name of the timer 4 | ---@param action function -- The function to execute when the timer is up 5 | ---@vararg any -- Arguments to pass to the action function 6 | local function WaitTimer(name, action, ...) 7 | if not Config.DefaultAlerts[name] then return end 8 | 9 | if not timer[name] then 10 | timer[name] = true 11 | action(...) 12 | Wait(Config.DefaultAlertsDelay * 1000) 13 | timer[name] = false 14 | end 15 | end 16 | 17 | ---@param witnesses table | Array of peds that witnessed the event 18 | ---@param ped number | Ped ID to check 19 | ---@return boolean | Returns true if the ped is in the witnesses table 20 | local function isPedAWitness(witnesses, ped) 21 | for k, v in pairs(witnesses) do 22 | if v == ped then 23 | return true 24 | end 25 | end 26 | return false 27 | end 28 | 29 | ---@param ped number | Ped ID to check 30 | ---@return boolean | Returns true if the ped is holding a whitelisted gun 31 | local function BlacklistedWeapon(ped) 32 | for i = 1, #Config.WeaponWhitelist do 33 | local weaponHash = joaat(Config.WeaponWhitelist[i]) 34 | if GetSelectedPedWeapon(ped) == weaponHash then 35 | return true -- Is a whitelisted weapon 36 | end 37 | end 38 | return false -- Is not a whitelisted weapon 39 | end 40 | 41 | AddEventHandler('CEventGunShot', function(witnesses, ped) 42 | if IsPedCurrentWeaponSilenced(cache.ped) then return end 43 | if inNoDispatchZone then return end 44 | if BlacklistedWeapon(cache.ped) then return end 45 | 46 | WaitTimer('Shooting', function() 47 | if cache.ped ~= ped then return end 48 | 49 | if PlayerData.job.type == 'leo' then 50 | if not Config.Debug then 51 | return 52 | end 53 | end 54 | 55 | if inHuntingZone then 56 | exports['ps-dispatch']:Hunting() 57 | return 58 | end 59 | 60 | if witnesses and not isPedAWitness(witnesses, ped) then return end 61 | 62 | if cache.vehicle then 63 | exports['ps-dispatch']:VehicleShooting() 64 | else 65 | exports['ps-dispatch']:Shooting() 66 | end 67 | end) 68 | end) 69 | 70 | AddEventHandler('CEventShockingSeenMeleeAction', function(witnesses, ped) 71 | WaitTimer('Melee', function() 72 | if cache.ped ~= ped then return end 73 | if witnesses and not isPedAWitness(witnesses, ped) then return end 74 | if not IsPedInMeleeCombat(ped) then return end 75 | 76 | exports['ps-dispatch']:Fight() 77 | end) 78 | end) 79 | 80 | AddEventHandler('CEventPedJackingMyVehicle', function(_, ped) 81 | WaitTimer('Autotheft', function() 82 | if cache.ped ~= ped then return end 83 | local vehicle = GetVehiclePedIsUsing(ped, true) 84 | exports['ps-dispatch']:CarJacking(vehicle) 85 | end) 86 | end) 87 | 88 | AddEventHandler('CEventShockingCarAlarm', function(_, ped) 89 | WaitTimer('Autotheft', function() 90 | if cache.ped ~= ped then return end 91 | local vehicle = GetVehiclePedIsUsing(ped, true) 92 | exports['ps-dispatch']:VehicleTheft(vehicle) 93 | end) 94 | end) 95 | 96 | AddEventHandler('CEventExplosionHeard', function(witnesses, ped) 97 | if witnesses and not isPedAWitness(witnesses, ped) then return end 98 | WaitTimer('Explosion', function() 99 | exports['ps-dispatch']:Explosion() 100 | end) 101 | end) 102 | 103 | AddEventHandler('gameEventTriggered', function(name, args) 104 | if name ~= 'CEventNetworkEntityDamage' then return end 105 | local victim = args[1] 106 | local isDead = args[6] == 1 107 | WaitTimer('PlayerDowned', function() 108 | if not victim or victim ~= cache.ped then return end 109 | if not isDead then return end 110 | 111 | if PlayerData.job.type == 'leo' then 112 | exports['ps-dispatch']:OfficerDown() 113 | elseif PlayerData.job.type == 'ems' then 114 | exports['ps-dispatch']:EmsDown() 115 | else 116 | exports['ps-dispatch']:InjuriedPerson() 117 | end 118 | end) 119 | end) 120 | 121 | local SpeedingEvents = { 122 | 'CEventShockingCarChase', 123 | 'CEventShockingDrivingOnPavement', 124 | 'CEventShockingBicycleOnPavement', 125 | 'CEventShockingMadDriverBicycle', 126 | 'CEventShockingMadDriverExtreme', 127 | 'CEventShockingEngineRevved', 128 | 'CEventShockingInDangerousVehicle' 129 | } 130 | 131 | local exemptVehicleClass = { 132 | [15] = true, -- Helicopters 133 | [16] = true, -- Planes 134 | } 135 | 136 | local SpeedTrigger = 0 137 | for i = 1, #SpeedingEvents do 138 | local event = SpeedingEvents[i] 139 | AddEventHandler(event, function(_, ped) 140 | WaitTimer('Speeding', function() 141 | local currentTime = GetGameTimer() 142 | if currentTime - SpeedTrigger < 10000 then 143 | return 144 | end 145 | if cache.ped ~= ped then return end 146 | 147 | if PlayerData.job.type == 'leo' then 148 | if not Config.Debug then 149 | return 150 | end 151 | end 152 | 153 | local vehicleClass = GetVehicleClass(cache.vehicle) 154 | if exemptVehicleClass[vehicleClass] then return end 155 | 156 | if GetEntitySpeed(cache.vehicle) * 3.6 < (80 + math.random(0, 20)) then return end 157 | 158 | if cache.ped ~= GetPedInVehicleSeat(cache.vehicle, -1) then return end 159 | 160 | exports['ps-dispatch']:SpeedingVehicle() 161 | SpeedTrigger = GetGameTimer() 162 | end) 163 | end) 164 | end 165 | -------------------------------------------------------------------------------- /client/main.lua: -------------------------------------------------------------------------------- 1 | QBCore = exports['qb-core']:GetCoreObject() 2 | PlayerData = {} 3 | inHuntingZone, inNoDispatchZone = false, false 4 | local huntingZones, nodispatchZones, huntingBlips = {} , {}, {} 5 | 6 | local blips = {} 7 | local radius2 = {} 8 | local alertsMuted = false 9 | local alertsDisabled = false 10 | local waypointCooldown = false 11 | 12 | -- Functions 13 | ---@param bool boolean Toggles visibilty of the menu 14 | local function toggleUI(bool) 15 | SetNuiFocus(bool, bool) 16 | SendNUIMessage({ action = "setVisible", data = bool }) 17 | end 18 | 19 | -- Zone Functions -- 20 | local function removeZones() 21 | -- Hunting Zone -- 22 | for i = 1, #huntingZones do 23 | huntingZones[i]:remove() 24 | end 25 | -- No Dispatch Zone -- 26 | for i = 1, #nodispatchZones do 27 | nodispatchZones[i]:remove() 28 | end 29 | -- Hunting Blips -- 30 | for i = 1, #huntingBlips do 31 | RemoveBlip(huntingBlips[i]) 32 | end 33 | -- Reset the stored values too 34 | huntingZones, nodispatchZones, huntingBlips = {} , {}, {} 35 | end 36 | 37 | local function createZones() 38 | -- Hunting Zone -- 39 | if Config.Locations['HuntingZones'][1] then 40 | for _, hunting in pairs(Config.Locations["HuntingZones"]) do 41 | -- Creates the Blips 42 | if Config.EnableHuntingBlip then 43 | local blip = AddBlipForCoord(hunting.coords.x, hunting.coords.y, hunting.coords.z) 44 | local huntingradius = AddBlipForRadius(hunting.coords.x, hunting.coords.y, hunting.coords.z, hunting.radius) 45 | SetBlipSprite(blip, 442) 46 | SetBlipAsShortRange(blip, true) 47 | SetBlipScale(blip, 0.8) 48 | SetBlipColour(blip, 0) 49 | SetBlipColour(huntingradius, 0) 50 | SetBlipAlpha(huntingradius, 40) 51 | BeginTextCommandSetBlipName("STRING") 52 | AddTextComponentString(hunting.label) 53 | EndTextCommandSetBlipName(blip) 54 | huntingBlips[#huntingBlips+1] = blip 55 | huntingBlips[#huntingBlips+1] = huntingradius 56 | end 57 | -- Creates the Sphere -- 58 | local huntingZone = lib.zones.sphere({ 59 | coords = hunting.coords, 60 | radius = hunting.radius, 61 | debug = Config.Debug, 62 | onEnter = function() 63 | inHuntingZone = true 64 | end, 65 | onExit = function() 66 | inHuntingZone = false 67 | end 68 | }) 69 | huntingZones[#huntingZones+1] = huntingZone 70 | end 71 | end 72 | -- No Dispatch Zone -- 73 | if Config.Locations['NoDispatchZones'][1] then 74 | for _, nodispatch in pairs(Config.Locations["NoDispatchZones"]) do 75 | local nodispatchZone = lib.zones.box({ 76 | coords = nodispatch.coords, 77 | size = vec3(nodispatch.length, nodispatch.width, nodispatch.maxZ - nodispatch.minZ), 78 | rotation = nodispatch.heading, 79 | debug = Config.Debug, 80 | onEnter = function() 81 | inNoDispatchZone = true 82 | end, 83 | onExit = function() 84 | inNoDispatchZone = false 85 | end 86 | }) 87 | nodispatchZones[#nodispatchZones+1] = nodispatchZone 88 | end 89 | end 90 | end 91 | 92 | local function setupDispatch() 93 | local playerInfo = QBCore.Functions.GetPlayerData() 94 | local locales = lib.getLocales() 95 | PlayerData = { 96 | charinfo = { 97 | firstname = playerInfo.charinfo.firstname, 98 | lastname = playerInfo.charinfo.lastname 99 | }, 100 | metadata = { 101 | callsign = playerInfo.metadata.callsign 102 | }, 103 | citizenid = playerInfo.citizenid, 104 | job = { 105 | type = playerInfo.job.type, 106 | name = playerInfo.job.name, 107 | label = playerInfo.job.label 108 | }, 109 | } 110 | 111 | Wait(1000) 112 | 113 | SendNUIMessage({ 114 | action = "setupUI", 115 | data = { 116 | locales = locales, 117 | player = PlayerData, 118 | keybind = Config.RespondKeybind, 119 | maxCallList = Config.MaxCallList, 120 | shortCalls = Config.ShortCalls, 121 | } 122 | }) 123 | end 124 | 125 | ---@param data string | table -- The player job or an array of jobs to check against 126 | ---@return boolean -- Returns true if the job is valid 127 | local function isJobValid(data) 128 | if PlayerData.job == nil then return false end 129 | local jobType = PlayerData.job.type 130 | local jobName = PlayerData.job.name 131 | 132 | if type(data) == "string" then 133 | return lib.table.contains(Config.Jobs, data) or lib.table.contains(Config.Jobs, jobName) 134 | elseif type(data) == "table" then 135 | return lib.table.contains(data, jobType) or lib.table.contains(data, jobName) 136 | end 137 | 138 | return false 139 | end 140 | 141 | local function openMenu() 142 | if not isJobValid(PlayerData.job.type) then return end 143 | 144 | local calls = lib.callback.await('ps-dispatch:callback:getCalls', false) 145 | if #calls == 0 then 146 | lib.notify({ description = locale('no_calls'), position = 'top', type = 'error' }) 147 | else 148 | SendNUIMessage({ action = 'setDispatchs', data = calls, }) 149 | toggleUI(true) 150 | end 151 | end 152 | 153 | local function setWaypoint() 154 | if not isJobValid(PlayerData.job.type) then return end 155 | if not IsOnDuty() then return end 156 | 157 | local data = lib.callback.await('ps-dispatch:callback:getLatestDispatch', false) 158 | 159 | if not data then return end 160 | 161 | if data.alertTime == nil then data.alertTime = Config.AlertTime end 162 | local timer = data.alertTime * 1000 163 | 164 | if not waypointCooldown and lib.table.contains(data.jobs, PlayerData.job.type) then 165 | SetNewWaypoint(data.coords.x, data.coords.y) 166 | TriggerServerEvent('ps-dispatch:server:attach', data.id, PlayerData) 167 | lib.notify({ description = locale('waypoint_set'), position = 'top', type = 'success' }) 168 | waypointCooldown = true 169 | SetTimeout(timer, function() 170 | waypointCooldown = false 171 | end) 172 | end 173 | end 174 | 175 | local function randomOffset(baseX, baseY, offset) 176 | local randomX = baseX + math.random(-offset, offset) 177 | local randomY = baseY + math.random(-offset, offset) 178 | 179 | return randomX, randomY 180 | end 181 | 182 | local function createBlipData(coords, radius, sprite, color, scale, flash) 183 | local blip = AddBlipForCoord(coords.x, coords.y, coords.z) 184 | local radiusBlip = AddBlipForRadius(coords.x, coords.y, coords.z, radius) 185 | 186 | SetBlipFlashes(blip, flash) 187 | SetBlipSprite(blip, sprite or 161) 188 | SetBlipHighDetail(blip, true) 189 | SetBlipScale(blip, scale or 1.0) 190 | SetBlipColour(blip, color or 84) 191 | SetBlipAlpha(blip, 255) 192 | SetBlipAsShortRange(blip, false) 193 | SetBlipCategory(blip, 2) 194 | SetBlipColour(radiusBlip, color or 84) 195 | SetBlipAlpha(radiusBlip, 128) 196 | 197 | return blip, radiusBlip 198 | end 199 | 200 | local function createBlip(data, blipData) 201 | local blip, radius = nil, nil 202 | local sprite = blipData.sprite or blipData.alert.sprite or 161 203 | local color = blipData.color or blipData.alert.color or 84 204 | local scale = blipData.scale or blipData.alert.scale or 1.0 205 | local flash = blipData.flash or false 206 | local alpha = 255 207 | local radiusAlpha = 128 208 | local blipWaitTime = ((blipData.length or blipData.alert.length) * 60000) / radiusAlpha 209 | 210 | if blipData.offset then 211 | local offsetX, offsetY = randomOffset(data.coords.x, data.coords.y, Config.MaxOffset) 212 | blip, radius = createBlipData({ x = offsetX, y = offsetY, z = data.coords.z }, blipData.radius, sprite, color, scale, flash) 213 | blips[data.id] = blip 214 | radius2[data.id] = radius 215 | else 216 | blip, radius = createBlipData(data.coords, blipData.radius, sprite, color, scale, flash) 217 | blips[data.id] = blip 218 | radius2[data.id] = radius 219 | end 220 | 221 | BeginTextCommandSetBlipName('STRING') 222 | AddTextComponentString(data.code .. ' - ' .. data.message) 223 | EndTextCommandSetBlipName(blip) 224 | 225 | while radiusAlpha > 0 do 226 | Wait(blipWaitTime) 227 | radiusAlpha = math.max(0, radiusAlpha - 1) 228 | SetBlipAlpha(radius, radiusAlpha) 229 | end 230 | 231 | RemoveBlip(radius) 232 | RemoveBlip(blip) 233 | end 234 | 235 | local function addBlip(data, blipData) 236 | CreateThread(function() 237 | createBlip(data, blipData) 238 | end) 239 | if not alertsMuted then 240 | if blipData.sound == "Lose_1st" then 241 | PlaySound(-1, blipData.sound, blipData.sound2, 0, 0, 1) 242 | else 243 | TriggerServerEvent("InteractSound_SV:PlayOnSource", blipData.sound or blipData.alert.sound, 0.25) 244 | end 245 | end 246 | end 247 | 248 | -- Keybind 249 | local RespondToDispatch = lib.addKeybind({ 250 | name = 'RespondToDispatch', 251 | description = 'Set waypoint to last call location', 252 | defaultKey = Config.RespondKeybind, 253 | onPressed = setWaypoint, 254 | }) 255 | 256 | local OpenDispatchMenu = lib.addKeybind({ 257 | name = 'OpenDispatchMenu', 258 | description = 'Open Dispatch Menu', 259 | defaultKey = Config.OpenDispatchMenu, 260 | onPressed = openMenu, 261 | }) 262 | 263 | -- Events 264 | RegisterNetEvent('ps-dispatch:client:notify', function(data, source) 265 | if data.alertTime == nil then data.alertTime = Config.AlertTime end 266 | local timer = data.alertTime * 1000 267 | 268 | if alertsDisabled then return end 269 | if not isJobValid(data.jobs) then return end 270 | if not IsOnDuty() then return end 271 | 272 | timerCheck = true 273 | 274 | SendNUIMessage({ 275 | action = 'newCall', 276 | data = { 277 | data = data, 278 | timer = timer, 279 | } 280 | }) 281 | 282 | addBlip(data, Config.Blips[data.codeName] or data.alert) 283 | 284 | RespondToDispatch:disable(false) 285 | OpenDispatchMenu:disable(true) 286 | 287 | local startTime = GetGameTimer() 288 | while timerCheck do 289 | Wait(1000) 290 | 291 | local currentTime = GetGameTimer() 292 | local elapsed = currentTime - startTime 293 | 294 | if elapsed >= timer then 295 | break 296 | end 297 | end 298 | 299 | timerCheck = false 300 | OpenDispatchMenu:disable(false) 301 | RespondToDispatch:disable(true) 302 | end) 303 | 304 | RegisterNetEvent('ps-dispatch:client:openMenu', function(data) 305 | if not isJobValid(PlayerData.job.type) then return end 306 | if not IsOnDuty() then return end 307 | 308 | if #data == 0 then 309 | lib.notify({ description = locale('no_calls'), position = 'top', type = 'error' }) 310 | else 311 | toggleUI(true) 312 | SendNUIMessage({ action = 'setDispatchs', data = data, }) 313 | end 314 | end) 315 | 316 | -- EventHandlers 317 | RegisterNetEvent("QBCore:Client:OnJobUpdate", setupDispatch) 318 | 319 | AddEventHandler('QBCore:Client:OnPlayerLoaded', function() 320 | setupDispatch() 321 | createZones() 322 | end) 323 | 324 | AddEventHandler('QBCore:Client:OnPlayerUnload', removeZones) 325 | 326 | AddEventHandler('onResourceStart', function(resourceName) 327 | if resourceName ~= GetCurrentResourceName() then return end 328 | setupDispatch() 329 | end) 330 | 331 | AddEventHandler('onResourceStop', function(resourceName) 332 | if resourceName ~= GetCurrentResourceName() then return end 333 | removeZones() 334 | end) 335 | 336 | -- NUICallbacks 337 | RegisterNUICallback("hideUI", function(_, cb) 338 | toggleUI(false) 339 | cb("ok") 340 | end) 341 | 342 | RegisterNUICallback("attachUnit", function(data, cb) 343 | TriggerServerEvent('ps-dispatch:server:attach', data.id, PlayerData) 344 | SetNewWaypoint(data.coords.x, data.coords.y) 345 | cb("ok") 346 | end) 347 | 348 | RegisterNUICallback("detachUnit", function(data, cb) 349 | TriggerServerEvent('ps-dispatch:server:detach', data.id, PlayerData) 350 | DeleteWaypoint() 351 | cb("ok") 352 | end) 353 | 354 | RegisterNUICallback("toggleMute", function(data, cb) 355 | local muteStatus = data.boolean and locale('muted') or locale('unmuted') 356 | lib.notify({ description = locale('alerts') .. muteStatus, position = 'top', type = 'warning' }) 357 | alertsMuted = data.boolean 358 | cb("ok") 359 | end) 360 | 361 | RegisterNUICallback("toggleAlerts", function(data, cb) 362 | local muteStatus = data.boolean and locale('disabled') or locale('enabled') 363 | lib.notify({ description = locale('alerts') .. muteStatus, position = 'top', type = 'warning' }) 364 | alertsDisabled = data.boolean 365 | cb("ok") 366 | end) 367 | 368 | RegisterNUICallback("clearBlips", function(data, cb) 369 | lib.notify({ description = locale('blips_cleared'), position = 'top', type = 'success' }) 370 | for k, v in pairs(blips) do 371 | RemoveBlip(v) 372 | end 373 | for k, v in pairs(radius2) do 374 | RemoveBlip(v) 375 | end 376 | cb("ok") 377 | end) 378 | 379 | RegisterNUICallback("refreshAlerts", function(data, cb) 380 | lib.notify({ description = locale('alerts_refreshed'), position = 'top', type = 'success' }) 381 | local data = lib.callback.await('ps-dispatch:callback:getCalls', false) 382 | SendNUIMessage({ action = 'setDispatchs', data = data, }) 383 | cb("ok") 384 | end) 385 | -------------------------------------------------------------------------------- /client/utils.lua: -------------------------------------------------------------------------------- 1 | function GetPlayerHeading() 2 | local heading = GetEntityHeading(cache.ped) 3 | 4 | if heading >= 315 or heading < 45 then 5 | return locale('north') 6 | elseif heading >= 45 and heading < 135 then 7 | return locale('west') 8 | elseif heading >= 135 and heading < 225 then 9 | return locale('south') 10 | elseif heading >= 225 and heading < 315 then 11 | return locale('east') 12 | end 13 | end 14 | 15 | function GetPlayerGender() 16 | local gender = locale('male') 17 | if QBCore.Functions.GetPlayerData().charinfo.gender == 1 then 18 | gender = locale('female') 19 | end 20 | return gender 21 | end 22 | 23 | function GetIsHandcuffed() 24 | return QBCore.Functions.GetPlayerData()?.metadata?.ishandcuffed 25 | end 26 | 27 | function IsOnDuty() 28 | if Config.OnDutyOnly then 29 | if QBCore.Functions.GetPlayerData().job.onduty then 30 | return true 31 | else 32 | return false 33 | end 34 | end 35 | return true 36 | end 37 | 38 | ---@return boolean 39 | local function HasPhone() 40 | for _, item in ipairs(Config.PhoneItems) do 41 | if QBCore.Functions.HasItem(item) then 42 | return true 43 | end 44 | end 45 | return false 46 | end 47 | 48 | ---@param coords table 49 | ---@return string 50 | function GetStreetAndZone(coords) 51 | local zone = GetLabelText(GetNameOfZone(coords.x, coords.y, coords.z)) 52 | local street = GetStreetNameFromHashKey(GetStreetNameAtCoord(coords.x, coords.y, coords.z)) 53 | return street .. ", " .. zone 54 | end 55 | 56 | ---@param vehicle string 57 | ---@return string 58 | local function getVehicleColor(vehicle) 59 | local vehicleColor1, vehicleColor2 = GetVehicleColours(vehicle) 60 | local color1 = Config.Colors[tostring(vehicleColor1)] 61 | local color2 = Config.Colors[tostring(vehicleColor2)] 62 | 63 | if color1 and color2 then 64 | return color2 .. " on " .. color1 65 | elseif color1 then 66 | return color1 67 | elseif color2 then 68 | return color2 69 | else 70 | return "Unknown" 71 | end 72 | end 73 | 74 | ---@param vehicle string 75 | ---@return string 76 | local function getVehicleDoors(vehicle) 77 | local doorCount = 0 78 | 79 | if GetEntityBoneIndexByName(vehicle, 'door_pside_f') ~= -1 then doorCount = doorCount + 1 end 80 | if GetEntityBoneIndexByName(vehicle, 'door_pside_r') ~= -1 then doorCount = doorCount + 1 end 81 | if GetEntityBoneIndexByName(vehicle, 'door_dside_f') ~= -1 then doorCount = doorCount + 1 end 82 | if GetEntityBoneIndexByName(vehicle, 'door_dside_r') ~= -1 then doorCount = doorCount + 1 end 83 | 84 | if doorCount == 2 then 85 | doorCount = locale('two_door') 86 | elseif doorCount == 3 then 87 | doorCount = locale('three_door') 88 | elseif doorCount == 4 then 89 | doorCount = locale('four_door') 90 | else 91 | doorCount = 'unknown' 92 | end 93 | 94 | return doorCount 95 | end 96 | 97 | ---@param vehicle string 98 | ---@return table 99 | function GetVehicleData(vehicle) 100 | local data = {} 101 | 102 | local vehicleClass = { 103 | [0] = locale('compact'), 104 | [1] = locale('sedan'), 105 | [2] = locale('suv'), 106 | [3] = locale('coupe'), 107 | [4] = locale('muscle'), 108 | [5] = locale('sports_classic'), 109 | [6] = locale('sports'), 110 | [7] = locale('super'), 111 | [8] = locale('motorcycle'), 112 | [9] = locale('offroad'), 113 | [10] = locale('industrial'), 114 | [11] = locale('utility'), 115 | [12] = locale('van'), 116 | [17] = locale('service'), 117 | [19] = locale('military'), 118 | [20] = locale('truck') 119 | } 120 | 121 | data.class = vehicleClass[GetVehicleClass(vehicle)] or "Unknown" 122 | data.name = GetLabelText(GetDisplayNameFromVehicleModel(GetEntityModel(vehicle))) 123 | data.plate = GetVehicleNumberPlateText(vehicle) 124 | data.doors = getVehicleDoors(vehicle) 125 | data.color = getVehicleColor(vehicle) 126 | data.id = NetworkGetNetworkIdFromEntity(vehicle) 127 | 128 | return data 129 | end 130 | 131 | function PhoneAnimation() 132 | lib.requestAnimDict("cellphone@in_car@ds", 500) 133 | 134 | if not IsEntityPlayingAnim(cache.ped, "cellphone@in_car@ds", "cellphone_call_listen_base", 3) then 135 | TaskPlayAnim(cache.ped, "cellphone@in_car@ds", "cellphone_call_listen_base", 3.0, 3.0, -1, 50, 0, false, false, false) 136 | end 137 | 138 | Wait(2500) 139 | StopEntityAnim(cache.ped, "cellphone_call_listen_base", "cellphone@in_car@ds", 3) 140 | end 141 | 142 | ---@param message string 143 | ---@return boolean 144 | function IsCallAllowed(message) 145 | local msgLength = string.len(message) 146 | 147 | if msgLength == 0 then return false end 148 | if GetIsHandcuffed() then return false end 149 | if Config.PhoneRequired and not HasPhone() then QBCore.Functions.Notify('You need a communications device for this.', 'error', 5000) return false end 150 | 151 | return true 152 | end 153 | 154 | local weaponTable = { 155 | [584646201] = "CLASS 2: AP-Pistol", 156 | [453432689] = "CLASS 1: Pistol", 157 | [3219281620] = "CLASS 1: Pistol MK2", 158 | [1593441988] = "CLASS 1: Combat Pistol", 159 | [-1716589765] = "CLASS 1: Heavy Pistol", 160 | [-1076751822] = "CLASS 1: SNS-Pistol", 161 | [-771403250] = "CLASS 2: Desert Eagle", 162 | [137902532] = "CLASS 2: Vintage Pistol", 163 | [-598887786] = "CLASS 2: Marksman Pistol", 164 | [-1045183535] = "CLASS 2: Revolver", 165 | [911657153] = "Taser", 166 | [324215364] = "CLASS 2: Micro-SMG", 167 | [-619010992] = "CLASS 2: Machine-Pistol", 168 | [736523883] = "CLASS 2: SMG", 169 | [2024373456] = "CLASS 2: SMG MK2", 170 | [-270015777] = "CLASS 2: Assault SMG", 171 | [171789620] = "CLASS 2: Combat PDW", 172 | [-1660422300] = "CLASS 4: Combat MG", 173 | [3686625920] = "CLASS 4: Combat MG MK2", 174 | [1627465347] = "CLASS 4: Gusenberg", 175 | [-1121678507] = "CLASS 2: Mini SMG", 176 | [-1074790547] = "CLASS 3: Assaultrifle", 177 | [961495388] = "CLASS 3: Assaultrifle MK2", 178 | [-2084633992] = "CLASS 3: Carbinerifle", 179 | [4208062921] = "CLASS 3: Carbinerifle MK2", 180 | [-1357824103] = "CLASS 3: Advancedrifle", 181 | [-1063057011] = "CLASS 3: Specialcarbine", 182 | [2132975508] = "CLASS 3: Bulluprifle", 183 | [1649403952] = "CLASS 3: Compactrifle", 184 | [100416529] = "CLASS 4: Sniperrifle", 185 | [205991906] = "CLASS 4: Heavy Sniper", 186 | [177293209] = "CLASS 4: Heavy Sniper MK2", 187 | [-952879014] = "CLASS 4: Marksmanrifle", 188 | [487013001] = "CLASS 2: Pumpshotgun", 189 | [2017895192] = "CLASS 2: Sawnoff Shotgun", 190 | [-1654528753] = "CLASS 3: Bullupshotgun", 191 | [-494615257] = "CLASS 3: Assaultshotgun", 192 | [-1466123874] = "CLASS 3: Musket", 193 | [984333226] = "CLASS 3: Heavyshotgun", 194 | [-275439685] = "CLASS 2: Doublebarrel Shotgun", 195 | [317205821] = "CLASS 2: Autoshotgun", 196 | [-1568386805] = "CLASS 5: GRENADE LAUNCHER", 197 | [-1312131151] = "CLASS 5: RPG", 198 | [125959754] = "CLASS 5: Compactlauncher" 199 | } 200 | 201 | function GetWeaponName() 202 | local currentWeapon = GetSelectedPedWeapon(cache.ped) 203 | return weaponTable[currentWeapon] or "Unknown" 204 | end 205 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'cerulean' 2 | 3 | game "gta5" 4 | 5 | author "Project Sloth & OK1ez" 6 | version '2.2.1' 7 | 8 | lua54 'yes' 9 | 10 | ui_page 'html/index.html' 11 | -- ui_page 'http://localhost:5173/' --for dev 12 | 13 | client_script { 14 | '@PolyZone/client.lua', 15 | '@PolyZone/CircleZone.lua', 16 | '@PolyZone/BoxZone.lua', 17 | 'client/**', 18 | } 19 | server_script { 20 | "server/**", 21 | } 22 | shared_script { 23 | "shared/**", 24 | '@ox_lib/init.lua', 25 | } 26 | 27 | files { 28 | 'html/**', 29 | 'locales/*.json', 30 | } 31 | 32 | ox_lib 'locale' -- v3.8.0 or above 33 | -------------------------------------------------------------------------------- /html/index.css: -------------------------------------------------------------------------------- 1 | div.svelte-11k92at{position:absolute;left:0;top:0}main.svelte-a4h32x{position:absolute;left:0;top:0;z-index:100;-webkit-user-select:none;-moz-user-select:none;user-select:none;box-sizing:border-box;padding:0;margin:0;height:100vh;width:100vw}*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.visible{visibility:visible}.absolute{position:absolute}.top-0{top:0}.z-\[1000\]{z-index:1000}.mx-\[1vh\]{margin-left:1vh;margin-right:1vh}.mx-\[2vh\]{margin-left:2vh;margin-right:2vh}.my-\[0\.5vh\]{margin-top:.5vh;margin-bottom:.5vh}.mb-\[1vh\]{margin-bottom:1vh}.ml-\[0\.5vh\]{margin-left:.5vh}.ml-\[1vh\]{margin-left:1vh}.ml-\[2vh\]{margin-left:2vh}.ml-\[3vh\]{margin-left:3vh}.ml-auto{margin-left:auto}.mr-4{margin-right:1rem}.mr-\[0\.5vh\]{margin-right:.5vh}.mr-\[1vh\]{margin-right:1vh}.mr-\[2vh\]{margin-right:2vh}.flex{display:flex}.h-\[3vh\]{height:3vh}.h-\[5vh\]{height:5vh}.h-\[85\%\]{height:85%}.h-\[97\%\]{height:97%}.h-fit{height:-moz-fit-content;height:fit-content}.h-screen{height:100vh}.w-\[25\%\]{width:25%}.w-\[3\.2vh\]{width:3.2vh}.w-\[30\%\]{width:30%}.w-\[70\%\]{width:70%}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-screen{width:100vw}.resize{resize:both}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.gap-2{gap:.5rem}.gap-\[0\.2vh\]{gap:.2vh}.gap-\[1vh\]{gap:1vh}.gap-y-\[0\.4vh\]{row-gap:.4vh}.overflow-auto{overflow:auto}.rounded-full{border-radius:9999px}.bg-\[\#004ca5\]{--tw-bg-opacity: 1;background-color:rgb(0 76 165 / var(--tw-bg-opacity))}.bg-\[\#0098A3\]{--tw-bg-opacity: 1;background-color:rgb(0 152 163 / var(--tw-bg-opacity))}.bg-\[\#232B33\]{--tw-bg-opacity: 1;background-color:rgb(35 43 51 / var(--tw-bg-opacity))}.bg-\[\#25303B\]{--tw-bg-opacity: 1;background-color:rgb(37 48 59 / var(--tw-bg-opacity))}.bg-\[\#4b4b4b\]{--tw-bg-opacity: 1;background-color:rgb(75 75 75 / var(--tw-bg-opacity))}.bg-\[\#e03535\]{--tw-bg-opacity: 1;background-color:rgb(224 53 53 / var(--tw-bg-opacity))}.bg-accent_cyan{--tw-bg-opacity: 1;background-color:rgb(0 152 163 / var(--tw-bg-opacity))}.bg-accent_dark_green{--tw-bg-opacity: 1;background-color:rgb(0 133 99 / var(--tw-bg-opacity))}.bg-accent_dark_red{--tw-bg-opacity: 1;background-color:rgb(133 0 50 / var(--tw-bg-opacity))}.bg-accent_green{--tw-bg-opacity: 1;background-color:rgb(0 163 121 / var(--tw-bg-opacity))}.bg-accent_red{--tw-bg-opacity: 1;background-color:rgb(255 0 78 / var(--tw-bg-opacity))}.bg-neutral-700{--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity))}.bg-primary{--tw-bg-opacity: 1;background-color:rgb(35 43 51 / var(--tw-bg-opacity))}.bg-priority_primary{--tw-bg-opacity: 1;background-color:rgb(51 35 40 / var(--tw-bg-opacity))}.bg-priority_quaternary{--tw-bg-opacity: 1;background-color:rgb(154 0 58 / var(--tw-bg-opacity))}.bg-priority_secondary{--tw-bg-opacity: 1;background-color:rgb(59 37 47 / var(--tw-bg-opacity))}.bg-priority_tertiary{--tw-bg-opacity: 1;background-color:rgb(71 47 57 / var(--tw-bg-opacity))}.bg-secondary{--tw-bg-opacity: 1;background-color:rgb(37 48 59 / var(--tw-bg-opacity))}.bg-tertiary{--tw-bg-opacity: 1;background-color:rgb(47 60 71 / var(--tw-bg-opacity))}.p-2{padding:.5rem}.p-\[1vh\]{padding:1vh}.px-\[1\.4vh\]{padding-left:1.4vh;padding-right:1.4vh}.px-\[1\.5vh\]{padding-left:1.5vh;padding-right:1.5vh}.px-\[2vh\]{padding-left:2vh;padding-right:2vh}.py-\[0\.2vh\]{padding-top:.2vh;padding-bottom:.2vh}.py-\[0\.4vh\]{padding-top:.4vh;padding-bottom:.4vh}.pr-\[0\.5vh\]{padding-right:.5vh}.text-start{text-align:start}.text-\[1\.3vh\]{font-size:1.3vh}.text-\[1\.4vh\]{font-size:1.4vh}.text-\[1\.5vh\]{font-size:1.5vh}.font-medium{font-weight:500}.uppercase{text-transform:uppercase}.text-accent_cyan{--tw-text-opacity: 1;color:rgb(0 152 163 / var(--tw-text-opacity))}.text-accent_red{--tw-text-opacity: 1;color:rgb(255 0 78 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}*{margin:0;padding:0}*:focus{outline:none}:root{font-size:62.5%;font-smooth:auto;color:#d8d8d8;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%}html,body{height:100vh;width:100vw;font-size:1.6rem;overflow:hidden}::-webkit-scrollbar{height:0px;width:4px;background-color:#7979795e;border-radius:50px}::-webkit-scrollbar-thumb{background-color:#cecece;border-radius:50px}.hover\:bg-secondary:hover{--tw-bg-opacity: 1;background-color:rgb(37 48 59 / var(--tw-bg-opacity))} 2 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | OK1ez 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /html/index.js: -------------------------------------------------------------------------------- 1 | (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))l(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const r of s.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&l(r)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function l(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();function q(){}const Vt=e=>e;function Ht(e,t){for(const n in t)e[n]=t[n];return e}function Et(e){return e()}function He(){return Object.create(null)}function Z(e){e.forEach(Et)}function De(e){return typeof e=="function"}function ie(e,t){return e!=e?t==t:e!==t||e&&typeof e=="object"||typeof e=="function"}function Yt(e){return Object.keys(e).length===0}function Nt(e,...t){if(e==null)return q;const n=e.subscribe(...t);return n.unsubscribe?()=>n.unsubscribe():n}function z(e,t,n){e.$$.on_destroy.push(Nt(t,n))}function Gt(e,t,n,l){if(e){const i=Dt(e,t,n,l);return e[0](i)}}function Dt(e,t,n,l){return e[1]&&l?Ht(n.ctx.slice(),e[1](l(t))):n.ctx}function zt(e,t,n,l){if(e[2]&&l){const i=e[2](l(n));if(t.dirty===void 0)return i;if(typeof i=="object"){const s=[],r=Math.max(t.dirty.length,i.length);for(let a=0;a32){const t=[],n=e.ctx.length/32;for(let l=0;lwindow.performance.now():()=>Date.now(),Oe=St?e=>requestAnimationFrame(e):q;const ae=new Set;function At(e){ae.forEach(t=>{t.c(e)||(ae.delete(t),t.f())}),ae.size!==0&&Oe(At)}function Kt(e){let t;return ae.size===0&&Oe(At),{promise:new Promise(n=>{ae.add(t={c:e,f:n})}),abort(){ae.delete(t)}}}function d(e,t){e.appendChild(t)}function It(e){if(!e)return document;const t=e.getRootNode?e.getRootNode():e.ownerDocument;return t&&t.host?t:e.ownerDocument}function Qt(e){const t=g("style");return Xt(It(e),t),t.sheet}function Xt(e,t){return d(e.head||e,t),t.sheet}function L(e,t,n){e.insertBefore(t,n||null)}function S(e){e.parentNode&&e.parentNode.removeChild(e)}function _e(e,t){for(let n=0;ne.removeEventListener(t,n,l)}function _(e,t,n){n==null?e.removeAttribute(t):e.getAttribute(t)!==n&&e.setAttribute(t,n)}function Zt(e){return Array.from(e.childNodes)}function B(e,t){t=""+t,e.data!==t&&(e.data=t)}function J(e,t,n){e.classList[n?"add":"remove"](t)}function xt(e,t,{bubbles:n=!1,cancelable:l=!1}={}){const i=document.createEvent("CustomEvent");return i.initCustomEvent(e,n,l,t),i}const ke=new Map;let Ee=0;function en(e){let t=5381,n=e.length;for(;n--;)t=(t<<5)-t^e.charCodeAt(n);return t>>>0}function tn(e,t){const n={stylesheet:Qt(t),rules:{}};return ke.set(e,n),n}function Ge(e,t,n,l,i,s,r,a=0){const f=16.666/l;let o=`{ 2 | `;for(let $=0;$<=1;$+=f){const y=t+(n-t)*s($);o+=$*100+`%{${r(y,1-y)}} 3 | `}const u=o+`100% {${r(n,1-n)}} 4 | }`,c=`__svelte_${en(u)}_${a}`,m=It(e),{stylesheet:p,rules:b}=ke.get(m)||tn(m,e);b[c]||(b[c]=!0,p.insertRule(`@keyframes ${c} ${u}`,p.cssRules.length));const N=e.style.animation||"";return e.style.animation=`${N?`${N}, `:""}${c} ${l}ms linear ${i}ms 1 both`,Ee+=1,c}function nn(e,t){const n=(e.style.animation||"").split(", "),l=n.filter(t?s=>s.indexOf(t)<0:s=>s.indexOf("__svelte")===-1),i=n.length-l.length;i&&(e.style.animation=l.join(", "),Ee-=i,Ee||ln())}function ln(){Oe(()=>{Ee||(ke.forEach(e=>{const{ownerNode:t}=e.stylesheet;t&&S(t)}),ke.clear())})}let ge;function ve(e){ge=e}function Re(){if(!ge)throw new Error("Function called outside component initialization");return ge}function Me(e){Re().$$.on_mount.push(e)}function rn(e){Re().$$.after_update.push(e)}function Ct(e){Re().$$.on_destroy.push(e)}const oe=[],ze=[];let fe=[];const We=[],sn=Promise.resolve();let Ce=!1;function on(){Ce||(Ce=!0,sn.then(jt))}function te(e){fe.push(e)}const Ae=new Set;let re=0;function jt(){if(re!==0)return;const e=ge;do{try{for(;ree.indexOf(l)===-1?t.push(l):n.push(l)),n.forEach(l=>l()),fe=t}let me;function cn(){return me||(me=Promise.resolve(),me.then(()=>{me=null})),me}function Ie(e,t,n){e.dispatchEvent(xt(`${t?"intro":"outro"}${n}`))}const $e=new Set;let X;function ne(){X={r:0,c:[],p:X}}function le(){X.r||Z(X.c),X=X.p}function F(e,t){e&&e.i&&($e.delete(e),e.i(t))}function V(e,t,n,l){if(e&&e.o){if($e.has(e))return;$e.add(e),X.c.push(()=>{$e.delete(e),l&&(n&&e.d(1),l())}),e.o(t)}else l&&l()}const un={duration:0};function ce(e,t,n,l){const i={direction:"both"};let s=t(e,n,i),r=l?0:1,a=null,f=null,o=null;function u(){o&&nn(e,o)}function c(p,b){const N=p.b-r;return b*=Math.abs(N),{a:r,b:p.b,d:N,duration:b,start:p.start,end:p.start+b,group:p.group}}function m(p){const{delay:b=0,duration:N=300,easing:$=Vt,tick:y=q,css:T}=s||un,E={start:Jt()+b,b:p};p||(E.group=X,X.r+=1),a||f?f=E:(T&&(u(),o=Ge(e,r,p,N,b,$,T)),p&&y(0,1),a=c(E,N),te(()=>Ie(e,p,"start")),Kt(w=>{if(f&&w>f.start&&(a=c(f,N),f=null,Ie(e,a.b,"start"),T&&(u(),o=Ge(e,r,a.b,a.duration,0,$,s.css))),a){if(w>=a.end)y(r=a.b,1-r),Ie(e,a.b,"end"),f||(a.b?u():--a.group.r||Z(a.group.c)),a=null;else if(w>=a.start){const U=w-a.start;r=a.a+a.d*$(U/a.duration),y(r,1-r)}}return!!(a||f)}))}return{run(p){De(s)?cn().then(()=>{s=s(i),m(p)}):m(p)},end(){u(),a=f=null}}}function dn(e,t){V(e,1,1,()=>{t.delete(e.key)})}function _n(e,t,n,l,i,s,r,a,f,o,u,c){let m=e.length,p=s.length,b=m;const N={};for(;b--;)N[e[b].key]=b;const $=[],y=new Map,T=new Map,E=[];for(b=p;b--;){const k=c(i,s,b),h=n(k);let v=r.get(h);v?l&&E.push(()=>v.p(k,t)):(v=o(h,k),v.c()),y.set(h,$[b]=v),h in N&&T.set(h,Math.abs(b-N[h]))}const w=new Set,U=new Set;function D(k){F(k,1),k.m(a,u),r.set(k.key,k),u=k.first,p--}for(;m&&p;){const k=$[p-1],h=e[m-1],v=k.key,I=h.key;k===h?(u=k.first,m--,p--):y.has(I)?!r.has(v)||w.has(v)?D(k):U.has(I)?m--:T.get(v)>T.get(I)?(U.add(v),D(k)):(w.add(I),m--):(f(h,r),m--)}for(;m--;){const k=e[m];y.has(k.key)||f(k,r)}for(;p;)D($[p-1]);return Z(E),$}function ye(e){e&&e.c()}function ue(e,t,n,l){const{fragment:i,after_update:s}=e.$$;i&&i.m(t,n),l||te(()=>{const r=e.$$.on_mount.map(Et).filter(De);e.$$.on_destroy?e.$$.on_destroy.push(...r):Z(r),e.$$.on_mount=[]}),s.forEach(te)}function de(e,t){const n=e.$$;n.fragment!==null&&(fn(n.after_update),Z(n.on_destroy),n.fragment&&n.fragment.d(t),n.on_destroy=n.fragment=null,n.ctx=[])}function pn(e,t){e.$$.dirty[0]===-1&&(oe.push(e),on(),e.$$.dirty.fill(0)),e.$$.dirty[t/31|0]|=1<{const b=p.length?p[0]:m;return o.ctx&&i(o.ctx[c],o.ctx[c]=b)&&(!o.skip_bound&&o.bound[c]&&o.bound[c](b),u&&pn(e,c)),m}):[],o.update(),u=!0,Z(o.before_update),o.fragment=l?l(o.ctx):!1,t.target){if(t.hydrate){const c=Zt(t.target);o.fragment&&o.fragment.l(c),c.forEach(S)}else o.fragment&&o.fragment.c();t.intro&&F(e.$$.fragment),ue(e,t.target,t.anchor,t.customElement),jt()}ve(f)}class he{$destroy(){de(this,1),this.$destroy=q}$on(t,n){if(!De(n))return q;const l=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return l.push(n),()=>{const i=l.indexOf(n);i!==-1&&l.splice(i,1)}}$set(t){this.$$set&&!Yt(t)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}}function be(e,t){const n=l=>{const{action:i,data:s}=l.data;i===e&&t(s)};Me(()=>window.addEventListener("message",n)),Ct(()=>window.removeEventListener("message",n))}const se=[];function hn(e,t){return{subscribe:W(e,t).subscribe}}function W(e,t=q){let n;const l=new Set;function i(a){if(ie(e,a)&&(e=a,n)){const f=!se.length;for(const o of l)o[1](),se.push(o,e);if(f){for(let o=0;o{l.delete(o),l.size===0&&n&&(n(),n=null)}}return{set:i,update:s,subscribe:r}}function mn(e,t,n){const l=!Array.isArray(e),i=l?[e]:e,s=t.length<2;return hn(n,r=>{let a=!1;const f=[];let o=0,u=q;const c=()=>{if(o)return;u();const p=t(l?f[0]:f,r);s?r(p):u=De(p)?p:q},m=i.map((p,b)=>Nt(p,N=>{f[b]=N,o&=~(1<{o|=1<t.filter(n=>n.data.id!==e))}const Be=W(null),Pe=W(null),vn=mn([Be,Lt,Ue],([e,t,n])=>!e||t===null||!n?[]:e.slice(-t).filter(l=>l.message&&l.jobs.includes(n.job.type)).reverse());let Le=!1;Se.subscribe(e=>{Le=e});let Ut="";je.subscribe(e=>{Ut=e});async function K(e,t={},n){if(Le==!0&&n||Le==!0)return Promise.resolve(n||{});const l={method:"post",headers:{"Content-Type":"application/json; charset=UTF-8"},body:JSON.stringify(t)},i=window.GetParentResourceName?window.GetParentResourceName():Ut;return await(await fetch(`https://${i}/${e}`,l)).json()}function Ke(e){let t,n;const l=e[2].default,i=Gt(l,e,e[1],null);return{c(){t=g("main"),i&&i.c(),_(t,"class","svelte-a4h32x")},m(s,r){L(s,t,r),i&&i.m(t,null),n=!0},p(s,r){i&&i.p&&(!n||r&2)&&Wt(i,l,s,s[1],n?zt(l,s[1],r,null):qt(s[1]),null)},i(s){n||(F(i,s),n=!0)},o(s){V(i,s),n=!1},d(s){s&&S(t),i&&i.d(s)}}}function gn(e){let t,n,l=e[0]&&Ke(e);return{c(){l&&l.c(),t=x()},m(i,s){l&&l.m(i,s),L(i,t,s),n=!0},p(i,[s]){i[0]?l?(l.p(i,s),s&1&&F(l,1)):(l=Ke(i),l.c(),F(l,1),l.m(t.parentNode,t)):l&&(ne(),V(l,1,1,()=>{l=null}),le())},i(i){n||(F(l),n=!0)},o(i){V(l),n=!1},d(i){l&&l.d(i),i&&S(t)}}}function yn(e,t,n){let l,i;z(e,Se,f=>n(4,l=f)),z(e,ee,f=>n(0,i=f));let{$$slots:s={},$$scope:r}=t,a;return ee.subscribe(f=>{a=f}),be("setVisible",f=>{ee.set(f)}),Me(()=>{const f=o=>{a&&o.code==="Escape"&&(K("hideUI"),ee.set(!1)),!a&&o.code==="Escape"&&l&&(K("setVisible",!0),ee.set(!0))};return window.addEventListener("keydown",f),()=>window.removeEventListener("keydown",f)}),e.$$set=f=>{"$$scope"in f&&n(1,r=f.$$scope)},[i,r,s]}class wn extends he{constructor(t){super(),pe(this,t,yn,gn,ie,{})}}const $n=()=>!window.invokeNative,Te=(e,t=0)=>{if($n())for(const n of e)setTimeout(()=>{window.dispatchEvent(new MessageEvent("message",{data:{action:n.action,data:n.data}}))},t)};function Qe(e,t,n){const l=e.slice();return l[4]=t[n],l}function Xe(e,t,n){const l=e.slice();return l[7]=t[n],l}function Ze(e){let t,n=e[1],l=[];for(let i=0;i{n(0,l=!l)},a=>{if(a.custom==!0){a.customFunction();return}Te([{action:a.action,data:a.data}])}]}class Nn extends he{constructor(t){super(),pe(this,t,En,kn,ie,{})}}function Dn(e,t,n){let l;z(e,ee,s=>n(0,l=s)),Te([{action:"setVisible",data:!0}]),Te([{action:"setBrowserMode",data:!0}]);function i(s){s.key==="="&&Mt(ee,l=!0,l)}return be("setBrowserMode",s=>{Se.set(s),console.log("browser mode enabled"),s?window.addEventListener("keydown",i):window.removeEventListener("keydown",i)}),be("newCall",s=>{Fe.update(r=>(r=r||[],r.push(s),r))}),be("setDispatchs",s=>{Be.set(s)}),be("setupUI",s=>{Ue.set(s.player),Pe.set(s.locales),Tt.set(s.keybind),Lt.set(s.maxCallList),Rt.set(s.shortCalls)}),[]}class Mn extends he{constructor(t){super(),pe(this,t,Dn,null,ie,{})}}function Ft(e){const t=e-1;return t*t*t+1}function Ne(e,{delay:t=0,duration:n=400,easing:l=Ft,x:i=0,y:s=0,opacity:r=0}={}){const a=getComputedStyle(e),f=+a.opacity,o=a.transform==="none"?"":a.transform,u=f*(1-r),[c,m]=Ye(i),[p,b]=Ye(s);return{delay:t,duration:n,easing:l,css:(N,$)=>` 5 | transform: ${o} translate(${(1-N)*c}${m}, ${(1-N)*p}${b}); 6 | opacity: ${f-u*$}`}}function tt(e,{delay:t=0,duration:n=400,easing:l=Ft,axis:i="y"}={}){const s=getComputedStyle(e),r=+s.opacity,a=i==="y"?"height":"width",f=parseFloat(s[a]),o=i==="y"?["top","bottom"]:["left","right"],u=o.map(y=>`${y[0].toUpperCase()}${y.slice(1)}`),c=parseFloat(s[`padding${u[0]}`]),m=parseFloat(s[`padding${u[1]}`]),p=parseFloat(s[`margin${u[0]}`]),b=parseFloat(s[`margin${u[1]}`]),N=parseFloat(s[`border${u[0]}Width`]),$=parseFloat(s[`border${u[1]}Width`]);return{delay:t,duration:n,easing:l,css:y=>`overflow: hidden;opacity: ${Math.min(y*20,1)*r};${a}: ${y*f}px;padding-${o[0]}: ${y*c}px;padding-${o[1]}: ${y*m}px;margin-${o[0]}: ${y*p}px;margin-${o[1]}: ${y*b}px;border-${o[0]}-width: ${y*N}px;border-${o[1]}-width: ${y*$}px;`}}const Sn=["January","February","March","April","May","June","July","August","September","October","November","December"];function we(e,t=!1,n=!1){const l=e.getDate(),i=Sn[e.getMonth()],s=e.getFullYear(),r=e.getHours();let a=e.getMinutes();return a<10&&(a=`0${a}`),t?`${t} at ${r}:${a}`:n?`${l}. ${i} at ${r}:${a}`:`${l}. ${i} ${s}. at ${r}:${a}`}function Bt(e){if(!e)return"Unknown";let t;try{t=typeof e=="object"?e:new Date(e)}catch{return"Invalid date"}if(isNaN(t))return"Invalid date";const n=864e5,l=new Date,i=new Date(l-n),s=Math.round((l-t)/1e3),r=Math.round(s/60),a=l.toDateString()===t.toDateString(),f=i.toDateString()===t.toDateString(),o=l.getFullYear()===t.getFullYear();return s<5?"Just Now":s<60?`${s} Seconds ago`:s<90?"A minute ago":r<60?`${r} Minutes ago`:a?we(t,"Today"):f?we(t,"Yesterday"):o?we(t,!1,!0):we(t)}function nt(e,t,n){const l=e.slice();return l[21]=t[n],l}function lt(e,t,n){const l=e.slice();return l[24]=t[n],l}function it(e,t,n){const l=e.slice();return l[27]=t[n],l}function rt(e){let t,n,l=e[6],i=[];for(let r=0;rV(i[r],1,1,()=>{i[r]=null});return{c(){for(let r=0;r0&&ft(e);function U(v,I){return I&320&&(p=null),p==null&&(p=!!Pt(v[21].units,v[8].citizenid)),p?In:An}let D=U(e,-1),k=D(e);function h(){return e[19](e[21])}return{c(){t=g("div"),w&&w.c(),n=M(),l=g("button"),i=g("p"),r=j(s),a=M(),o=j(f),c=M(),m=g("p"),k.c(),N=M(),_(i,"class",u="mx-[2vh] px-[2vh] py-[0.2vh] rounded-full "+(e[21].priority==1?" bg-accent_dark_red":" bg-accent_dark_green")),_(m,"class","ml-[3vh]"),_(l,"class",b="w-full h-[5vh] "+(e[21].priority==1?" bg-priority_quaternary":" bg-accent_green")+" flex items-center font-medium"),_(t,"class","mb-[1vh]")},m(v,I){L(v,t,I),w&&w.m(t,null),d(t,n),d(t,l),d(l,i),d(i,r),d(i,a),d(i,o),d(l,c),d(l,m),k.m(m,null),d(t,N),y=!0,T||(E=Q(l,"click",h),T=!0)},p(v,I){e=v,e[21].units.length>0?w?w.p(e,I):(w=ft(e),w.c(),w.m(t,n)):w&&(w.d(1),w=null),(!y||I&64)&&s!==(s=e[21].units.length+"")&&B(r,s),(!y||I&128)&&f!==(f=e[7].units+"")&&B(o,f),(!y||I&64&&u!==(u="mx-[2vh] px-[2vh] py-[0.2vh] rounded-full "+(e[21].priority==1?" bg-accent_dark_red":" bg-accent_dark_green")))&&_(i,"class",u),D===(D=U(e,I))&&k?k.p(e,I):(k.d(1),k=D(e),k&&(k.c(),k.m(m,null))),(!y||I&64&&b!==(b="w-full h-[5vh] "+(e[21].priority==1?" bg-priority_quaternary":" bg-accent_green")+" flex items-center font-medium"))&&_(l,"class",b)},i(v){y||(te(()=>{y&&($||($=ce(t,tt,{duration:300},!0)),$.run(1))}),y=!0)},o(v){$||($=ce(t,tt,{duration:300},!1)),$.run(0),y=!1},d(v){v&&S(t),w&&w.d(),k.d(),v&&$&&$.end(),T=!1,E()}}}function ft(e){let t,n,l=e[21].units.slice(0,e[1][e[21].id]?e[21].units.length:3),i=[];for(let r=0;r3&&ut(e);return{c(){t=g("div");for(let r=0;r3?s?s.p(r,a):(s=ut(r),s.c(),s.m(t,null)):s&&(s.d(1),s=null)},d(r){r&&S(t),_e(i,r),s&&s.d()}}}function ct(e){let t,n,l=e[24].metadata.callsign+"",i,s,r,a,f=e[24].job.name+"",o,u,c,m,p=e[24].charinfo.firstname+"",b,N,$=e[24].charinfo.lastname+"",y,T;return{c(){t=g("div"),n=g("p"),i=j(l),r=M(),a=g("p"),o=j(f),c=M(),m=g("p"),b=j(p),N=M(),y=j($),_(n,"class",s="ml-[2vh] px-[1.4vh] py-[0.2vh] rounded-full "+(e[21].priority==1?"bg-priority_secondary":"bg-secondary")),_(a,"class",u="mx-[1vh] px-[1.5vh] py-[0.2vh] rounded-full uppercase "+(e[24].job.type=="leo"?"bg-[#004ca5] ":e[24].job.type=="ems"?"bg-[#e03535]":"bg-[#4b4b4b]")),_(m,"class","ml-[0.5vh]"),_(t,"class",T="w-full h-[5vh] flex "+(e[21].priority==1?"bg-priority_tertiary":"bg-tertiary")+" flex items-center font-medium")},m(E,w){L(E,t,w),d(t,n),d(n,i),d(t,r),d(t,a),d(a,o),d(t,c),d(t,m),d(m,b),d(m,N),d(m,y)},p(E,w){w&66&&l!==(l=E[24].metadata.callsign+"")&&B(i,l),w&64&&s!==(s="ml-[2vh] px-[1.4vh] py-[0.2vh] rounded-full "+(E[21].priority==1?"bg-priority_secondary":"bg-secondary"))&&_(n,"class",s),w&66&&f!==(f=E[24].job.name+"")&&B(o,f),w&66&&u!==(u="mx-[1vh] px-[1.5vh] py-[0.2vh] rounded-full uppercase "+(E[24].job.type=="leo"?"bg-[#004ca5] ":E[24].job.type=="ems"?"bg-[#e03535]":"bg-[#4b4b4b]"))&&_(a,"class",u),w&66&&p!==(p=E[24].charinfo.firstname+"")&&B(b,p),w&66&&$!==($=E[24].charinfo.lastname+"")&&B(y,$),w&64&&T!==(T="w-full h-[5vh] flex "+(E[21].priority==1?"bg-priority_tertiary":"bg-tertiary")+" flex items-center font-medium")&&_(t,"class",T)},d(E){E&&S(t)}}}function ut(e){let t,n=!e[1][e[21].id]&&dt(e);return{c(){n&&n.c(),t=x()},m(l,i){n&&n.m(l,i),L(l,t,i)},p(l,i){l[1][l[21].id]?n&&(n.d(1),n=null):n?n.p(l,i):(n=dt(l),n.c(),n.m(t.parentNode,t))},d(l){n&&n.d(l),l&&S(t)}}}function dt(e){let t,n,l,i=pt(e[21])+"",s,r,a=e[7].additionals+"",f,o,u,c;function m(){return e[18](e[21])}return{c(){t=g("button"),n=g("p"),l=j("+"),s=j(i),r=M(),f=j(a),_(n,"class","ml-[0.5vh]"),_(t,"class",o="w-full h-[5vh] flex items-center justify-center "+(e[21].priority==1?"bg-priority_tertiary":"bg-tertiary")+" flex items-center font-medium")},m(p,b){L(p,t,b),d(t,n),d(n,l),d(n,s),d(n,r),d(n,f),u||(c=Q(t,"click",m),u=!0)},p(p,b){e=p,b&64&&i!==(i=pt(e[21])+"")&&B(s,i),b&128&&a!==(a=e[7].additionals+"")&&B(f,a),b&64&&o!==(o="w-full h-[5vh] flex items-center justify-center "+(e[21].priority==1?"bg-priority_tertiary":"bg-tertiary")+" flex items-center font-medium")&&_(t,"class",o)},d(p){p&&S(t),u=!1,c()}}}function An(e){let t=e[7].dispatch_attach+"",n;return{c(){n=j(t)},m(l,i){L(l,n,i)},p(l,i){i&128&&t!==(t=l[7].dispatch_attach+"")&&B(n,t)},d(l){l&&S(n)}}}function In(e){let t=e[7].dispatch_detach+"",n;return{c(){n=j(t)},m(l,i){L(l,n,i)},p(l,i){i&128&&t!==(t=l[7].dispatch_detach+"")&&B(n,t)},d(l){l&&S(n)}}}function _t(e){let t,n,l,i,s=e[21].id+"",r,a,f,o=e[21].code+"",u,c,m,p,b=e[21].message+"",N,$,y,T,E,w,U,D,k,h,v,I,P,G=e[14](e[21]),O=[];for(let C=0;C{A=null}),le())},i(C){v||(F(A),v=!0)},o(C){V(A),v=!1},d(C){C&&S(t),_e(O,C),C&&S(k),A&&A.d(C),C&&S(h),I=!1,P()}}}function Cn(e){let t,n,l,i,s,r,a,f,o,u,c,m,p,b,N,$,y,T,E,w,U,D,k,h,v=e[5]&&rt(e);return{c(){t=g("div"),n=g("div"),l=g("button"),l.innerHTML='',i=M(),s=g("button"),r=g("i"),f=M(),o=g("button"),u=g("i"),m=M(),p=g("button"),p.innerHTML='',b=M(),N=g("button"),$=g("i"),T=M(),E=g("div"),v&&v.c(),_(l,"class","w-full h-[3vh] flex items-center justify-center bg-primary hover:bg-secondary"),_(r,"class",a="fas fa-volume-"+(e[4]?"xmark":"high")+" text-[1.5vh]"),_(s,"class","w-full h-[3vh] flex items-center justify-center bg-primary hover:bg-secondary"),_(u,"class",c="fas fa-"+(e[3]?"bell-slash":"bell")+" text-[1.5vh]"),_(o,"class","w-full h-[3vh] flex items-center justify-center bg-primary hover:bg-secondary"),_(p,"class","w-full h-[3vh] flex items-center justify-center bg-primary hover:bg-secondary"),_($,"class",y="fas fa-"+(e[2]?"hand-point-left":"hand-point-right")+" text-[1.5vh]"),_(N,"class","w-full h-[3vh] flex items-center justify-center bg-primary hover:bg-secondary"),_(n,"class","w-[3.2vh] h-[85%] flex flex-col gap-[1vh]"),J(n,"ml-[1vh]",!e[2]),J(n,"mr-[1vh]",e[2]),_(E,"class","w-[25%] h-[97%] overflow-auto pr-[0.5vh]"),J(E,"ml-[2vh]",!e[2]),J(E,"mr-[2vh]",e[2]),_(t,"class",w="w-screen h-screen flex items-center justify-end "+(e[2]?"flex-row":"flex-row-reverse"))},m(I,P){L(I,t,P),d(t,n),d(n,l),d(n,i),d(n,s),d(s,r),d(n,f),d(n,o),d(o,u),d(n,m),d(n,p),d(n,b),d(n,N),d(N,$),d(t,T),d(t,E),v&&v.m(E,null),D=!0,k||(h=[Q(l,"click",e[15]),Q(s,"click",e[12]),Q(o,"click",e[13]),Q(p,"click",e[16]),Q(N,"click",e[11])],k=!0)},p(I,[P]){e=I,(!D||P&16&&a!==(a="fas fa-volume-"+(e[4]?"xmark":"high")+" text-[1.5vh]"))&&_(r,"class",a),(!D||P&8&&c!==(c="fas fa-"+(e[3]?"bell-slash":"bell")+" text-[1.5vh]"))&&_(u,"class",c),(!D||P&4&&y!==(y="fas fa-"+(e[2]?"hand-point-left":"hand-point-right")+" text-[1.5vh]"))&&_($,"class",y),(!D||P&4)&&J(n,"ml-[1vh]",!e[2]),(!D||P&4)&&J(n,"mr-[1vh]",e[2]),e[5]?v?(v.p(e,P),P&32&&F(v,1)):(v=rt(e),v.c(),F(v,1),v.m(E,null)):v&&(ne(),V(v,1,1,()=>{v=null}),le()),(!D||P&4)&&J(E,"ml-[2vh]",!e[2]),(!D||P&4)&&J(E,"mr-[2vh]",e[2]),(!D||P&4&&w!==(w="w-screen h-screen flex items-center justify-end "+(e[2]?"flex-row":"flex-row-reverse")))&&_(t,"class",w)},i(I){D||(F(v),te(()=>{D&&(U||(U=ce(t,Ne,{x:e[2]?400:-400},!0)),U.run(1))}),D=!0)},o(I){V(v),U||(U=ce(t,Ne,{x:e[2]?400:-400},!1)),U.run(0),D=!1},d(I){I&&S(t),v&&v.d(),I&&U&&U.end(),k=!1,Z(h)}}}function Pt(e,t){for(let n=0;nn(3,i=h)),z(e,qe,h=>n(4,s=h)),z(e,Be,h=>n(5,r=h)),z(e,vn,h=>n(6,a=h)),z(e,Pe,h=>n(7,f=h)),z(e,Ue,h=>n(8,o=h));let u=null,c={},m;Me(()=>{m=Ot.subscribe(h=>{n(2,l=h)})}),Ct(()=>{m()});function p(h){u===h?n(0,u=null):n(0,u=h)}function b(h){n(1,c[h]=!c[h],c)}function N(){n(2,l=!l)}function $(){qe.update(h=>!h),K("toggleMute",{boolean:s})}function y(){Je.update(h=>!h),K("toggleAlerts",{boolean:i})}function T(h){return[{icon:"fas fa-clock",label:"Time",value:Bt(h.time)},{icon:"fas fa-user",label:"Name",value:h.name},{icon:"fas fa-phone",label:"Number",value:h.number},{icon:"fas fa-comment",label:"Information",value:h.information},{icon:"fas fa-map-location-dot",label:"Street",value:h.street},{icon:"fas fa-user",label:"Gender",value:h.gender},{icon:"fas fa-gun",label:"Automatic Gun Fire",value:h.automaticGunFire},{icon:"fas fa-gun",label:"Weapon",value:h.weapon},{icon:"fas fa-car",label:"Vehicle",value:h.vehicle},{icon:"fas fa-rectangle-list",label:"Plate",value:h.plate},{icon:"fas fa-droplet",label:"Color",value:h.color},{icon:"fas fa-car",label:"Class",value:h.class},{icon:"fas fa-door-open",label:"Doors",value:h.doors},{icon:"fas fa-compass",label:"Heading",value:h.heading},{icon:"fas fa-user-group",label:"Units",value:h.units.length}]}const E=()=>{K("refreshAlerts")},w=()=>{K("clearBlips")},U=h=>p(h.id),D=h=>b(h.id),k=h=>{Pt(h.units,o.citizenid)?(K("detachUnit",h),K("refreshAlerts")):(K("attachUnit",h),K("refreshAlerts"))};return n(2,l=!1),[u,c,l,i,s,r,a,f,o,p,b,N,$,y,T,E,w,U,D,k]}class Ln extends he{constructor(t){super(),pe(this,t,jn,Cn,ie,{})}}function ht(e,t,n){const l=e.slice();return l[6]=t[n],l[8]=n,l}function mt(e,t,n){const l=e.slice();return l[9]=t[n],l}function bt(e){let t,n=e[3](e[6]),l=[];for(let i=0;i{O&&(G||(G=ce(n,Ne,{x:t[1]?400:-400},!0)),G.run(1))}),O=!0)},o(C){G||(G=ce(n,Ne,{x:t[1]?400:-400},!1)),G.run(0),O=!1},d(C){C&&S(n),Y&&Y.d(),A&&A.d(),C&&G&&G.end()}}}function Tn(e){let t,n,l=[],i=new Map,s,r,a=e[0].slice().reverse();const f=o=>o[6].data.id;for(let o=0;on(4,l=o)),z(e,Ot,o=>n(1,i=o)),z(e,Tt,o=>n(2,s=o));let r=[];Fe.subscribe(o=>{n(0,r=o||[])});function a(o){bn(o)}Me(()=>{r.forEach(o=>{const{data:u,timer:c}=o;setTimeout(()=>{a(u.id)},c)})}),rn(()=>{r.forEach(o=>{const{data:u,timer:c}=o;setTimeout(()=>{a(u.id)},c)})});function f(o){return l?[{label:"Call",value:o.data.message},{icon:"fas fa-comment",label:"Information",value:o.data.information}]:[{icon:"fas fa-clock",label:"Time",value:Bt(o.data.time)},{icon:"fas fa-user",label:"Name",value:o.data.name},{icon:"fas fa-phone",label:"Number",value:o.data.number},{icon:"fas fa-comment",label:"Information",value:o.data.information},{icon:"fas fa-map-location-dot",label:"Street",value:o.data.street},{icon:"fas fa-user",label:"Gender",value:o.data.gender},{icon:"fas fa-gun",label:"Automatic Gun Fire",value:o.data.automaticGunFire},{icon:"fas fa-gun",label:"Weapon",value:o.data.weapon},{icon:"fas fa-car",label:"Vehicle",value:o.data.vehicle},{icon:"fas fa-rectangle-list",label:"Plate",value:o.data.plate},{icon:"fas fa-droplet",label:"Color",value:o.data.color},{icon:"fas fa-car",label:"Class",value:o.data.class},{icon:"fas fa-door-open",label:"Doors",value:o.data.doors},{icon:"fas fa-compass",label:"Heading",value:o.data.heading}]}return[r,i,s,f]}class Rn extends he{constructor(t){super(),pe(this,t,On,Tn,ie,{})}}function $t(e){let t,n,l,i;return t=new wn({props:{$$slots:{default:[Un]},$$scope:{ctx:e}}}),l=new Rn({}),{c(){ye(t.$$.fragment),n=M(),ye(l.$$.fragment)},m(s,r){ue(t,s,r),L(s,n,r),ue(l,s,r),i=!0},i(s){i||(F(t.$$.fragment,s),F(l.$$.fragment,s),i=!0)},o(s){V(t.$$.fragment,s),V(l.$$.fragment,s),i=!1},d(s){de(t,s),s&&S(n),de(l,s)}}}function Un(e){let t,n;return t=new Ln({}),{c(){ye(t.$$.fragment)},m(l,i){ue(t,l,i),n=!0},i(l){n||(F(t.$$.fragment,l),n=!0)},o(l){V(t.$$.fragment,l),n=!1},d(l){de(t,l)}}}function kt(e){let t,n,l,i;return t=new Nn({}),{c(){ye(t.$$.fragment),n=M(),l=g("body"),_(l,"class","bg-neutral-700")},m(s,r){ue(t,s,r),L(s,n,r),L(s,l,r),i=!0},i(s){i||(F(t.$$.fragment,s),i=!0)},o(s){V(t.$$.fragment,s),i=!1},d(s){de(t,s),s&&S(n),s&&S(l)}}}function Fn(e){let t,n,l,i,s,r=e[0]&&$t(e);n=new Mn({});let a=e[1]&&kt();return{c(){r&&r.c(),t=M(),ye(n.$$.fragment),l=M(),a&&a.c(),i=x()},m(f,o){r&&r.m(f,o),L(f,t,o),ue(n,f,o),L(f,l,o),a&&a.m(f,o),L(f,i,o),s=!0},p(f,[o]){f[0]?r?o&1&&F(r,1):(r=$t(f),r.c(),F(r,1),r.m(t.parentNode,t)):r&&(ne(),V(r,1,1,()=>{r=null}),le()),f[1]?a?o&2&&F(a,1):(a=kt(),a.c(),F(a,1),a.m(i.parentNode,i)):a&&(ne(),V(a,1,1,()=>{a=null}),le())},i(f){s||(F(r),F(n.$$.fragment,f),F(a),s=!0)},o(f){V(r),V(n.$$.fragment,f),V(a),s=!1},d(f){r&&r.d(f),f&&S(t),de(n,f),f&&S(l),a&&a.d(f),f&&S(i)}}}function Bn(e,t,n){let l,i,s;return z(e,je,r=>n(2,l=r)),z(e,Pe,r=>n(0,i=r)),z(e,Se,r=>n(1,s=r)),Mt(je,l="ps-dispatch",l),[i,s]}class Pn extends he{constructor(t){super(),pe(this,t,Bn,Fn,ie,{})}}new Pn({target:document.getElementById("app")}); 7 | -------------------------------------------------------------------------------- /locales/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "dispatch_detach": "Odpojení od dispečerského volání", 3 | "dispatch_attach": "Připojení k dispečerskému volání", 4 | "unit": "Jednotka", 5 | "units": "Jednotky", 6 | "additionals": "Dodatečné jednotky", 7 | 8 | "open_dispatch": "Otevřít Dispatch", 9 | "911_help": "Pošle zprávu policistům.", 10 | "911a_help": "Pošle anonymní zprávu policistům.", 11 | "311_help": "Pošle zprávu doktorům.", 12 | "311a_help": "Pošle anonymní zprávu doktorům.", 13 | 14 | "no_calls": "Nebyly nalezeny žádné zprávy", 15 | "alerts": "Alerty ", 16 | "blips_cleared": "Blipy Vyčištěny", 17 | "alerts_refreshed": "Obnovená upozornění", 18 | "enabled": "Zapnuto", 19 | "disabled": "Vypnuto", 20 | "muted": "Ztlumení", 21 | "unmuted": "Zrušit ztlumení", 22 | "waypoint_set": "Nastavit Wayipont.", 23 | 24 | "caller_local": "Místní", 25 | "call_from": "Zpráva od ", 26 | "two_door": "Dvoudveřové ", 27 | "three_door": "Trojdveřové ", 28 | "four_door": "Čtyřdveřové ", 29 | "compact": "Compact", 30 | "sedan": "Sedan", 31 | "suv": "SUV", 32 | "coupe": "Coupe", 33 | "muscle": "Muscle car", 34 | "sports_classic": "Sports classic", 35 | "sports": "Sports car", 36 | "super": "Super car", 37 | "motorcycle": "Motorka", 38 | "offroad": "Off-road vozidlo", 39 | "industrial": "Industrial vozidlo", 40 | "utility": "Utility vozidlo", 41 | "van": "Van", 42 | "service": "Service vozidlo", 43 | "military": "Vojenské vozidlo", 44 | "truck": "Kamion", 45 | "north": "North Bound", 46 | "east": "East Bound", 47 | "south": "South Bound", 48 | "west": "West Bound", 49 | "male": "Male", 50 | "female": "Female", 51 | 52 | "anon_call": "Příchozí Anonnymní Zpráva", 53 | "anon": "Anononym", 54 | "hidden_number": "Skryté Číslo", 55 | "call": "Příchozí Zpráva", 56 | "vehicleshots": "Střelba z Vozidla", 57 | "shooting": "Střelba ze zbraně", 58 | "melee": "Bitka", 59 | "driveby": "Střelba z auta", 60 | "speeding": "Bezohledná jízda", 61 | "autotheft": "Krádež vozidla", 62 | "persondown": "Zraněná osoba", 63 | "civbled": "Civilian Bled Out", 64 | "storerobbery": "Vykrádání Obchodu", 65 | "fleecabank": "Přepadení banky", 66 | "paletobank": "Přepadení banky", 67 | "pacificbank": "Přepadení banky", 68 | "bobcatsecurity": "Bobcat Bezpečnostní loupež", 69 | "prisonbreak": "Probíhá útěk z vězení", 70 | "vangelico": "Loupežné přepadení klenotnictvý", 71 | "houserobbery": "Vykrádání Domu", 72 | "drugsell": "Podezřelé předání", 73 | "carjacking": "Vykrádání vozidla", 74 | "vehicletheft": "Krádež vozidla", 75 | "officerdown": "Policista postřelen", 76 | "officerdistress": "Policista v nouzi", 77 | "officerbackup": "Policista potřebuje zálohu", 78 | "emsdown": "EMS Down", 79 | "artgalleryrobbery": "Loupež v umělecké galerie", 80 | "humanelabsrobbery": "Loupež v laboratořích Humane Labs", 81 | "trainrobbery": "Přepadení vlaku", 82 | "vanrobbery": "Přepadení bezpečnostní dodávky", 83 | "underground": "Přepadení bunkru", 84 | "drugboatrobbery": "podezřelá loď", 85 | "unionrobbery": "Loupež v Union Depository", 86 | "carboosting": "Car Boosting Probíhá", 87 | "yachtheist": "Probíhá loupež jachty", 88 | "susactivity": "Podezřelá aktivita", 89 | "hunting": "Možné porušení lovu", 90 | "explosion": "Hlášena exploze", 91 | 92 | "justnow": "práve teď", 93 | "minute": "před minutamy", 94 | "hour": "hodiny zpátky", 95 | "day": "dny zpátky" 96 | } 97 | -------------------------------------------------------------------------------- /locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "dispatch_detach": "Vom Einsatz ablösen", 3 | "dispatch_attach": "Zu Einsatz zuordnen", 4 | "unit": "Einheit", 5 | "units": "Einheiten", 6 | "additionals": "Weitere Einheiten", 7 | 8 | "open_dispatch": "Dispatch-Menü öffnen", 9 | "911_help": "Eine Nachricht an die Polizei senden.", 10 | "911a_help": "Sende eine anonyme Nachricht an die Polizei.", 11 | "311_help": "Eine Nachricht an den Rettungsdienst senden.", 12 | "311a_help": "Sende eine anonyme Nachricht an den Rettungsdienst.", 13 | 14 | "no_calls": "Keine Einsätze gefunden.", 15 | "alerts": "Meldungen ", 16 | "blips_cleared": "Blips entfernt", 17 | "alerts_refreshed": "Meldungen aktualisiert", 18 | "enabled": "Aktiviert", 19 | "disabled": "Deaktiviert", 20 | "muted": "Stummgeschaltet", 21 | "unmuted": "Entstummt", 22 | "waypoint_set": "Wegpunkt gesetzt.", 23 | 24 | "caller_local": "Einheimische/r", 25 | "call_from": "Anruf von ", 26 | "two_door": "Zweitürig", 27 | "three_door": "Dreitürig", 28 | "four_door": "Viertürig", 29 | "compact": "Kompaktwagen", 30 | "sedan": "Limousine", 31 | "suv": "SUV", 32 | "coupe": "Coupé", 33 | "muscle": "Muscle Car", 34 | "sports_classic": "Sportklassiker", 35 | "sports": "Sportwagen", 36 | "super": "Supersportwagen", 37 | "motorcycle": "Motorrad", 38 | "offroad": "Geländefahrzeug", 39 | "industrial": "Industriefahrzeug", 40 | "utility": "Nutzfahrzeug", 41 | "van": "Lieferwagen", 42 | "service": "Servicefahrzeug", 43 | "military": "Militärfahrzeug", 44 | "truck": "LKW", 45 | "north": "Nördlich", 46 | "east": "Östlich", 47 | "south": "Südlich", 48 | "west": "Westlich", 49 | "male": "Mann", 50 | "female": "Frau", 51 | 52 | "anon_call": "Anonymer eingehender Anruf", 53 | "anon": "Anonym", 54 | "hidden_number": "Versteckte Nummer", 55 | "call": "Eingehender Anruf", 56 | "vehicleshots": "Schüsse aus einem Fahrzeug", 57 | "shooting": "Schießerei", 58 | "melee": "Schlägerei im Gange", 59 | "driveby": "Drive-by im Gange", 60 | "speeding": "Rücksichtsloses Fahren", 61 | "autotheft": "Diebstahl eines Kraftfahrzeugs", 62 | "persondown": "Person verletzt", 63 | "civbled": "Zivilist verblutet", 64 | "storerobbery": "Ladenraub", 65 | "fleecabank": "Fleeca-Bankraub", 66 | "paletobank": "Paleto-Bankraub", 67 | "pacificbank": "Pacific-Bankraub", 68 | "bobcatsecurity": "Bobcat Security Raubüberfall", 69 | "prisonbreak": "Gefängnisausbruch im Gange", 70 | "vangelico": "Vangelico-Raubüberfall", 71 | "houserobbery": "Hauseinbruch", 72 | "drugsell": "Verdächtige Übergabe", 73 | "carjacking": "Autodiebstahl", 74 | "vehicletheft": "Fahrzeugdiebstahl", 75 | "officerdown": "Officer verletzt", 76 | "officerdistress": "Officer in Not", 77 | "officerbackup": "Officer braucht Verstärkung", 78 | "emsdown": "Rettungsdienst verletzt", 79 | "artgalleryrobbery": "Kunstgalerie-Raubüberfall", 80 | "humanelabsrobbery": "Humane Labs-Raubüberfall", 81 | "trainrobbery": "Zugraubüberfall", 82 | "vanrobbery": "Sicherheitswagen-Raubüberfall", 83 | "underground": "Bunker-Raubüberfall", 84 | "drugboatrobbery": "Verdächtiges Boot", 85 | "unionrobbery": "Union Depository-Raubüberfall", 86 | "carboosting": "Auto-Boosting im Gange", 87 | "yachtheist": "Yacht-Raubüberfall im Gange", 88 | "susactivity": "Verdächtige Aktivität", 89 | "hunting": "Mögliche Jagdverletzung", 90 | "explosion": "Explosion gemeldet", 91 | 92 | "justnow": "gerade eben", 93 | "minute": "vor Minuten", 94 | "hour": "vor Stunden", 95 | "day": "vor Tagen" 96 | } 97 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "dispatch_detach": "Detach from Dispatch Call", 3 | "dispatch_attach": "Attach to Dispatch Call", 4 | "unit": "Unit", 5 | "units": "Units", 6 | "additionals": "Additional Units", 7 | 8 | "open_dispatch": "Open Dispatch Menu", 9 | "911_help": "Send a message to the police.", 10 | "911a_help": "Send an anonymous message to the police.", 11 | "311_help": "Send a message to the EMS.", 12 | "311a_help": "Send an anonymous message to the EMS.", 13 | 14 | "no_calls": "No Calls Found.", 15 | "alerts": "Alerts ", 16 | "blips_cleared": "Blips Cleared", 17 | "alerts_refreshed": "Alerts Refreshed", 18 | "enabled": "Enabled", 19 | "disabled": "Disabled", 20 | "muted": "Muted", 21 | "unmuted": "Unmuted", 22 | "waypoint_set": "Waypoint set.", 23 | 24 | "caller_local": "Local", 25 | "call_from": "Call from ", 26 | "two_door": "Two-door ", 27 | "three_door": "Three-door ", 28 | "four_door": "Four-door ", 29 | "compact": "Compact", 30 | "sedan": "Sedan", 31 | "suv": "SUV", 32 | "coupe": "Coupe", 33 | "muscle": "Muscle car", 34 | "sports_classic": "Sports classic", 35 | "sports": "Sports car", 36 | "super": "Super car", 37 | "motorcycle": "Motorcycle", 38 | "offroad": "Off-road vehicle", 39 | "industrial": "Industrial vehicle", 40 | "utility": "Utility vehicle", 41 | "van": "Van", 42 | "service": "Service vehicle", 43 | "military": "Military vehicle", 44 | "truck": "Truck", 45 | "north": "North Bound", 46 | "east": "East Bound", 47 | "south": "South Bound", 48 | "west": "West Bound", 49 | "male": "Male", 50 | "female": "Female", 51 | 52 | "anon_call": "Incoming Anonymous Call", 53 | "anon": "Anonymous", 54 | "hidden_number": "Hidden Number", 55 | "call": "Incoming Call", 56 | "vehicleshots": "Shots Fired from Vehicle", 57 | "shooting": "Discharge of a firearm", 58 | "melee": "Fight in progress", 59 | "driveby": "Drive-by shooting", 60 | "speeding": "Reckless driving", 61 | "autotheft": "Theft of a motor vehicle", 62 | "persondown": "Person is injured", 63 | "civbled": "Civilian Bled Out", 64 | "storerobbery": "Store Robbery", 65 | "fleecabank": "Fleeca Bank Robbery", 66 | "paletobank": "Paleto Bank Robbery", 67 | "pacificbank": "Pacific Bank Robbery", 68 | "bobcatsecurity": "Bobcat Security Heist", 69 | "prisonbreak": "Prison Break In Progress", 70 | "vangelico": "Vangelico Robbery", 71 | "houserobbery": "House Robbery", 72 | "drugsell": "Suspicious Handoff", 73 | "carjacking": "Car Jacking", 74 | "vehicletheft": "Vehicle Theft", 75 | "officerdown": "Officer is down", 76 | "officerdistress": "Officer in distress", 77 | "officerbackup": "Officer needs backup", 78 | "emsdown": "EMS Down", 79 | "artgalleryrobbery": "Art Gallery Robbery", 80 | "humanelabsrobbery": "Humane Labs Robbery", 81 | "trainrobbery": "Train Robbery", 82 | "vanrobbery": "Security Van Robbery", 83 | "underground": "Bunker Robbery", 84 | "drugboatrobbery": "Suspicious Boat", 85 | "unionrobbery": "Union Depository Robbery", 86 | "carboosting": "Car Boosting In Progress", 87 | "yachtheist": "Yacht Heist In Progress", 88 | "susactivity": "Suspicious Activity", 89 | "hunting": "Possible Hunting Violation", 90 | "explosion": "Explosion Reported", 91 | 92 | "justnow": "just now", 93 | "minute": "minutes ago", 94 | "hour": "hours ago", 95 | "day": "days ago" 96 | } 97 | -------------------------------------------------------------------------------- /locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "dispatch_detach": "Desasignar de la Llamada Dispatch", 3 | "dispatch_attach": "Asignar a la Llamada Dispatch", 4 | "unit": "Unidad", 5 | "units": "Unidades", 6 | "additionals": "Unidades Adicionales", 7 | 8 | "open_dispatch": "Abrir Menú Dispatch", 9 | "911_help": "Enviar un mensaje a la policía.", 10 | "911a_help": "Enviar un mensaje anónimo a la policía.", 11 | "311_help": "Enviar un mensaje al servicio de emergencias.", 12 | "311a_help": "Enviar un mensaje anónimo al servicio de emergencias.", 13 | 14 | "no_calls": "No se encontraron llamadas.", 15 | "alerts": "Alertas ", 16 | "blips_cleared": "Marcadores Eliminados", 17 | "alerts_refreshed": "Alertas Actualizadas", 18 | "enabled": "Habilitado", 19 | "disabled": "Deshabilitado", 20 | "muted": "Silenciado", 21 | "unmuted": "No silenciado", 22 | "waypoint_set": "Establecido punto de ruta.", 23 | 24 | "caller_local": "Local", 25 | "call_from": "Llamada de ", 26 | "two_door": "Vehículo de dos puertas ", 27 | "three_door": "Vehículo de tres puertas ", 28 | "four_door": "Vehículo de cuatro puertas ", 29 | "compact": "Compacto", 30 | "sedan": "Sedán", 31 | "suv": "SUV", 32 | "coupe": "Coupé", 33 | "muscle": "Muscle", 34 | "sports_classic": "Deportivo Clásico", 35 | "sports": "Deportivo", 36 | "super": "Superdeportivo", 37 | "motorcycle": "Motocicleta", 38 | "offroad": "Todoterreno", 39 | "industrial": "Vehículo industrial", 40 | "utility": "Vehículo utilitario", 41 | "van": "Furgoneta", 42 | "service": "Vehículo de servicio", 43 | "military": "Vehículo militar", 44 | "truck": "Camión", 45 | "north": "Dirección norte", 46 | "east": "Dirección este", 47 | "south": "Dirección sur", 48 | "west": "Dirección oeste", 49 | "male": "Masculina", 50 | "female": "Femenina", 51 | 52 | "anon_call": "Llamada Anónima Entrante", 53 | "anon": "Anónimo", 54 | "hidden_number": "Número Oculto", 55 | "call": "Llamada Entrante", 56 | "vehicleshots": "Disparos desde un vehículo", 57 | "shooting": "Disparo de arma de fuego", 58 | "melee": "Pelea informada", 59 | "driveby": "Tiroteo desde un vehículo en movimiento", 60 | "speeding": "Conducción temeraria", 61 | "autotheft": "Robo de un vehículo automotor", 62 | "persondown": "Persona herida", 63 | "civbled": "Civil herido", 64 | "storerobbery": "Robo en una tienda", 65 | "fleecabank": "Robo a Banco Fleeca", 66 | "paletobank": "Robo a Banco de Paleto", 67 | "pacificbank": "Robo a Banco Pacific", 68 | "bobcatsecurity": "Robo en Bobcat Security", 69 | "prisonbreak": "Intento de fuga en prisión", 70 | "vangelico": "Robo a Vangelico", 71 | "houserobbery": "Robo en una casa", 72 | "drugsell": "Entrega sospechosa", 73 | "carjacking": "Robo de vehículo", 74 | "vehicletheft": "Robo de vehículo", 75 | "officerdown": "Oficial caído", 76 | "officerdistress": "Oficial en peligro", 77 | "officerbackup": "oficial necesita refuerzos", 78 | "emsdown": "Servicios Médicos de Emergencia caídos", 79 | "artgalleryrobbery": "Robo en una galería de arte", 80 | "humanelabsrobbery": "Robo en Laboratorios Humane", 81 | "trainrobbery": "Robo de tren", 82 | "vanrobbery": "Robo de furgoneta blindada", 83 | "underground": "Robo en un búnker", 84 | "drugboatrobbery": "Embarcación sospechosa", 85 | "unionrobbery": "Robo en la Depositaría Union", 86 | "carboosting": "Robo de vehículo", 87 | "yachtheist": "Robo en yate", 88 | "susactivity": "Actividad sospechosa", 89 | "hunting": "Posible violación de caza", 90 | "explosion": "Explosión informada", 91 | 92 | "justnow": "justo ahora", 93 | "minute": "minutos atrás", 94 | "hour": "horas atrás", 95 | "day": "días atrás" 96 | } 97 | -------------------------------------------------------------------------------- /locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "dispatch_detach": "Détacher de l'appel du dispatch", 3 | "dispatch_attach": "Attacher à l'appel du dispatch", 4 | "unit": "Unité", 5 | "units": "Unités", 6 | "additionals": "Unités additionnelles", 7 | 8 | "open_dispatch": "Ouvrir le menu du dispatch", 9 | "911_help": "Envoyer un message à la police.", 10 | "911a_help": "Envoyer un message anonyme à la police.", 11 | "311_help": "Envoyer un message au service médical d'urgence (EMS).", 12 | "311a_help": "Envoyer un message anonyme au service médical d'urgence (EMS).", 13 | 14 | "no_calls": "Aucun appel trouvé.", 15 | "alerts": "Alertes", 16 | "blips_cleared": "Marqueurs effacés", 17 | "alerts_refreshed": "Alertes rafraîchies", 18 | "enabled": "Activé", 19 | "disabled": "Désactivé", 20 | "muted": "Muet", 21 | "unmuted": "Non muet", 22 | "waypoint_set": "Waypoint défini.", 23 | 24 | "caller_local": "Local", 25 | "call_from": "Appel de", 26 | "two_door": "Véhicule 2 portes", 27 | "three_door": "Véhicule 3 portes", 28 | "four_door": "Véhicule 4 portes", 29 | "compact": "Véhicule compact", 30 | "sedan": "Berline", 31 | "suv": "SUV", 32 | "coupe": "Coupé", 33 | "muscle": "Grosse cylindrée", 34 | "sports_classic": "Voiture de sport classique", 35 | "sports": "Voiture de sport", 36 | "super": "Supercar", 37 | "motorcycle": "Motos", 38 | "offroad": "Véhicule tout-terrain", 39 | "industrial": "Véhicule industriel", 40 | "utility": "Véhicule utilitaire", 41 | "van": "Van", 42 | "service": "Véhicule de service", 43 | "military": "Véhicule militaire", 44 | "truck": "Camion", 45 | "north": "Direction Nord", 46 | "east": "Direction Est", 47 | "south": "Direction Sud", 48 | "west": "Direction Ouest", 49 | "male": "Homme", 50 | "female": "Femme", 51 | 52 | "anon_call": "Appel anonyme entrant", 53 | "anon": "Anonyme", 54 | "hidden_number": "Numéro masqué", 55 | "call": "Appel entrant", 56 | "vehicleshots": "Coups de feu depuis un véhicule", 57 | "shooting": "Fusillade", 58 | "melee": "Bagarre en cours", 59 | "driveby": "Tir depuis un véhicule en mouvement", 60 | "speeding": "Conduite imprudente", 61 | "autotheft": "Vol de véhicule", 62 | "persondown": "Personne blessée", 63 | "civbled": "Civil blessé", 64 | "storerobbery": "Braquage de magasin", 65 | "fleecabank": "Braquage de la banque Fleeca", 66 | "paletobank": "Braquage de la banque Paleto", 67 | "pacificbank": "Braquage de la banque Pacific", 68 | "bobcatsecurity": "Vol de Bobcat Security", 69 | "prisonbreak": "Évasion de prison en cours", 70 | "vangelico": "Braquage de Vangelico", 71 | "houserobbery": "Cambriolage de maison", 72 | "drugsell": "Transaction suspecte", 73 | "carjacking": "Carjacking", 74 | "vehicletheft": "Vol de véhicule", 75 | "officerdown": "Officier blessé", 76 | "officerdistress": "Officier en détresse", 77 | "officerbackup": "Officier a besoin de renforts", 78 | "emsdown": "EMS blessé", 79 | "artgalleryrobbery": "Braquage de galerie d'art", 80 | "humanelabsrobbery": "Braquage des laboratoires Humane", 81 | "trainrobbery": "Braquage de train", 82 | "vanrobbery": "Braquage de fourgon blindé", 83 | "underground": "Braquage de bunker", 84 | "drugboatrobbery": "Bateau suspect", 85 | "unionrobbery": "Braquage de la banque Union Depository", 86 | "carboosting": "Vol de voiture en cours", 87 | "yachtheist": "Cambriolage de yacht en cours", 88 | "susactivity": "Activité suspecte", 89 | "hunting": "Violation possible de la chasse", 90 | "explosion": "Explosion signalée", 91 | 92 | "justnow": "à l'instant", 93 | "minute": "minute(s) auparavant", 94 | "hour": "heure(s) auparavant", 95 | "day": "jour(s) auparavant" 96 | } 97 | -------------------------------------------------------------------------------- /locales/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "dispatch_detach": "Loskoppelen van Dispatch Oproep", 3 | "dispatch_attach": "Verbinden met Dispatch Oproep", 4 | "unit": "Eenheid", 5 | "units": "Eenheden", 6 | "additionals": "Extra Eenheden", 7 | 8 | "open_dispatch": "Open Dispatch Menu", 9 | "911_help": "Stuur een bericht naar de politie.", 10 | "911a_help": "Stuur een anoniem bericht naar de politie.", 11 | "311_help": "Stuur een bericht naar de ambulance.", 12 | "311a_help": "Stuur een anoniem bericht naar de ambulance.", 13 | 14 | "no_calls": "Geen Oproepen Gevonden.", 15 | "alerts": "Meldingen", 16 | "blips_cleared": "Blips Gewist", 17 | "alerts_refreshed": "Meldingen herladen", 18 | "enabled": "Ingeschakeld", 19 | "disabled": "Uitgeschakeld", 20 | "muted": "Gedempt", 21 | "unmuted": "Niet Gedempt", 22 | "waypoint_set": "Gps ingesteld", 23 | 24 | "caller_local": "Burger", 25 | "call_from": "Oproep van ", 26 | "two_door": "Tweedeurs ", 27 | "three_door": "Driedeurs ", 28 | "four_door": "Vierdeurs ", 29 | "compact": "Compact", 30 | "sedan": "Sedan", 31 | "suv": "SUV", 32 | "coupe": "Coupé", 33 | "muscle": "Muscle car", 34 | "sports_classic": "Klassieke Sportauto", 35 | "sports": "Sportauto", 36 | "super": "Supercar", 37 | "motorcycle": "Motor", 38 | "offroad": "Off-road voertuig", 39 | "industrial": "Industrieel voertuig", 40 | "utility": "Dienstvoertuig", 41 | "van": "Bestelwagen", 42 | "service": "Servicevoertuig", 43 | "military": "Militair voertuig", 44 | "truck": "Vrachtwagen", 45 | "north": "Noordwaarts", 46 | "east": "Oostwaarts", 47 | "south": "Zuidwaarts", 48 | "west": "Westwaarts", 49 | "male": "Mannelijk", 50 | "female": "Vrouwelijk", 51 | 52 | "anon_call": "Inkomende Anonieme Oproep", 53 | "anon": "Anoniem", 54 | "hidden_number": "Verborgen Nummer", 55 | "call": "Inkomende Oproep", 56 | "vehicleshots": "Schoten Afgevuurd vanuit Voertuig", 57 | "shooting": "Schietpartij", 58 | "melee": "Vechtpartij gaande", 59 | "driveby": "Drive-by schietpartij", 60 | "speeding": "Roekeloos rijgedrag", 61 | "autotheft": "Voertuig diefstal", 62 | "persondown": "Persoon is gewond", 63 | "civbled": "Bloedverlies", 64 | "storerobbery": "Winkeloverval", 65 | "fleecabank": "Fleeca Bankoverval", 66 | "paletobank": "Paleto Bankoverval", 67 | "pacificbank": "Pacific Bankoverval", 68 | "prisonbreak": "Gevangenisuitbraak gaande", 69 | "bobcatsecurity": "Bobcat overval word gepleegd", 70 | "vangelico": "Vangelico Overval", 71 | "houserobbery": "Huisoverval", 72 | "drugsell": "Verdachte Overdracht", 73 | "carjacking": "Auto-Ontvoering", 74 | "vehicletheft": "Voertuigdiefstal", 75 | "officerdown": "Agent neer", 76 | "officerdistress": "Agent in nood", 77 | "officerbackup": "Agent heeft hulp nodig", 78 | "emsdown": "Ambulancepersoneel neer", 79 | "artgalleryrobbery": "Kunstgalerijoverval", 80 | "humanelabsrobbery": "Humane Labs Overval", 81 | "trainrobbery": "Treinoverval", 82 | "vanrobbery": "Beveiligingswagenoverval", 83 | "underground": "Bunkeroverval", 84 | "drugboatrobbery": "Verdacht Bootgedrag", 85 | "unionrobbery": "Union Depository Overval", 86 | "carboosting": "Auto Diefstal Gaande", 87 | "yachtheist": "Jachtheist Gaande", 88 | "susactivity": "Verdachte Activiteit", 89 | "hunting": "Mogelijke Jacht Overtreding", 90 | "explosion": "Explosie Gemeld", 91 | 92 | "justnow": "zojuist", 93 | "minute": "minuten geleden", 94 | "hour": "uren geleden", 95 | "day": "dagen geleden" 96 | } 97 | -------------------------------------------------------------------------------- /locales/pt-br.json: -------------------------------------------------------------------------------- 1 | { 2 | "dispatch_detach": "Desanexar do Chamdo do Dispatch", 3 | "dispatch_attach": "Anexar a Chamado do Dispatch", 4 | "unit": "Unidade", 5 | "units": "Unidades", 6 | "additionals": "Unidades adicionais", 7 | 8 | "open_dispatch": "Abra o menu de Dispatch", 9 | "911_help": "Envie uma mensagem para a polícia.", 10 | "911a_help": "Envie uma mensagem anônima para a polícia.", 11 | "311_help": "Envie uma mensagem para o EMS.", 12 | "311a_help": "Envie uma mensagem anônima para o EMS.", 13 | 14 | "no_calls": "Nenhuma chamada encontrada.", 15 | "alerts": "Alertas ", 16 | "blips_cleared": "Blips Limpos", 17 | "enabled": "Habilitada", 18 | "disabled": "Desabilitada", 19 | "muted": "Silenciada", 20 | "unmuted": "Desmutado", 21 | 22 | "caller_local": "Local", 23 | "call_from": "Chamado de ", 24 | "two_door": "Duas Portas ", 25 | "three_door": "Três Portas ", 26 | "four_door": "Quatro Portas ", 27 | "compact": "Compacto", 28 | "sedan": "Sedãn", 29 | "suv": "SUV", 30 | "coupe": "Coupê", 31 | "muscle": "Muscle", 32 | "sports_classic": "Esportivo Clássico", 33 | "sports": "Esportivo", 34 | "super": "Super", 35 | "motorcycle": "Motocicleta", 36 | "offroad": "Veículo Off-Road", 37 | "industrial": "Veículo Industrial", 38 | "utility": "Veículo Utilitário", 39 | "van": "Van", 40 | "service": "Veículo de Serviço", 41 | "military": "Veículo Militar", 42 | "truck": "Caminhão", 43 | "north": "Sentido Norte", 44 | "east": "Sentido Leste", 45 | "south": "Sentido Sul", 46 | "west": "Sentido Oeste", 47 | "male": "Macho", 48 | "female": "Fêmea", 49 | 50 | "anon_call": "Chamada Anônima Recebida", 51 | "anon": "Anônima", 52 | "hidden_number": "Número oculto", 53 | "call": "Chamada recebida", 54 | "vehicleshots": "Tiros disparados do veículo", 55 | "shooting": "Descarga de uma arma de fogo", 56 | "melee": "Lutar em andamento", 57 | "driveby": "Tiroteio", 58 | "speeding": "Condução imprudente", 59 | "autotheft": "Roubo de um veículo a motor", 60 | "persondown": "Pessoa está ferida", 61 | "civbled": "Civil sangrando", 62 | "storerobbery": "Roubo a Loja", 63 | "fleecabank": "Roubo ao Fleeca Bank", 64 | "paletobank": "Roubo ao Paleto Bank", 65 | "pacificbank": "Roubo ao Pacific Bank", 66 | "bobcatsecurity": "Roubo ao Bobcat Security", 67 | "prisonbreak": "Fuga da Prisão", 68 | "vangelico": "Roubo a Joalheria", 69 | "houserobbery": "Assalto a casa", 70 | "drugsell": "Venda de Drogas", 71 | "carjacking": "Arrombamento de Veículo", 72 | "vehicletheft": "Roubo de veículo", 73 | "officerdown": "Oficial Caído", 74 | "officerdistress": "Oficial em angústia", 75 | "officerbackup": "Oficial precisa de reforços", 76 | "emsdown": "EMS Caído", 77 | "artgalleryrobbery": "Roubo da galeria de arte", 78 | "humanelabsrobbery": "Roubo de laboratórios Humane", 79 | "trainrobbery": "Assalto a trem", 80 | "vanrobbery": "Roubo a Van", 81 | "underground": "Roubo de Bunker", 82 | "drugboatrobbery": "Barco suspeito", 83 | "unionrobbery": "Roubo de depósito da União", 84 | "carboosting": "Boosting em andamento", 85 | "yachtheist": "Assalto de iate em andamento", 86 | "susactivity": "Atividade suspeita", 87 | "hunting": "Possível violação de caça", 88 | "explosion": "Explosão relatada", 89 | "carheist": "Roubo a Aeronave", 90 | 91 | "justnow": "agora mesmo", 92 | "minute": "minutos atrás", 93 | "hour": "horas atrás", 94 | "day": "dias atrás" 95 | } 96 | -------------------------------------------------------------------------------- /locales/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "dispatch_detach": "Çağrıdan Ayrıl", 3 | "dispatch_attach": "Çağrıya Katıl", 4 | "unit": "Birim", 5 | "units": "Birimler", 6 | "additionals": "Ek Birimler", 7 | 8 | "open_dispatch": "Çağrı Menüsünü Aç", 9 | "911_help": "Polise mesaj gönder.", 10 | "911a_help": "Polise anonim mesaj gönder.", 11 | "311_help": "EMS'e mesaj gönder.", 12 | "311a_help": "EMS'e anonim mesaj gönder.", 13 | 14 | "no_calls": "Çağrı Bulunamadı.", 15 | "alerts": "Uyarılar ", 16 | "blips_cleared": "İşaretler Temizlendi", 17 | "alerts_refreshed": "Uyarılar Yenilendi", 18 | "enabled": "Açıldı", 19 | "disabled": "Kapatıldı", 20 | "muted": "Susturuldu", 21 | "unmuted": "Susturma Açıldı", 22 | "waypoint_set": "İşaret ayarlandı.", 23 | 24 | "caller_local": "Yerel", 25 | "call_from": " tarafından çağrı", 26 | "two_door": "İki kapı ", 27 | "three_door": "Üç kapı ", 28 | "four_door": "Dört kapı ", 29 | "compact": "Kompakt", 30 | "sedan": "Sedan", 31 | "suv": "SUV", 32 | "coupe": "Coupe", 33 | "muscle": "Muscle Araç", 34 | "sports_classic": "Spor Klasik", 35 | "sports": "Spor Araç", 36 | "super": "Süper Araç", 37 | "motorcycle": "Motosiklet", 38 | "offroad": "Off-Road Araç", 39 | "industrial": "Endüstriyel Araç", 40 | "utility": "Kamu Aracı", 41 | "van": "Van", 42 | "service": "Kamu Aracı", 43 | "military": "Askeri Araç", 44 | "truck": "Tır", 45 | "north": "Kuzey Sınırı", 46 | "east": "Doğu Sınırı", 47 | "south": "Güney Sınırı", 48 | "west": "Batı Sınırı", 49 | "male": "Erkek", 50 | "female": "Kadın", 51 | 52 | "anon_call": "Gelen Anonim Çağrı", 53 | "anon": "Anonim", 54 | "hidden_number": "Gizli Numara", 55 | "call": "Gelen İhbar", 56 | "vehicleshots": "Araçtan Silah Sıkıldı", 57 | "shooting": "Ateşli Silah Sıkıldı", 58 | "melee": "Kavga", 59 | "driveby": "Drive-by Ateşli Silah Sıkıldı", 60 | "speeding": "Tehlikeli Sürüş", 61 | "autotheft": "Motor Hırsızlığı", 62 | "persondown": "Sivil Yaralandı", 63 | "civbled": "Sivil Kan Kaybediyor", 64 | "storerobbery": "Mağaza Soygunu", 65 | "fleecabank": "Fleeca Bankası Soygunu", 66 | "paletobank": "Paleto Bankası Soygunu", 67 | "pacificbank": "Pacific Merkez Bankası Soygunu", 68 | "bobcatsecurity": "Bobcat Soygunu", 69 | "prisonbreak": "Hapishaneden Firar Ediliyor", 70 | "vangelico": "Kuyumcu Soygunu", 71 | "houserobbery": "Ev Soygunu", 72 | "drugsell": "Şüpheli Satış", 73 | "carjacking": "Araç Soygunu", 74 | "vehicletheft": "Araç Hırsızlığı", 75 | "officerdown": "Memur Yaralandı", 76 | "officerdistress": "Memur Zor Durumda", 77 | "officerbackup": "Memur Yardım Talep Ediyor", 78 | "emsdown": "EMS Yaralandı", 79 | "artgalleryrobbery": "Sanat Eseri Soygunu", 80 | "humanelabsrobbery": "Humane Laboratuvarı Soygunu", 81 | "trainrobbery": "Tren Soygunu", 82 | "vanrobbery": "Güvenlik Aracı Soygunu", 83 | "underground": "Yeraltı Sığınağı Soygunu", 84 | "drugboatrobbery": "Şüpheli Bot", 85 | "unionrobbery": "Union Rezerv Bankası Soygunu", 86 | "carboosting": "Araç Yükseltmesi Yapıldı", 87 | "yachtheist": "Yat Soygunu", 88 | "susactivity": "Şüpheli Aktivite", 89 | "hunting": "Olası Avlanma İhlali", 90 | "explosion": "Patlama Rapor Edildi", 91 | 92 | "justnow": "şuan", 93 | "minute": "dakika önce", 94 | "hour": "saat önce", 95 | "day": "gün önce" 96 | } -------------------------------------------------------------------------------- /server/main.lua: -------------------------------------------------------------------------------- 1 | local calls = {} 2 | local callCount = 0 3 | 4 | -- Functions 5 | exports('GetDispatchCalls', function() 6 | return calls 7 | end) 8 | 9 | -- Events 10 | RegisterServerEvent('ps-dispatch:server:notify', function(data) 11 | callCount = callCount + 1 12 | data.id = callCount 13 | data.time = os.time() * 1000 14 | data.units = {} 15 | data.responses = {} 16 | 17 | if #calls > 0 then 18 | if calls[#calls] == data then 19 | return 20 | end 21 | end 22 | 23 | if #calls >= Config.MaxCallList then 24 | table.remove(calls, 1) 25 | end 26 | 27 | calls[#calls + 1] = data 28 | 29 | TriggerClientEvent('ps-dispatch:client:notify', -1, data) 30 | end) 31 | 32 | RegisterServerEvent('ps-dispatch:server:attach', function(id, player) 33 | for i=1, #calls do 34 | if calls[i]['id'] == id then 35 | for j = 1, #calls[i]['units'] do 36 | if calls[i]['units'][j]['citizenid'] == player.citizenid then 37 | return 38 | end 39 | end 40 | calls[i]['units'][#calls[i]['units'] + 1] = player 41 | return 42 | end 43 | end 44 | end) 45 | 46 | RegisterServerEvent('ps-dispatch:server:detach', function(id, player) 47 | for i = #calls, 1, -1 do 48 | if calls[i]['id'] == id then 49 | if calls[i]['units'] and (#calls[i]['units'] or 0) > 0 then 50 | for j = #calls[i]['units'], 1, -1 do 51 | if calls[i]['units'][j]['citizenid'] == player.citizenid then 52 | table.remove(calls[i]['units'], j) 53 | end 54 | end 55 | end 56 | return 57 | end 58 | end 59 | end) 60 | 61 | -- Callbacks 62 | lib.callback.register('ps-dispatch:callback:getLatestDispatch', function(source) 63 | return calls[#calls] 64 | end) 65 | 66 | lib.callback.register('ps-dispatch:callback:getCalls', function(source) 67 | return calls 68 | end) 69 | 70 | -- Commands 71 | lib.addCommand('dispatch', { 72 | help = locale('open_dispatch') 73 | }, function(source, raw) 74 | TriggerClientEvent("ps-dispatch:client:openMenu", source, calls) 75 | end) 76 | 77 | lib.addCommand('911', { 78 | help = 'Send a message to 911', 79 | params = { { name = 'message', type = 'string', help = '911 Message' }}, 80 | }, function(source, args, raw) 81 | local fullMessage = raw:sub(5) 82 | TriggerClientEvent('ps-dispatch:client:sendEmergencyMsg', source, fullMessage, "911", false) 83 | end) 84 | lib.addCommand('911a', { 85 | help = 'Send an anonymous message to 911', 86 | params = { { name = 'message', type = 'string', help = '911 Message' }}, 87 | }, function(source, args, raw) 88 | local fullMessage = raw:sub(5) 89 | TriggerClientEvent('ps-dispatch:client:sendEmergencyMsg', source, fullMessage, "911", true) 90 | end) 91 | 92 | lib.addCommand('311', { 93 | help = 'Send a message to 311', 94 | params = { { name = 'message', type = 'string', help = '311 Message' }}, 95 | }, function(source, args, raw) 96 | local fullMessage = raw:sub(5) 97 | TriggerClientEvent('ps-dispatch:client:sendEmergencyMsg', source, fullMessage, "311", false) 98 | end) 99 | 100 | lib.addCommand('311a', { 101 | help = 'Send an anonymous message to 311', 102 | params = { { name = 'message', type = 'string', help = '311 Message' }}, 103 | }, function(source, args, raw) 104 | local fullMessage = raw:sub(5) 105 | TriggerClientEvent('ps-dispatch:client:sendEmergencyMsg', source, fullMessage, "311", true) 106 | end) 107 | 108 | -------------------------------------------------------------------------------- /shared/config.lua: -------------------------------------------------------------------------------- 1 | Config = Config or {} 2 | 3 | Config.ShortCalls = false -- Dispatch notifications are sent containing only the alert name, omitting additional details. For more information, the dispatch menu can be accessed. 4 | Config.Debug = false -- Enables debug and send alerts when leo break the law. 5 | 6 | Config.RespondKeybind = 'E' 7 | Config.OpenDispatchMenu = 'O' 8 | Config.AlertTime = 5 -- Specify the duration for the alert to appear on the screen. The default time is 5 seconds for all alerts. To set a different duration for specific alerts, change the value in `alertTime = nil` found in the alerts.lua file. 9 | 10 | Config.MaxCallList = 25 -- maximum dispatch calls in dispatch list 11 | Config.OnDutyOnly = true -- Set true if only on duty players can see the alert 12 | Config.Jobs = { -- Job Types or names that can access the dispatch menu. If you want to allow more jobs to see certain dispatch alerts. Go to alerts.lua and add the job name to the alert. 13 | "leo", 14 | "ems" 15 | } 16 | 17 | Config.AlertCommandCooldown = 60 -- this would make the command work every 60 seconds to avoid spamming 18 | 19 | Config.DefaultAlertsDelay = 5 -- Delay between each default alert, prevent spamming 20 | Config.DefaultAlerts = { 21 | Speeding = true, 22 | Shooting = true, 23 | Autotheft = true, 24 | Melee = true, 25 | PlayerDowned = true, 26 | Explosion = true 27 | } 28 | 29 | Config.MinOffset = 1 30 | Config.MaxOffset = 120 31 | 32 | Config.PhoneRequired = true -- Set true if only can use 911/311 command when got a phone on inventory. 33 | Config.PhoneItems = { -- Add the entire list of your phone items. 34 | "phone", 35 | } 36 | 37 | -- Locations for the Hunting Zones and No Dispatch Zones( Label: Name of Blip // Radius: Radius of the Alert and Blip) 38 | Config.EnableHuntingBlip = true 39 | 40 | Config.Locations = { 41 | ["HuntingZones"] = { 42 | [1] = {label = "Hunting Zone", radius = 650.0, coords = vector3(-938.61, 4823.99, 313.92)}, 43 | }, 44 | ["NoDispatchZones"] = { 45 | [1] = {label = "Ammunation 1", coords = vector3(13.53, -1097.92, 29.8), length = 14.0, width = 5.0, heading = 70, minZ = 28.8, maxZ = 32.8}, 46 | [2] = {label = "Ammunation 2", coords = vector3(821.96, -2163.09, 29.62), length = 14.0, width = 5.0, heading = 270, minZ = 28.62, maxZ = 32.62}, 47 | }, 48 | } 49 | 50 | -- Whitelist Guns that do not send shooting alerts 51 | Config.WeaponWhitelist = { 52 | 'WEAPON_GRENADE', 53 | 'WEAPON_BZGAS', 54 | 'WEAPON_MOLOTOV', 55 | 'WEAPON_STICKYBOMB', 56 | 'WEAPON_PROXMINE', 57 | 'WEAPON_SNOWBALL', 58 | 'WEAPON_PIPEBOMB', 59 | 'WEAPON_BALL', 60 | 'WEAPON_SMOKEGRENADE', 61 | 'WEAPON_FLARE', 62 | 'WEAPON_PETROLCAN', 63 | 'WEAPON_FIREEXTINGUISHER', 64 | 'WEAPON_HAZARDCAN', 65 | 'WEAPON_RAYCARBINE', 66 | 'WEAPON_STUNGUN' 67 | } 68 | 69 | Config.Blips = { 70 | ['vehicleshots'] = { -- Need to match the codeName in alerts.lua 71 | radius = 0, 72 | sprite = 119, 73 | color = 1, 74 | scale = 1.5, 75 | length = 2, 76 | sound = 'Lose_1st', 77 | sound2 = 'GTAO_FM_Events_Soundset', 78 | offset = false, 79 | flash = false 80 | }, 81 | ['shooting'] = { 82 | radius = 0, 83 | sprite = 110, 84 | color = 1, 85 | scale = 1.5, 86 | length = 2, 87 | sound = 'Lose_1st', 88 | sound2 = 'GTAO_FM_Events_Soundset', 89 | offset = false, 90 | flash = false 91 | }, 92 | ['speeding'] = { 93 | radius = 0, 94 | sprite = 326, 95 | color = 84, 96 | scale = 1.5, 97 | length = 2, 98 | sound = 'Lose_1st', 99 | sound2 = 'GTAO_FM_Events_Soundset', 100 | offset = false, 101 | flash = false 102 | }, 103 | ['fight'] = { 104 | radius = 0, 105 | sprite = 685, 106 | color = 69, 107 | scale = 1.5, 108 | length = 2, 109 | sound = 'Lose_1st', 110 | sound2 = 'GTAO_FM_Events_Soundset', 111 | offset = false, 112 | flash = false 113 | }, 114 | ['civdown'] = { 115 | radius = 0, 116 | sprite = 126, 117 | color = 3, 118 | scale = 1.5, 119 | length = 2, 120 | sound = 'dispatch', 121 | offset = false, 122 | flash = false 123 | }, 124 | ['civdead'] = { 125 | radius = 0, 126 | sprite = 126, 127 | color = 3, 128 | scale = 1.5, 129 | length = 2, 130 | sound = 'dispatch', 131 | offset = false, 132 | flash = false 133 | }, 134 | ['911call'] = { 135 | radius = 0, 136 | sprite = 480, 137 | color = 1, 138 | scale = 1.5, 139 | length = 2, 140 | sound = 'Lose_1st', 141 | sound2 = 'GTAO_FM_Events_Soundset', 142 | offset = false, 143 | flash = false 144 | }, 145 | ['311call'] = { 146 | radius = 0, 147 | sprite = 480, 148 | color = 3, 149 | scale = 1.5, 150 | length = 2, 151 | sound = 'Lose_1st', 152 | sound2 = 'GTAO_FM_Events_Soundset', 153 | offset = false, 154 | flash = false 155 | }, 156 | ['officerdown'] = { 157 | radius = 15.0, 158 | sprite = 526, 159 | color = 1, 160 | scale = 1.5, 161 | length = 2, 162 | sound = 'panicbutton', 163 | offset = false, 164 | flash = true 165 | }, 166 | ['officerbackup'] = { 167 | radius = 15.0, 168 | sprite = 526, 169 | color = 1, 170 | scale = 1.5, 171 | length = 2, 172 | sound = 'panicbutton', 173 | offset = false, 174 | flash = true 175 | }, 176 | ['officerdistress'] = { 177 | radius = 15.0, 178 | sprite = 526, 179 | color = 1, 180 | scale = 1.5, 181 | length = 2, 182 | sound = 'panicbutton', 183 | offset = false, 184 | flash = true 185 | }, 186 | ['emsdown'] = { 187 | radius = 15.0, 188 | sprite = 526, 189 | color = 3, 190 | scale = 1.5, 191 | length = 2, 192 | sound = 'panicbutton', 193 | offset = false, 194 | flash = false 195 | }, 196 | ['hunting'] = { 197 | radius = 0, 198 | sprite = 141, 199 | color = 2, 200 | scale = 1.5, 201 | length = 2, 202 | sound = 'Lose_1st', 203 | sound2 = 'GTAO_FM_Events_Soundset', 204 | offset = false, 205 | flash = false 206 | }, 207 | ['storerobbery'] = { 208 | radius = 0, 209 | sprite = 52, 210 | color = 1, 211 | scale = 1.5, 212 | length = 2, 213 | sound = 'Lose_1st', 214 | sound2 = 'GTAO_FM_Events_Soundset', 215 | offset = false, 216 | flash = false 217 | }, 218 | ['bankrobbery'] = { 219 | radius = 0, 220 | sprite = 500, 221 | color = 2, 222 | scale = 1.5, 223 | length = 2, 224 | sound = 'robberysound', 225 | offset = false, 226 | flash = false 227 | }, 228 | ['paletobankrobbery'] = { 229 | radius = 0, 230 | sprite = 500, 231 | color = 12, 232 | scale = 1.5, 233 | length = 2, 234 | sound = 'robberysound', 235 | offset = false, 236 | flash = false 237 | }, 238 | ['pacificbankrobbery'] = { 239 | radius = 0, 240 | sprite = 500, 241 | color = 5, 242 | scale = 1.5, 243 | length = 2, 244 | sound = 'robberysound', 245 | offset = false, 246 | flash = false 247 | }, 248 | ['bobcatsecurityheist'] = { 249 | radius = 0, 250 | sprite = 500, 251 | color = 5, 252 | scale = 1.5, 253 | length = 2, 254 | sound = 'robberysound', 255 | offset = false, 256 | flash = false 257 | }, 258 | ['prisonbreak'] = { 259 | radius = 0, 260 | sprite = 189, 261 | color = 59, 262 | scale = 1.5, 263 | length = 2, 264 | sound = 'robberysound', 265 | offset = false, 266 | flash = false 267 | }, 268 | ['vangelicorobbery'] = { 269 | radius = 0, 270 | sprite = 434, 271 | color = 5, 272 | scale = 1.5, 273 | length = 2, 274 | sound = 'robberysound', 275 | offset = false, 276 | flash = false 277 | }, 278 | ['houserobbery'] = { 279 | radius = 0, 280 | sprite = 40, 281 | color = 5, 282 | scale = 1.5, 283 | length = 2, 284 | sound = 'Lose_1st', 285 | sound2 = 'GTAO_FM_Events_Soundset', 286 | offset = false, 287 | flash = false 288 | }, 289 | ['suspicioushandoff'] = { 290 | radius = 120.0, 291 | sprite = 469, 292 | color = 52, 293 | scale = 0, 294 | length = 2, 295 | sound = 'Lose_1st', 296 | sound2 = 'GTAO_FM_Events_Soundset', 297 | offset = true, 298 | flash = false 299 | }, 300 | ['yachtheist'] = { 301 | radius = 0, 302 | sprite = 455, 303 | color = 60, 304 | scale = 1.5, 305 | length = 2, 306 | sound = 'robberysound', 307 | offset = false, 308 | flash = false 309 | }, 310 | ['vehicletheft'] = { 311 | radius = 0, 312 | sprite = 595, 313 | color = 60, 314 | scale = 1.5, 315 | length = 2, 316 | sound = 'Lose_1st', 317 | sound2 = 'GTAO_FM_Events_Soundset', 318 | offset = false, 319 | flash = false 320 | }, 321 | ['signrobbery'] = { 322 | radius = 0, 323 | sprite = 358, 324 | color = 60, 325 | scale = 1.5, 326 | length = 2, 327 | sound = 'Lose_1st', 328 | sound2 = 'GTAO_FM_Events_Soundset', 329 | offset = false, 330 | flash = false 331 | }, 332 | ['susactivity'] = { 333 | radius = 0, 334 | sprite = 66, 335 | color = 37, 336 | scale = 0.5, 337 | length = 2, 338 | sound = 'Lose_1st', 339 | sound2 = 'GTAO_FM_Events_Soundset', 340 | offset = false, 341 | flash = false 342 | }, 343 | -- Rainmad Scripts 344 | ['artgalleryrobbery'] = { 345 | radius = 0, 346 | sprite = 269, 347 | color = 59, 348 | scale = 1.5, 349 | length = 2, 350 | sound = 'robberysound', 351 | offset = false, 352 | flash = false 353 | }, 354 | ['humanelabsrobbery'] = { 355 | radius = 0, 356 | sprite = 499, 357 | color = 1, 358 | scale = 1.5, 359 | length = 2, 360 | sound = 'robberysound', 361 | offset = false, 362 | flash = false 363 | }, 364 | ['trainrobbery'] = { 365 | radius = 0, 366 | sprite = 667, 367 | color = 78, 368 | scale = 1.5, 369 | length = 2, 370 | sound = 'robberysound', 371 | offset = false, 372 | flash = false 373 | }, 374 | ['vanrobbery'] = { 375 | radius = 0, 376 | sprite = 67, 377 | color = 59, 378 | scale = 1.5, 379 | length = 2, 380 | sound = 'robberysound', 381 | offset = false, 382 | flash = false 383 | }, 384 | ['undergroundrobbery'] = { 385 | radius = 0, 386 | sprite = 486, 387 | color = 59, 388 | scale = 1.5, 389 | length = 2, 390 | sound = 'robberysound', 391 | offset = false, 392 | flash = false 393 | }, 394 | ['drugboatrobbery'] = { 395 | radius = 0, 396 | sprite = 427, 397 | color = 26, 398 | scale = 1.5, 399 | length = 2, 400 | sound = 'robberysound', 401 | offset = false, 402 | flash = false 403 | }, 404 | ['unionrobbery'] = { 405 | radius = 0, 406 | sprite = 500, 407 | color = 60, 408 | scale = 1.5, 409 | length = 2, 410 | sound = 'robberysound', 411 | offset = false, 412 | flash = false 413 | }, 414 | ['carboosting'] = { 415 | radius = 0, 416 | sprite = 595, 417 | color = 60, 418 | scale = 1.5, 419 | length = 2, 420 | sound = 'Lose_1st', 421 | sound2 = 'GTAO_FM_Events_Soundset', 422 | offset = false, 423 | flash = false 424 | }, 425 | ['carjack'] = { 426 | radius = 0, 427 | sprite = 595, 428 | color = 60, 429 | scale = 1.5, 430 | length = 2, 431 | sound = 'Lose_1st', 432 | sound2 = 'GTAO_FM_Events_Soundset', 433 | offset = false, 434 | flash = false 435 | }, 436 | ['explosion'] = { 437 | radius = 75.0, 438 | sprite = 436, 439 | color = 1, 440 | scale = 1.5, 441 | length = 2, 442 | sound = 'Lose_1st', 443 | sound2 = 'GTAO_FM_Events_Soundset', 444 | offset = true, 445 | flash = false 446 | } 447 | } 448 | 449 | Config.Colors = { 450 | ['0'] = "Metallic Black", 451 | ['1'] = "Metallic Graphite Black", 452 | ['2'] = "Metallic Black Steel", 453 | ['3'] = "Metallic Dark Silver", 454 | ['4'] = "Metallic Silver", 455 | ['5'] = "Metallic Blue Silver", 456 | ['6'] = "Metallic Steel Gray", 457 | ['7'] = "Metallic Shadow Silver", 458 | ['8'] = "Metallic Stone Silver", 459 | ['9'] = "Metallic Midnight Silver", 460 | ['10'] = "Metallic Gun Metal", 461 | ['11'] = "Metallic Anthracite Grey", 462 | ['12'] = "Matte Black", 463 | ['13'] = "Matte Gray", 464 | ['14'] = "Matte Light Grey", 465 | ['15'] = "Util Black", 466 | ['16'] = "Util Black Poly", 467 | ['17'] = "Util Dark silver", 468 | ['18'] = "Util Silver", 469 | ['19'] = "Util Gun Metal", 470 | ['20'] = "Util Shadow Silver", 471 | ['21'] = "Worn Black", 472 | ['22'] = "Worn Graphite", 473 | ['23'] = "Worn Silver Grey", 474 | ['24'] = "Worn Silver", 475 | ['25'] = "Worn Blue Silver", 476 | ['26'] = "Worn Shadow Silver", 477 | ['27'] = "Metallic Red", 478 | ['28'] = "Metallic Torino Red", 479 | ['29'] = "Metallic Formula Red", 480 | ['30'] = "Metallic Blaze Red", 481 | ['31'] = "Metallic Graceful Red", 482 | ['32'] = "Metallic Garnet Red", 483 | ['33'] = "Metallic Desert Red", 484 | ['34'] = "Metallic Cabernet Red", 485 | ['35'] = "Metallic Candy Red", 486 | ['36'] = "Metallic Sunrise Orange", 487 | ['37'] = "Metallic Classic Gold", 488 | ['38'] = "Metallic Orange", 489 | ['39'] = "Matte Red", 490 | ['40'] = "Matte Dark Red", 491 | ['41'] = "Matte Orange", 492 | ['42'] = "Matte Yellow", 493 | ['43'] = "Util Red", 494 | ['44'] = "Util Bright Red", 495 | ['45'] = "Util Garnet Red", 496 | ['46'] = "Worn Red", 497 | ['47'] = "Worn Golden Red", 498 | ['48'] = "Worn Dark Red", 499 | ['49'] = "Metallic Dark Green", 500 | ['50'] = "Metallic Racing Green", 501 | ['51'] = "Metallic Sea Green", 502 | ['52'] = "Metallic Olive Green", 503 | ['53'] = "Metallic Green", 504 | ['54'] = "Metallic Gasoline Blue Green", 505 | ['55'] = "Matte Lime Green", 506 | ['56'] = "Util Dark Green", 507 | ['57'] = "Util Green", 508 | ['58'] = "Worn Dark Green", 509 | ['59'] = "Worn Green", 510 | ['60'] = "Worn Sea Wash", 511 | ['61'] = "Metallic Midnight Blue", 512 | ['62'] = "Metallic Dark Blue", 513 | ['63'] = "Metallic Saxony Blue", 514 | ['64'] = "Metallic Blue", 515 | ['65'] = "Metallic Mariner Blue", 516 | ['66'] = "Metallic Harbor Blue", 517 | ['67'] = "Metallic Diamond Blue", 518 | ['68'] = "Metallic Surf Blue", 519 | ['69'] = "Metallic Nautical Blue", 520 | ['70'] = "Metallic Bright Blue", 521 | ['71'] = "Metallic Purple Blue", 522 | ['72'] = "Metallic Spinnaker Blue", 523 | ['73'] = "Metallic Ultra Blue", 524 | ['74'] = "Metallic Bright Blue", 525 | ['75'] = "Util Dark Blue", 526 | ['76'] = "Util Midnight Blue", 527 | ['77'] = "Util Blue", 528 | ['78'] = "Util Sea Foam Blue", 529 | ['79'] = "Uil Lightning blue", 530 | ['80'] = "Util Maui Blue Poly", 531 | ['81'] = "Util Bright Blue", 532 | ['82'] = "Matte Dark Blue", 533 | ['83'] = "Matte Blue", 534 | ['84'] = "Matte Midnight Blue", 535 | ['85'] = "Worn Dark blue", 536 | ['86'] = "Worn Blue", 537 | ['87'] = "Worn Light blue", 538 | ['88'] = "Metallic Taxi Yellow", 539 | ['89'] = "Metallic Race Yellow", 540 | ['90'] = "Metallic Bronze", 541 | ['91'] = "Metallic Yellow Bird", 542 | ['92'] = "Metallic Lime", 543 | ['93'] = "Metallic Champagne", 544 | ['94'] = "Metallic Pueblo Beige", 545 | ['95'] = "Metallic Dark Ivory", 546 | ['96'] = "Metallic Choco Brown", 547 | ['97'] = "Metallic Golden Brown", 548 | ['98'] = "Metallic Light Brown", 549 | ['99'] = "Metallic Straw Beige", 550 | ['100'] = "Metallic Moss Brown", 551 | ['101'] = "Metallic Biston Brown", 552 | ['102'] = "Metallic Beechwood", 553 | ['103'] = "Metallic Dark Beechwood", 554 | ['104'] = "Metallic Choco Orange", 555 | ['105'] = "Metallic Beach Sand", 556 | ['106'] = "Metallic Sun Bleeched Sand", 557 | ['107'] = "Metallic Cream", 558 | ['108'] = "Util Brown", 559 | ['109'] = "Util Medium Brown", 560 | ['110'] = "Util Light Brown", 561 | ['111'] = "Metallic White", 562 | ['112'] = "Metallic Frost White", 563 | ['113'] = "Worn Honey Beige", 564 | ['114'] = "Worn Brown", 565 | ['115'] = "Worn Dark Brown", 566 | ['116'] = "Worn straw beige", 567 | ['117'] = "Brushed Steel", 568 | ['118'] = "Brushed Black Steel", 569 | ['119'] = "Brushed Aluminium", 570 | ['120'] = "Chrome", 571 | ['121'] = "Worn Off White", 572 | ['122'] = "Util Off White", 573 | ['123'] = "Worn Orange", 574 | ['124'] = "Worn Light Orange", 575 | ['125'] = "Metallic Securicor Green", 576 | ['126'] = "Worn Taxi Yellow", 577 | ['127'] = "Police Car Blue", 578 | ['128'] = "Matte Green", 579 | ['129'] = "Matte Brown", 580 | ['130'] = "Worn Orange", 581 | ['131'] = "Matte White", 582 | ['132'] = "Worn White", 583 | ['133'] = "Worn Olive Army Green", 584 | ['134'] = "Pure White", 585 | ['135'] = "Hot Pink", 586 | ['136'] = "Salmon pink", 587 | ['137'] = "Metallic Vermillion Pink", 588 | ['138'] = "Orange", 589 | ['139'] = "Green", 590 | ['140'] = "Blue", 591 | ['141'] = "Mettalic Black Blue", 592 | ['142'] = "Metallic Black Purple", 593 | ['143'] = "Metallic Black Red", 594 | ['144'] = "hunter green", 595 | ['145'] = "Metallic Purple", 596 | ['146'] = "Metallic Dark Blue", 597 | ['147'] = "Black", 598 | ['148'] = "Matte Purple", 599 | ['149'] = "Matte Dark Purple", 600 | ['150'] = "Metallic Lava Red", 601 | ['151'] = "Matte Forest Green", 602 | ['152'] = "Matte Olive Drab", 603 | ['153'] = "Matte Desert Brown", 604 | ['154'] = "Matte Desert Tan", 605 | ['155'] = "Matte Foilage Green", 606 | ['156'] = "Default Alloy Color", 607 | ['157'] = "Epsilon Blue", 608 | ['158'] = "Pure Gold", 609 | ['159'] = "Brushed Gold", 610 | ['160'] = "MP100" 611 | } 612 | -------------------------------------------------------------------------------- /sounds/dispatch.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Sloth/ps-dispatch/e42760ba65c1d840a01ba8519d82e1af96f0e14a/sounds/dispatch.ogg -------------------------------------------------------------------------------- /sounds/panicbutton.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Sloth/ps-dispatch/e42760ba65c1d840a01ba8519d82e1af96f0e14a/sounds/panicbutton.ogg -------------------------------------------------------------------------------- /sounds/robberysound.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Sloth/ps-dispatch/e42760ba65c1d840a01ba8519d82e1af96f0e14a/sounds/robberysound.ogg -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /ui/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": true, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # Svelte + TS + Vite 2 | 3 | This template should help get you started developing with Svelte and TypeScript in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). 8 | 9 | ## Need an official Svelte framework? 10 | 11 | Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. 12 | 13 | ## Technical considerations 14 | 15 | **Why use this over SvelteKit?** 16 | 17 | - It brings its own routing solution which might not be preferable for some users. 18 | - It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. 19 | `vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example. 20 | 21 | This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. 22 | 23 | Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. 24 | 25 | **Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** 26 | 27 | Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. 28 | 29 | **Why enable `allowJs` in the TS template?** 30 | 31 | While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. 32 | 33 | **Why is HMR not preserving my local component state?** 34 | 35 | HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). 36 | 37 | If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. 38 | 39 | ```ts 40 | // store.ts 41 | // An extremely simple external store 42 | import { writable } from 'svelte/store' 43 | export default writable(0) 44 | ``` 45 | -------------------------------------------------------------------------------- /ui/app.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | window.addEventListener('message', function (event) { 3 | let data = event.data; 4 | if (data.update == 'newCall') { 5 | addNewCall(data.callID, data.timer, data.data, data.isPolice); 6 | } 7 | }); 8 | }); 9 | 10 | const MONTH_NAMES = [ 11 | 'January', 12 | 'February', 13 | 'March', 14 | 'April', 15 | 'May', 16 | 'June', 17 | 'July', 18 | 'August', 19 | 'September', 20 | 'October', 21 | 'November', 22 | 'December', 23 | ]; 24 | 25 | function getFormattedDate(date, prefomattedDate = false, hideYear = false) { 26 | const day = date.getDate(); 27 | const month = MONTH_NAMES[date.getMonth()]; 28 | const year = date.getFullYear(); 29 | const hours = date.getHours(); 30 | let minutes = date.getMinutes(); 31 | 32 | if (minutes < 10) { 33 | minutes = `0${minutes}`; 34 | } 35 | 36 | if (prefomattedDate) { 37 | return `${prefomattedDate} at ${hours}:${minutes}`; 38 | } 39 | 40 | if (hideYear) { 41 | return `${day}. ${month} at ${hours}:${minutes}`; 42 | } 43 | 44 | return `${day}. ${month} ${year}. at ${hours}:${minutes}`; 45 | } 46 | 47 | function timeAgo(dateParam) { 48 | if (!dateParam) { 49 | return null; 50 | } 51 | 52 | const date = 53 | typeof dateParam === 'object' ? dateParam : new Date(dateParam); 54 | const DAY_IN_MS = 86400000; 55 | const today = new Date(); 56 | const yesterday = new Date(today - DAY_IN_MS); 57 | const seconds = Math.round((today - date) / 1000); 58 | const minutes = Math.round(seconds / 60); 59 | const isToday = today.toDateString() === date.toDateString(); 60 | const isYesterday = yesterday.toDateString() === date.toDateString(); 61 | const isThisYear = today.getFullYear() === date.getFullYear(); 62 | 63 | if (seconds < 5) { 64 | return 'Just Now'; 65 | } else if (seconds < 60) { 66 | return `${seconds} Seconds ago`; 67 | } else if (seconds < 90) { 68 | return 'About a minute ago'; 69 | } else if (minutes < 60) { 70 | return `${minutes} Minutes ago`; 71 | } else if (isToday) { 72 | return getFormattedDate(date, 'Today'); 73 | } else if (isYesterday) { 74 | return getFormattedDate(date, 'Yesterday'); 75 | } else if (isThisYear) { 76 | return getFormattedDate(date, false, true); 77 | } 78 | 79 | return getFormattedDate(date); 80 | } 81 | 82 | function addNewCall(callID, timer, info, isPolice) { 83 | const prio = info['priority']; 84 | let DispatchItem; 85 | if (info['isDead']) { 86 | DispatchItem = `
#${callID}
${info.dispatchCode}
${info.dispatchMessage}
`; 87 | } else { 88 | DispatchItem = `
#${callID}
${info.dispatchCode}
${info.dispatchMessage}
`; 89 | } 90 | 91 | // Above we are defining a default dispatch item and then we will append the data we have been sent. 92 | 93 | if (info['time']) { 94 | DispatchItem += `
${timeAgo( 95 | info['time'] 96 | )}
`; 97 | } 98 | 99 | if (info['firstStreet']) { 100 | DispatchItem += `
${info['firstStreet']}
`; 101 | } 102 | 103 | if (info['heading']) { 104 | DispatchItem += `
${info['heading']}
`; 105 | } 106 | 107 | if (info['callsign']) { 108 | DispatchItem += `
${info['callsign']}
`; 109 | } 110 | 111 | if (info['doorCount']) { 112 | DispatchItem += `
${info['doorCount']}
`; 113 | } 114 | 115 | if (info['weapon']) { 116 | DispatchItem += `
${info['weapon']}
`; 117 | } 118 | 119 | if (info['camId']) { 120 | DispatchItem += `
${info['camId']}
`; 121 | } 122 | 123 | if (info['gender']) { 124 | DispatchItem += `
${info['gender']}
`; 125 | } 126 | 127 | if (info['model'] && info['plate']) { 128 | DispatchItem += `
${info['model']}${info['plate']}
`; 129 | } else if (info['plate']) { 130 | DispatchItem += `
${info['plate']}
`; 131 | } else if (info['model']) { 132 | DispatchItem += `
${info['model']}
`; 133 | } 134 | 135 | if (info['firstColor']) { 136 | DispatchItem += `
${info['firstColor']}
`; 137 | } 138 | if (info['automaticGunfire'] == true) { 139 | DispatchItem += `
Automatic Gunfire
`; 140 | } 141 | 142 | if (info['name'] && info['number']) { 143 | DispatchItem += `
${info['name']}${info['number']}
`; 144 | } else if (info['number']) { 145 | DispatchItem += `
${info['number']}
`; 146 | } else if (info['name']) { 147 | DispatchItem += `
${info['name']}
`; 148 | } 149 | 150 | if (info['information']) { 151 | DispatchItem += `
${info['information']}
`; 152 | } 153 | 154 | DispatchItem += `
`; 155 | 156 | $('.dispatch-holder').prepend(DispatchItem); 157 | 158 | var timer = 4000; 159 | 160 | if (prio == 1) { 161 | timer = 12000; 162 | } else if (prio == 2) { 163 | timer = 9000; 164 | } 165 | 166 | $(`.${callID}`).addClass('animate__backInRight'); 167 | setTimeout(() => { 168 | $(`.${callID}`).addClass('animate__backOutRight'); 169 | setTimeout(() => { 170 | $(`.${callID}`).remove(); 171 | }, 1000); 172 | }, timer || 4500); 173 | } 174 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | OK1ez 22 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "would_you_rather", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-check --tsconfig ./tsconfig.json" 10 | }, 11 | "devDependencies": { 12 | "@sveltejs/vite-plugin-svelte": "^2.4.2", 13 | "@tsconfig/svelte": "^3.0.0", 14 | "@types/leaflet": "^1.9.8", 15 | "autoprefixer": "^10.4.16", 16 | "daisyui": "^3.1.2", 17 | "postcss": "^8.4.31", 18 | "svelte": "^3.59.2", 19 | "svelte-check": "^2.7.2", 20 | "svelte-preprocess": "^5.1.3", 21 | "tailwindcss": "^3.4.1", 22 | "tslib": "^2.6.2", 23 | "typescript": "^4.7.3", 24 | "vite": "4.5.3" 25 | }, 26 | "dependencies": { 27 | "leaflet": "^1.9.4", 28 | "lucide": "^0.307.0", 29 | "lucide-svelte": "^0.307.0", 30 | "svelte-dnd-action": "^0.9.38" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | import tailwind from 'tailwindcss'; 2 | import autoprefixer from 'autoprefixer'; 3 | import tailwindConfig from './tailwind.config.cjs'; 4 | 5 | export default { 6 | plugins: [tailwind(tailwindConfig), autoprefixer], 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/App.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | {#if $Locale} 14 | 15 | 16 | 17 | 18 |
19 | {/if} 20 | 21 | 22 | 23 | {#if $BROWSER_MODE} 24 | 25 | 26 | {/if} 27 | -------------------------------------------------------------------------------- /ui/src/Tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | *:focus { 11 | outline: none; 12 | } 13 | 14 | :root { 15 | font-size: 62.5%; 16 | font-smooth: auto; 17 | color: #d8d8d8; 18 | font-synthesis: none; 19 | text-rendering: optimizeLegibility; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | -webkit-text-size-adjust: 100%; 23 | } 24 | 25 | html, body { 26 | height: 100vh; 27 | width: 100vw; 28 | font-size: 1.6rem; 29 | overflow: hidden; 30 | } 31 | 32 | ::-webkit-scrollbar { 33 | height: 0px; 34 | width: 4px; 35 | background-color: #7979795e; 36 | border-radius: 50px; 37 | } 38 | 39 | ::-webkit-scrollbar-thumb { 40 | background-color: #cecece; 41 | border-radius: 50px; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /ui/src/components/Main.svelte: -------------------------------------------------------------------------------- 1 | 61 | 62 | 63 |
64 |
68 | {#each notifications.slice().reverse() as dispatch, index (dispatch.data.id)} 69 |
70 |
71 |

72 | #{dispatch.data.id} 73 |

74 |

75 | {dispatch.data.code} 76 |

77 |

78 | {dispatch.data.message} 79 |

80 | 81 |
82 |
83 |
84 | {#if dispatch.data} 85 | {#each getDispatchData(dispatch) as field} 86 | {#if field.value} 87 |

88 | 89 | {field.label}: {field.value} 90 |

91 | {/if} 92 | {/each} 93 | {/if} 94 |
95 |
96 | {#if index === 0} 97 |

98 | [{$RESPOND_KEYBIND}] Respond 99 |

100 | {/if} 101 |
102 |
103 |
104 | {/each} 105 |
106 |
107 | -------------------------------------------------------------------------------- /ui/src/components/Menu.svelte: -------------------------------------------------------------------------------- 1 | 87 | 88 | 204 | 205 | -------------------------------------------------------------------------------- /ui/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.svelte' 2 | import './Tailwind.css' 3 | 4 | const app = new App({ 5 | target: document.getElementById('app') 6 | }) 7 | 8 | export default app -------------------------------------------------------------------------------- /ui/src/providers/AlwaysListener.svelte: -------------------------------------------------------------------------------- 1 | 57 | -------------------------------------------------------------------------------- /ui/src/providers/BackdropFix.svelte: -------------------------------------------------------------------------------- 1 | 177 | 178 |
179 | 184 |
185 | 186 | -------------------------------------------------------------------------------- /ui/src/providers/DebugBrowser.svelte: -------------------------------------------------------------------------------- 1 | 118 | 119 | 120 |
121 | 128 | {#if show} 129 |
130 | {#each options as option} 131 |
132 |

{option.component}

133 | {#each option.actions as action} 134 | 151 | {/each} 152 |
153 | {/each} 154 |
155 | {/if} 156 |
157 | 158 | -------------------------------------------------------------------------------- /ui/src/providers/VisibilityProvider.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 | {#if $VISIBILITY} 37 |
38 | 39 |
40 | 41 | {/if} 42 | 43 | 57 | -------------------------------------------------------------------------------- /ui/src/store/stores.ts: -------------------------------------------------------------------------------- 1 | import { writable, derived } from "svelte/store"; 2 | 3 | export const VISIBILITY = writable(false); 4 | export const BROWSER_MODE = writable(false); 5 | export const RESOURCE_NAME = writable(""); 6 | 7 | export const PLAYER = writable(null); 8 | export const MAX_CALL_LIST = writable(null); 9 | export const RESPOND_KEYBIND = writable(""); 10 | 11 | export const DISPATCH_MUTED = writable(false); 12 | export const DISPATCH_DISABLED = writable(false); 13 | 14 | export const DISPATCH = writable(null); 15 | 16 | export const IS_RIGHT_MARGIN = writable(true); 17 | 18 | export const shortCalls = writable(true); 19 | 20 | export function removeDispatch(callID) { 21 | DISPATCH.update(dispatches => { 22 | return dispatches.filter(dispatch => dispatch.data.id !== callID); 23 | }); 24 | } 25 | 26 | interface DISPATCHMENU_DATA { 27 | id: number, 28 | message: string, 29 | code: string, 30 | icon: string, 31 | time: number, 32 | priority: number, 33 | street: string, 34 | coords: any[], 35 | gender: string, 36 | automaticGunFire: boolean, 37 | weapon: string, 38 | units: any[], 39 | name: string, 40 | number: string, 41 | information: string, 42 | vehicle: string, 43 | color: string, 44 | plate: string, 45 | class: string, 46 | doors: string, 47 | heading: string, 48 | jobs: any[], 49 | } 50 | 51 | export const DISPATCH_MENU = writable(null); 52 | export const DISPATCH_MENUS = writable(null); 53 | 54 | 55 | interface LOCALE_DATA { 56 | dispatch_detach: string, 57 | dispatch_attach: string, 58 | unit: string, 59 | units: string, 60 | additionals: string, 61 | } 62 | 63 | export const Locale = writable(null); 64 | 65 | export const processedDispatchMenu = derived( 66 | [DISPATCH_MENU, MAX_CALL_LIST, PLAYER], 67 | ([$DISPATCH_MENU, $MAX_CALL_LIST, $PLAYER]) => { 68 | if (!$DISPATCH_MENU || $MAX_CALL_LIST === null || !$PLAYER) { 69 | // Handling null or undefined values 70 | return []; 71 | } 72 | 73 | return $DISPATCH_MENU 74 | .slice(-$MAX_CALL_LIST) 75 | .filter(dispatch => 76 | dispatch.message && dispatch.jobs.includes($PLAYER.job.type) 77 | ) 78 | .reverse(); 79 | } 80 | ); -------------------------------------------------------------------------------- /ui/src/typings/type.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Sloth/ps-dispatch/e42760ba65c1d840a01ba8519d82e1af96f0e14a/ui/src/typings/type.ts -------------------------------------------------------------------------------- /ui/src/utils/ReceiveNUI.ts: -------------------------------------------------------------------------------- 1 | import { onMount, onDestroy } from "svelte"; 2 | 3 | interface NuiMessage { 4 | action: string; 5 | data: T; 6 | } 7 | 8 | /** 9 | * A function that manage events listeners for receiving data from the client scripts 10 | * @param action The specific `action` that should be listened for. 11 | * @param handler The callback function that will handle data relayed by this function 12 | * 13 | * @example 14 | * useNuiEvent<{VISIBILITY: true, wasVisible: 'something'}>('setVisible', (data) => { 15 | * // whatever logic you want 16 | * }) 17 | * 18 | **/ 19 | 20 | export function ReceiveNUI( 21 | action: string, 22 | handler: (data: T) => void 23 | ) { 24 | const eventListener = (event: MessageEvent>) => { 25 | const { action: eventAction, data } = event.data; 26 | 27 | eventAction === action && handler(data); 28 | }; 29 | onMount(() => window.addEventListener("message", eventListener)); 30 | onDestroy(() => window.removeEventListener("message", eventListener)); 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/utils/SendNUI.ts: -------------------------------------------------------------------------------- 1 | import { BROWSER_MODE, RESOURCE_NAME } from '@store/stores' 2 | 3 | let isBrowserMode: boolean = false; 4 | BROWSER_MODE.subscribe((value: boolean) => { 5 | isBrowserMode = value; 6 | }); 7 | 8 | let debugResName: string = ""; 9 | RESOURCE_NAME.subscribe((value: string) => { 10 | debugResName = value; 11 | }); 12 | 13 | /** 14 | * @param eventName - The endpoint eventname to target 15 | * @param data - Data you wish to send in the NUI Callback 16 | * 17 | * @return returnData - A promise for the data sent back by the NuiCallbacks CB argument 18 | */ 19 | 20 | export async function SendNUI( 21 | eventName: string, 22 | data: unknown = {}, 23 | debugReturn?: T 24 | ): Promise { 25 | if ((isBrowserMode == true && debugReturn) || (isBrowserMode == true)) { 26 | return Promise.resolve(debugReturn || {} as T) 27 | } 28 | const options = { 29 | method: "post", 30 | headers: { 31 | "Content-Type": "application/json; charset=UTF-8", 32 | }, 33 | body: JSON.stringify(data), 34 | }; 35 | 36 | const resourceName = (window as any).GetParentResourceName 37 | ? (window as any).GetParentResourceName() 38 | : debugResName; 39 | 40 | 41 | const resp: Response = await fetch(`https://${resourceName}/${eventName}`, options); 42 | return await resp.json() 43 | } -------------------------------------------------------------------------------- /ui/src/utils/debugData.ts: -------------------------------------------------------------------------------- 1 | import {isEnvBrowser} from "./misc"; 2 | 3 | interface DebugEvent { 4 | action: string; 5 | data: T; 6 | } 7 | 8 | /* 9 | * Emulates dispatching an event using SendNuiMessage in the lua scripts. 10 | * This is used when developing in browser 11 | * 12 | * @param events - The event you want to cover 13 | * @param timer - How long until it should trigger (ms) 14 | */ 15 | export const debugData =

(events: DebugEvent

[], timer = 0): void => { 16 | if (isEnvBrowser()) { 17 | for (const event of events) { 18 | setTimeout(() => { 19 | window.dispatchEvent( 20 | new MessageEvent("message", { 21 | data: { 22 | action: event.action, 23 | data: event.data, 24 | }, 25 | }) 26 | ); 27 | }, timer); 28 | } 29 | } 30 | }; -------------------------------------------------------------------------------- /ui/src/utils/misc.ts: -------------------------------------------------------------------------------- 1 | export const isEnvBrowser = (): boolean => !(window as any).invokeNative; -------------------------------------------------------------------------------- /ui/src/utils/timeAgo.ts: -------------------------------------------------------------------------------- 1 | const MONTH_NAMES = [ 2 | 'January', 3 | 'February', 4 | 'March', 5 | 'April', 6 | 'May', 7 | 'June', 8 | 'July', 9 | 'August', 10 | 'September', 11 | 'October', 12 | 'November', 13 | 'December', 14 | ]; 15 | 16 | function getFormattedDate(date, prefomattedDate = false, hideYear = false) { 17 | const day = date.getDate(); 18 | const month = MONTH_NAMES[date.getMonth()]; 19 | const year = date.getFullYear(); 20 | const hours = date.getHours(); 21 | let minutes = date.getMinutes(); 22 | 23 | if (minutes < 10) { 24 | minutes = `0${minutes}`; 25 | } 26 | 27 | if (prefomattedDate) { 28 | return `${prefomattedDate} at ${hours}:${minutes}`; 29 | } 30 | 31 | if (hideYear) { 32 | return `${day}. ${month} at ${hours}:${minutes}`; 33 | } 34 | 35 | return `${day}. ${month} ${year}. at ${hours}:${minutes}`; 36 | } 37 | 38 | 39 | export function timeAgo(dateParam) { 40 | if (!dateParam) { 41 | return 'Unknown'; 42 | } 43 | 44 | let date; 45 | try { 46 | date = typeof dateParam === 'object' ? dateParam : new Date(dateParam); 47 | } catch (e) { 48 | return 'Invalid date'; 49 | } 50 | 51 | if (isNaN(date)) { 52 | return 'Invalid date'; 53 | } 54 | const DAY_IN_MS = 86400000; 55 | const today = new Date(); 56 | const yesterday = new Date(today - DAY_IN_MS); 57 | const seconds = Math.round((today - date) / 1000); 58 | const minutes = Math.round(seconds / 60); 59 | const isToday = today.toDateString() === date.toDateString(); 60 | const isYesterday = yesterday.toDateString() === date.toDateString(); 61 | const isThisYear = today.getFullYear() === date.getFullYear(); 62 | 63 | if (seconds < 5) { 64 | return 'Just Now'; 65 | } else if (seconds < 60) { 66 | return `${seconds} Seconds ago`; 67 | } else if (seconds < 90) { 68 | return 'A minute ago'; 69 | } else if (minutes < 60) { 70 | return `${minutes} Minutes ago`; 71 | } else if (isToday) { 72 | return getFormattedDate(date, 'Today'); 73 | } else if (isYesterday) { 74 | return getFormattedDate(date, 'Yesterday'); 75 | } else if (isThisYear) { 76 | return getFormattedDate(date, false, true); 77 | } 78 | 79 | return getFormattedDate(date); 80 | } -------------------------------------------------------------------------------- /ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /ui/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Montserrat'); 2 | 3 | body { 4 | font-family: 'Montserrat', sans-serif; 5 | overflow: hidden; 6 | /* display: none; */ 7 | } 8 | 9 | ::-webkit-scrollbar { 10 | width: 0px; 11 | } 12 | 13 | .dispatch-container { 14 | width: 100%; 15 | height: 100%; 16 | position: absolute; 17 | display: flex; 18 | } 19 | 20 | .dispatch-holder { 21 | width: 50vh; 22 | height: 87.5vh; 23 | margin: auto; 24 | margin-top: 9.75vh; 25 | margin-right: 0vh; 26 | display: flex; 27 | flex-direction: column; 28 | overflow-y: scroll; 29 | } 30 | 31 | .dispatch-item { 32 | margin: auto; 33 | margin-top: 0vh; 34 | margin-right: 1vh; 35 | margin-bottom: .5vh; 36 | background-color: rgba(21, 38, 56, 0.80); 37 | border-right: .75vh solid rgba(57, 59, 57, 0.80); 38 | width: 37.5vh; 39 | height: fit-content; 40 | border-bottom-left-radius: 1vh; 41 | border-top-left-radius: 1vh; 42 | display: flex; 43 | flex-direction: column; 44 | } 45 | 46 | .dispatch-item-true { 47 | background-color: rgba(21, 38, 56, 0.80); 48 | } 49 | 50 | .dispatch-item-false { 51 | background-color: rgba(56, 21, 21, 0.8); 52 | } 53 | 54 | .dispatch-item-officer { 55 | background-color: rgb(97, 3, 11); 56 | } 57 | 58 | .top-info-holder { 59 | width: 95%; 60 | height: fit-content; 61 | margin: auto; 62 | margin-top: .75vh; 63 | margin-left: .75vh; 64 | align-items: center; 65 | display: flex; 66 | flex-direction: row; 67 | color: white; 68 | font-size: 1.25vh; 69 | font-weight: bold; 70 | white-space: nowrap; 71 | } 72 | 73 | .call-id { 74 | background-color: #950909; 75 | padding: .35vh; 76 | padding-bottom: .4vh; 77 | padding-left: 1.75vh; 78 | padding-right: 1.75vh; 79 | margin-top: auto; 80 | margin-bottom: auto; 81 | margin-left: 0vh; 82 | margin-right: .4vh; 83 | border-radius: 1vh; 84 | text-align: center; 85 | } 86 | 87 | .call-code { 88 | background-color: #097C95; 89 | padding: .35vh; 90 | padding-bottom: .4vh; 91 | padding-left: 1.75vh; 92 | padding-right: 1.75vh; 93 | margin-top: auto; 94 | margin-bottom: auto; 95 | margin-left: 0vh; 96 | margin-right: .5vh; 97 | border-radius: 1vh; 98 | text-align: center; 99 | } 100 | 101 | .call-name { 102 | margin-top: auto; 103 | margin-bottom: auto; 104 | margin-left: 0vh; 105 | margin-right: auto; 106 | border-radius: 1vh; 107 | text-align: left; 108 | font-size: 1.7vh; 109 | overflow: hidden; 110 | text-overflow: ellipsis; 111 | } 112 | 113 | .bottom-info-holder { 114 | width: 95%; 115 | height: fit-content; 116 | margin: auto; 117 | margin-top: .75vh; 118 | margin-bottom: .6vh; 119 | align-items: center; 120 | display: flex; 121 | flex-direction: column; 122 | color: white; 123 | font-size: 1.2vh; 124 | font-weight: bold; 125 | white-space: nowrap; 126 | text-align: left; 127 | } 128 | 129 | .call-bottom-info { 130 | margin: auto; 131 | margin-left: 0vh; 132 | margin-bottom: .5vh; 133 | text-align: left; 134 | overflow:wrap; 135 | white-space:normal; 136 | } 137 | 138 | .line { 139 | background-color: rgba(128, 128, 128, 0.1); 140 | height: 0.05vh; 141 | width: 100%; 142 | margin-top: 0.1vh; 143 | margin-bottom: 0.1vh 144 | } 145 | 146 | .call-bottom-information { 147 | margin-top: 0.5vh; 148 | } 149 | 150 | .fas { 151 | margin-right: .5vh; 152 | } 153 | 154 | .fab { 155 | margin-right: .5vh; 156 | } 157 | 158 | .far { 159 | margin-right: .5vh; 160 | } 161 | 162 | .fab { 163 | margin-right: .5vh; 164 | } 165 | 166 | .priority-1 { 167 | background: linear-gradient(270deg, #b70000, #0a24b0); 168 | background-size: 400% 400%; 169 | animation: gradient 3s ease infinite; 170 | background-color: #097b9500; 171 | } 172 | 173 | .priority-2 { 174 | background-color: #ce7808; 175 | } 176 | 177 | @keyframes gradient { 178 | 0%{background-position:0% 50%} 179 | 50%{background-position:100% 50%} 180 | 100%{background-position:0% 50%} 181 | } -------------------------------------------------------------------------------- /ui/svelte.config.js: -------------------------------------------------------------------------------- 1 | import sveltePreprocess from 'svelte-preprocess' 2 | 3 | export default { 4 | // Consult https://github.com/sveltejs/svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: sveltePreprocess() 7 | } 8 | -------------------------------------------------------------------------------- /ui/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | darkmode: true, 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{svelte,js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | primary: '#232B33', 11 | secondary: '#25303B', 12 | tertiary: '#2F3C47', 13 | priority_primary: '#332328', 14 | priority_secondary: '#3B252F', 15 | priority_tertiary: '#472F39', 16 | priority_quaternary: '#9A003A', 17 | accent: '#2284d9', 18 | accent_green: '#00A379', 19 | accent_dark_green: '#008563', 20 | accent_cyan: '#0098A3', 21 | accent_red: '#FF004E', 22 | accent_dark_red: '#850032', 23 | border_primary: '#373a40', 24 | hover_secondary: '#2c2e33', 25 | } 26 | }, 27 | }, 28 | plugins: [], 29 | } 30 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "resolveJsonModule": true, 8 | "baseUrl": ".", 9 | "paths": { 10 | "@assets/*": ["src/assets/*"], 11 | "@components/*": ["src/components/*"], 12 | "@providers/*": ["src/providers/*"], 13 | "@store/*": ["src/store/*"], 14 | "@utils/*": ["src/utils/*"], 15 | "@typings/*": ["src/typings/*"], 16 | "@layout/*": ["src/layout/*"], 17 | "@pages/*": ["src/pages/*"], 18 | }, 19 | /** 20 | * Typecheck JS in `.svelte` and `.js` files by default. 21 | * Disable checkJs if you'd like to use dynamic types in JS. 22 | * Note that setting allowJs false does not prevent the use 23 | * of JS in `.svelte` files. 24 | */ 25 | "allowJs": true, 26 | "checkJs": true 27 | }, 28 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] 29 | } 30 | -------------------------------------------------------------------------------- /ui/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | import postcss from './postcss.config.js'; 4 | import { resolve } from "path"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | css: { 9 | postcss, 10 | }, 11 | plugins: [svelte({ 12 | /* plugin options */ 13 | })], 14 | base: './', // fivem nui needs to have local dir reference 15 | resolve: { 16 | alias: { 17 | "@assets": resolve("./src/assets"), 18 | "@components": resolve("./src/components"), 19 | "@providers": resolve("./src/providers"), 20 | "@store": resolve("./src/store"), 21 | "@utils": resolve("./src/utils"), 22 | "@typings": resolve("./src/typings"), 23 | "@layout": resolve("./src/layout"), 24 | "@pages": resolve("./src/pages"), 25 | }, 26 | }, 27 | build: { 28 | emptyOutDir: true, 29 | outDir: '../html', 30 | assetsDir: './', 31 | rollupOptions: { 32 | output: { 33 | // By not having hashes in the name, you don't have to update the manifest, yay! 34 | entryFileNames: `[name].js`, 35 | chunkFileNames: `[name].js`, 36 | assetFileNames: `[name].[ext]` 37 | } 38 | } 39 | } 40 | 41 | }) 42 | --------------------------------------------------------------------------------