ed
|
@ -1311,8 +1311,18 @@ CodeStudio.Products = {
|
||||||
itemPrice = 50,
|
itemPrice = 50,
|
||||||
itemInfo = "",
|
itemInfo = "",
|
||||||
},
|
},
|
||||||
|
['Bodycam'] = {
|
||||||
|
itemName = "Bodycam",
|
||||||
|
itemStock = 150,
|
||||||
|
itemPrice = 50,
|
||||||
|
itemInfo = "",
|
||||||
|
},
|
||||||
|
['Dashcam'] = {
|
||||||
|
itemName = "Dashcam",
|
||||||
|
itemStock = 150,
|
||||||
|
itemPrice = 50,
|
||||||
|
itemInfo = "",
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
BIN
resources/[inventory]/cs_shops/ui/image/bodycam.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
resources/[inventory]/cs_shops/ui/image/dashcam.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
resources/[inventory]/inventory_images/images/bodycam.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
resources/[inventory]/inventory_images/images/dashcam.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
|
@ -10401,5 +10401,28 @@ itemsData = {
|
||||||
image = 'kq_winch.png',
|
image = 'kq_winch.png',
|
||||||
name = 'kq_winch',
|
name = 'kq_winch',
|
||||||
},
|
},
|
||||||
|
bodycam = {
|
||||||
|
shouldClose = true,
|
||||||
|
type = 'item',
|
||||||
|
description = '',
|
||||||
|
weight = 500,
|
||||||
|
label = 'Bodycam',
|
||||||
|
unique = true,
|
||||||
|
useable = true,
|
||||||
|
image = 'bodycam.png',
|
||||||
|
name = 'bodycam',
|
||||||
|
},
|
||||||
|
dashcam = {
|
||||||
|
shouldClose = true,
|
||||||
|
type = 'item',
|
||||||
|
description = '',
|
||||||
|
weight = 500,
|
||||||
|
label = 'Dashcam',
|
||||||
|
unique = true,
|
||||||
|
useable = true,
|
||||||
|
image = 'dashcam.png',
|
||||||
|
name = 'dashcam',
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
CreateThread(function()
|
||||||
|
if Config.Dependency.UseTarget == 'qb' then
|
||||||
|
for k,v in ipairs(Config.WatchLoc) do
|
||||||
|
exports['qb-target']:AddCircleZone("spycam_watch"..k,v.coords,v.rad, {
|
||||||
|
name = "spycam_watch"..k,
|
||||||
|
useZ=true,
|
||||||
|
debugPoly = v.debug,
|
||||||
|
}, {
|
||||||
|
options = {
|
||||||
|
{
|
||||||
|
type = "client",
|
||||||
|
icon = "fas fa-sign-in-alt",
|
||||||
|
label = 'Open Camera Portal',
|
||||||
|
action = function(entity)
|
||||||
|
TriggerEvent('spy-bodycam:openActiveMenu',k)
|
||||||
|
end,
|
||||||
|
canInteract = function(entity)
|
||||||
|
return targetAuth(k,PlayerJob.name)
|
||||||
|
end,
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "client",
|
||||||
|
icon = "fas fa-list",
|
||||||
|
label = 'Records Database',
|
||||||
|
action = function(entity)
|
||||||
|
TriggerServerEvent('spy-bodycam:server:showrecordingUI')
|
||||||
|
end,
|
||||||
|
canInteract = function(entity)
|
||||||
|
return targetAuth(k,PlayerJob.name)
|
||||||
|
end,
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
distance = 2.5
|
||||||
|
})
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for k,v in ipairs(Config.WatchLoc) do
|
||||||
|
exports.ox_target:addSphereZone(
|
||||||
|
{
|
||||||
|
coords = v.coords,
|
||||||
|
radius = v.rad,
|
||||||
|
debug = v.debug,
|
||||||
|
drawSprite = false,
|
||||||
|
options = {
|
||||||
|
{
|
||||||
|
name = "spycam_watch"..k,
|
||||||
|
label = 'Open Camera Portal',
|
||||||
|
icon = "fas fa-sign-in-alt",
|
||||||
|
distance = 2.5,
|
||||||
|
onSelect = function(data)
|
||||||
|
TriggerEvent('spy-bodycam:openActiveMenu',k)
|
||||||
|
end,
|
||||||
|
canInteract = function(entity, distance, coords, name, bone)
|
||||||
|
return targetAuth(k,PlayerJob.name)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "spycam_watch"..k,
|
||||||
|
label = 'Records Database',
|
||||||
|
icon = "fas fa-list",
|
||||||
|
distance = 2.5,
|
||||||
|
onSelect = function(data)
|
||||||
|
TriggerServerEvent('spy-bodycam:server:showrecordingUI')
|
||||||
|
end,
|
||||||
|
canInteract = function(entity, distance, coords, name, bone)
|
||||||
|
return targetAuth(k,PlayerJob.name)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
AddEventHandler('onResourceStop', function(resourceName)
|
||||||
|
if GetCurrentResourceName() == resourceName then
|
||||||
|
if Config.Dependency.UseTarget == 'qb' then
|
||||||
|
for k,v in ipairs(Config.WatchLoc) do
|
||||||
|
exports['qb-target']:RemoveZone("spycam_watch"..k)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
102
resources/[jobs]/[police]/[spy-bodycam]/spy-bodycam/config.lua
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
-- ███████╗██████╗ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ █████╗ ███╗ ███╗
|
||||||
|
-- ██╔════╝██╔══██╗╚██╗ ██╔╝ ██╔══██╗██╔═══██╗██╔══██╗╚██╗ ██╔╝██╔════╝██╔══██╗████╗ ████║
|
||||||
|
-- ███████╗██████╔╝ ╚████╔╝ ██████╔╝██║ ██║██║ ██║ ╚████╔╝ ██║ ███████║██╔████╔██║
|
||||||
|
-- ╚════██║██╔═══╝ ╚██╔╝ ██╔══██╗██║ ██║██║ ██║ ╚██╔╝ ██║ ██╔══██║██║╚██╔╝██║
|
||||||
|
-- ███████║██║ ██║ ██████╔╝╚██████╔╝██████╔╝ ██║ ╚██████╗██║ ██║██║ ╚═╝ ██║
|
||||||
|
-- ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝
|
||||||
|
|
||||||
|
Config = Config or {}
|
||||||
|
|
||||||
|
Config.Framework = 'qb' -- qb | esx
|
||||||
|
|
||||||
|
if Config.Framework == 'qb' then -- Dont touch this part
|
||||||
|
QBCore = exports["qb-core"]:GetCoreObject()
|
||||||
|
elseif Config.Framework == 'esx' then
|
||||||
|
ESX = exports.es_extended:getSharedObject()
|
||||||
|
end
|
||||||
|
|
||||||
|
Config.Dependency = {
|
||||||
|
UseTarget = 'qb', -- qb | ox
|
||||||
|
UseInventory = 'qb', -- qb | ox | esx
|
||||||
|
UseProgress = 'qb', -- qb | ox | esx
|
||||||
|
UseMenu = 'qb', -- qb | ox | esx
|
||||||
|
UseNotify = 'qb', -- qb | ox | esx
|
||||||
|
UseAppearance = 'illenium', -- qb | illenium | false
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.ExitCamKey = 'BACK'
|
||||||
|
|
||||||
|
Config.CameraEffect = {
|
||||||
|
bodycam = 'Island_CCTV_ChannelFuzz',
|
||||||
|
dashcam = 'TinyRacerMoBlur',
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.ForceViewCam = false -- Forces cam view to first person when recording.
|
||||||
|
Config.RecordTime = 30 --Recording Time | 30 = 30 seconds | [*note: Dont increase the time too much or it may not upload to any service you are using for example - discord.]
|
||||||
|
|
||||||
|
Config.PropLoc = { -- Change prop position according to ur clothing pack.
|
||||||
|
male = {
|
||||||
|
bone = 24818,
|
||||||
|
pos = vector3(0.16683200771922, 0.11320925137666, 0.11986595654326),
|
||||||
|
rot = vector3(-14.502323044318, 82.191095946679, -164.22066869048),
|
||||||
|
},
|
||||||
|
female = {
|
||||||
|
bone = 24818,
|
||||||
|
pos = vector3(0.16683200771922, 0.11320925137666, 0.11986595654326),
|
||||||
|
rot = vector3(-14.502323044318, 82.191095946679, -164.22066869048),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.AllowedJobs = { -- Only these jobs can use bodycam/dashcam item.
|
||||||
|
'police',
|
||||||
|
'ambulance',
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.AllowedClass = {18} -- Vehicle classes allowed to use the dashcam feature.
|
||||||
|
|
||||||
|
Config.WatchLoc = {
|
||||||
|
[1] = {
|
||||||
|
coords = vector3(440.149445, -979.437378, 30.453491),
|
||||||
|
rad = 1.5,
|
||||||
|
debug = false,
|
||||||
|
jobCam = {'police','ambulance'}, -- jobs mentioned here are shown in the list | false = able to view all the bodycams
|
||||||
|
carCam = { -- false = able to view all the dashcams
|
||||||
|
job = {'police'}, -- Jobs that activate dashcams shown in the list | false excludes.
|
||||||
|
class = {18} -- Dashcam activated on these vehicleclass shown in the list | false excludes.
|
||||||
|
},
|
||||||
|
targetAuth = {'police'}, -- jobs mentioned here can use this location from target | false = everyone can access this location
|
||||||
|
|
||||||
|
}, -- You can add more locations
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.DebugCamera = false -- Make it true if you want to get new camera offset for some vehicle.
|
||||||
|
Config.VehCamOffset = {
|
||||||
|
[`police2`] = {0.000000, 0.330000, 0.530000},
|
||||||
|
-- [`18chgr2`] = {0.000000, 0.510000, 0.630000}, -- Example vehicle. The script comes with its own offset finder just set DebugCamera to true and get the offset.
|
||||||
|
-- [`vehiclespawncode`] = {0.000000, 0.510000, 0.630000},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Vehicle Classes:
|
||||||
|
-- 0: Compacts
|
||||||
|
-- 1: Sedans
|
||||||
|
-- 2: SUVs
|
||||||
|
-- 3: Coupes
|
||||||
|
-- 4: Muscle
|
||||||
|
-- 5: Sports Classics
|
||||||
|
-- 6: Sports
|
||||||
|
-- 7: Super
|
||||||
|
-- 8: Motorcycles
|
||||||
|
-- 9: Off-road
|
||||||
|
-- 10: Industrial
|
||||||
|
-- 11: Utility
|
||||||
|
-- 12: Vans
|
||||||
|
-- 13: Cycles
|
||||||
|
-- 14: Boats
|
||||||
|
-- 15: Helicopters
|
||||||
|
-- 16: Planes
|
||||||
|
-- 17: Service
|
||||||
|
-- 18: Emergency
|
||||||
|
-- 19: Military
|
||||||
|
-- 20: Commercial
|
||||||
|
-- 21: Trains
|
||||||
|
-- 22: Open Wheel
|
|
@ -0,0 +1,29 @@
|
||||||
|
fx_version 'cerulean'
|
||||||
|
|
||||||
|
game "gta5"
|
||||||
|
|
||||||
|
author "Spy-Script"
|
||||||
|
version '2.5.6'
|
||||||
|
|
||||||
|
lua54 'yes'
|
||||||
|
|
||||||
|
ui_page 'web/index.html'
|
||||||
|
|
||||||
|
shared_script {
|
||||||
|
"config.lua",
|
||||||
|
'@ox_lib/init.lua',
|
||||||
|
}
|
||||||
|
|
||||||
|
server_script {
|
||||||
|
"server/**",
|
||||||
|
'@oxmysql/lib/MySQL.lua',
|
||||||
|
}
|
||||||
|
|
||||||
|
client_script {
|
||||||
|
'client/**',
|
||||||
|
}
|
||||||
|
|
||||||
|
files {
|
||||||
|
'web/**',
|
||||||
|
"node_modules/fivem-game-view/**/*",
|
||||||
|
}
|
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 7.2 KiB |
|
@ -0,0 +1,24 @@
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET NAMES utf8 */;
|
||||||
|
/*!50503 SET NAMES utf8mb4 */;
|
||||||
|
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||||
|
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||||
|
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||||
|
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||||
|
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `spy_bodycam` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`job` varchar(255) NOT NULL,
|
||||||
|
`videolink` longtext NOT NULL,
|
||||||
|
`street` varchar(255) NOT NULL,
|
||||||
|
`date` varchar(255) NOT NULL,
|
||||||
|
`playername` varchar(255) NOT NULL,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=42 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||||
|
|
||||||
|
/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */;
|
||||||
|
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
|
||||||
|
/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */;
|
|
@ -0,0 +1,373 @@
|
||||||
|
PlayerOnBodycam = {}
|
||||||
|
GlobalState.PlayerOnBodycam = PlayerOnBodycam
|
||||||
|
CarsOnBodycam = {}
|
||||||
|
GlobalState.CarsOnBodycam = CarsOnBodycam
|
||||||
|
|
||||||
|
lib.callback.register('spy-bodycam:servercb:getPedCoords', function(source, targetId)
|
||||||
|
local targetPed = GetPlayerPed(targetId)
|
||||||
|
if targetPed == 0 then return false end
|
||||||
|
local targetCoords = GetEntityCoords(targetPed)
|
||||||
|
if targetCoords then return targetCoords end
|
||||||
|
|
||||||
|
end)
|
||||||
|
|
||||||
|
lib.callback.register('spy-bodycam:servercb:getCarCoords', function(source, netId)
|
||||||
|
local veh = NetworkGetEntityFromNetworkId(netId)
|
||||||
|
if DoesEntityExist(veh) then
|
||||||
|
local targetCoords = GetEntityCoords(veh)
|
||||||
|
if targetCoords then return targetCoords end
|
||||||
|
else
|
||||||
|
CarsOnBodycam[netId] = nil
|
||||||
|
GlobalState.CarsOnBodycam = CarsOnBodycam
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
lib.callback.register('spy-bodycam:servercb:getNamESX', function(source)
|
||||||
|
local xPlayer = ESX.GetPlayerFromId(source)
|
||||||
|
local Name = xPlayer.getName()
|
||||||
|
if Name then return Name end
|
||||||
|
end)
|
||||||
|
|
||||||
|
RegisterNetEvent('spy-bodycam:server:toggleList',function(bool)
|
||||||
|
local src = source
|
||||||
|
if bool then
|
||||||
|
if Config.Framework == 'qb' then
|
||||||
|
local Player = QBCore.Functions.GetPlayer(src)
|
||||||
|
local Name = Player.PlayerData.charinfo.firstname .. " " .. Player.PlayerData.charinfo.lastname
|
||||||
|
local Job = Player.PlayerData.job.label
|
||||||
|
local Rank = Player.PlayerData.job.grade.name
|
||||||
|
PlayerOnBodycam[src] = {
|
||||||
|
name = Name,
|
||||||
|
jobkey = Player.PlayerData.job.name,
|
||||||
|
job = Job,
|
||||||
|
rank = Rank,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
local xPlayer = ESX.GetPlayerFromId(src)
|
||||||
|
local Name = xPlayer.getName()
|
||||||
|
local Job = xPlayer.getJob().label
|
||||||
|
local Rank = xPlayer.getJob().grade_label
|
||||||
|
PlayerOnBodycam[src] = {
|
||||||
|
name = Name,
|
||||||
|
jobkey = xPlayer.getJob().name,
|
||||||
|
job = Job,
|
||||||
|
rank = Rank,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
GlobalState.PlayerOnBodycam = PlayerOnBodycam
|
||||||
|
else
|
||||||
|
PlayerOnBodycam[src] = nil
|
||||||
|
GlobalState.PlayerOnBodycam = PlayerOnBodycam
|
||||||
|
end
|
||||||
|
SetTimeout(1000, function()
|
||||||
|
TriggerClientEvent('spy-bodycam:updatelisteffect',-1)
|
||||||
|
end)
|
||||||
|
|
||||||
|
end)
|
||||||
|
|
||||||
|
RegisterNetEvent('spy-bodycam:server:toggleListCars',function(bool,entityId,carPlate,carName,carClass)
|
||||||
|
local src = source
|
||||||
|
if bool then
|
||||||
|
local veh = NetworkGetEntityFromNetworkId(entityId)
|
||||||
|
if DoesEntityExist(veh) then
|
||||||
|
local jobkey
|
||||||
|
local Name
|
||||||
|
if Config.Framework == 'qb' then
|
||||||
|
local Player = QBCore.Functions.GetPlayer(src)
|
||||||
|
jobkey = Player.PlayerData.job.name
|
||||||
|
Name = Player.PlayerData.charinfo.firstname .. " " .. Player.PlayerData.charinfo.lastname
|
||||||
|
else
|
||||||
|
local xPlayer = ESX.GetPlayerFromId(src)
|
||||||
|
jobkey = xPlayer.getJob().name
|
||||||
|
Name = xPlayer.getName()
|
||||||
|
end
|
||||||
|
CarsOnBodycam[entityId] = {
|
||||||
|
plate = carPlate,
|
||||||
|
carname = carName,
|
||||||
|
name = Name,
|
||||||
|
jobkey = jobkey,
|
||||||
|
carclass = carClass,
|
||||||
|
}
|
||||||
|
GlobalState.CarsOnBodycam = CarsOnBodycam
|
||||||
|
else
|
||||||
|
CarsOnBodycam[entityId] = nil
|
||||||
|
GlobalState.CarsOnBodycam = CarsOnBodycam
|
||||||
|
end
|
||||||
|
else
|
||||||
|
CarsOnBodycam[entityId] = nil
|
||||||
|
GlobalState.CarsOnBodycam = CarsOnBodycam
|
||||||
|
end
|
||||||
|
SetTimeout(1000, function()
|
||||||
|
TriggerClientEvent('spy-bodycam:updatelisteffectcar',-1)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
RegisterNetEvent('spy-bodycam:server:ReqDeleteDecoyPed',function()
|
||||||
|
if not Config.Dependency.UseAppearance then return end
|
||||||
|
local src = source
|
||||||
|
TriggerClientEvent('spy-bodycam:client:deleteDecoyPed',-1,src)
|
||||||
|
end)
|
||||||
|
|
||||||
|
RegisterNetEvent('spy-bodycam:server:ReqDecoyPed', function(cid, pedCoords)
|
||||||
|
if not Config.Dependency.UseAppearance then return end
|
||||||
|
local src = source
|
||||||
|
local result
|
||||||
|
local function handleDecoyPed(model, skin, pedCoords, src)
|
||||||
|
local nPlayers = lib.getNearbyPlayers(vector3(pedCoords.x, pedCoords.y, pedCoords.z), 150)
|
||||||
|
if nPlayers then
|
||||||
|
for i = 1, #nPlayers do
|
||||||
|
TriggerClientEvent('spy-bodycam:client:createDecoyPed', nPlayers[i].id, model, skin, pedCoords, src)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if Config.Framework == 'qb' then
|
||||||
|
result = MySQL.query.await('SELECT * FROM playerskins WHERE citizenid = ? AND active = ?', {cid, 1})
|
||||||
|
if result[1] ~= nil then
|
||||||
|
local skinData = json.decode(result[1].skin)
|
||||||
|
if Config.Dependency.UseAppearance == 'qb' then
|
||||||
|
handleDecoyPed(tonumber(result[1].model), skinData, pedCoords, src)
|
||||||
|
elseif Config.Dependency.UseAppearance == 'illenium' then
|
||||||
|
handleDecoyPed(joaat(skinData.model), skinData, pedCoords, src)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif Config.Framework == 'esx' then
|
||||||
|
result = MySQL.single.await("SELECT skin FROM users WHERE identifier = ?", {cid})
|
||||||
|
if result then
|
||||||
|
local skinData = json.decode(result.skin)
|
||||||
|
if Config.Dependency.UseAppearance == 'illenium' then
|
||||||
|
handleDecoyPed(joaat(skinData.model), skinData, pedCoords, src)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
Citizen.CreateThread(function()
|
||||||
|
if Config.Dependency.UseInventory == 'qb' then
|
||||||
|
QBCore.Functions.CreateUseableItem('bodycam', function(source, item)
|
||||||
|
TriggerClientEvent('spy-bodycam:bodycamstatus',source)
|
||||||
|
end)
|
||||||
|
QBCore.Functions.CreateUseableItem('dashcam', function(source, item)
|
||||||
|
TriggerClientEvent('spy-bodycam:toggleCarCam',source)
|
||||||
|
end)
|
||||||
|
elseif Config.Dependency.UseInventory == 'esx' then
|
||||||
|
ESX.RegisterUsableItem('bodycam', function(playerId)
|
||||||
|
TriggerClientEvent('spy-bodycam:bodycamstatus', playerId)
|
||||||
|
end)
|
||||||
|
ESX.RegisterUsableItem('dashcam', function(playerId)
|
||||||
|
TriggerClientEvent('spy-bodycam:toggleCarCam', playerId)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
RegisterNetEvent('spy-bodycam:server:logVideoDetails', function(videoUrl,streetName)
|
||||||
|
local src = source
|
||||||
|
local offName
|
||||||
|
local offJob
|
||||||
|
local offRank
|
||||||
|
local jobKey
|
||||||
|
local date = os.date('%Y-%m-%d')
|
||||||
|
|
||||||
|
if Config.Framework == 'qb' then
|
||||||
|
local Player = QBCore.Functions.GetPlayer(src)
|
||||||
|
offName = Player.PlayerData.charinfo.firstname .. " " .. Player.PlayerData.charinfo.lastname
|
||||||
|
offJob = Player.PlayerData.job.label
|
||||||
|
offRank = Player.PlayerData.job.grade.name
|
||||||
|
jobKey = Player.PlayerData.job.name
|
||||||
|
else
|
||||||
|
local xPlayer = ESX.GetPlayerFromId(src)
|
||||||
|
offName = xPlayer.getName()
|
||||||
|
offJob = xPlayer.getJob().label
|
||||||
|
offRank = xPlayer.getJob().grade_label
|
||||||
|
jobKey = xPlayer.getJob().name
|
||||||
|
end
|
||||||
|
|
||||||
|
---SQL UPLOAD
|
||||||
|
MySQL.Async.execute('INSERT INTO spy_bodycam (job, videolink, street, date, playername) VALUES (@job, @videolink, @street, @date, @playername)', {
|
||||||
|
['@job'] = jobKey,
|
||||||
|
['@videolink'] = videoUrl,
|
||||||
|
['@street'] = streetName,
|
||||||
|
['@date'] = date,
|
||||||
|
['@playername'] = offName
|
||||||
|
}, function(rowsChanged) end)
|
||||||
|
|
||||||
|
if Upload.DiscordLogs.Enabled then
|
||||||
|
local defwebhook
|
||||||
|
local author
|
||||||
|
if Upload.JobUploads[jobKey] then
|
||||||
|
defwebhook = Upload.JobUploads[jobKey].webhook
|
||||||
|
author = Upload.JobUploads[jobKey].author
|
||||||
|
else
|
||||||
|
defwebhook = Upload.DefaultUploads.webhook
|
||||||
|
author = Upload.DefaultUploads.author
|
||||||
|
end
|
||||||
|
local embedData = {
|
||||||
|
{
|
||||||
|
title = Upload.DiscordLogs.Title,
|
||||||
|
color = 16761035,
|
||||||
|
fields = {
|
||||||
|
{ name = "Name:", value = offName, inline = false },
|
||||||
|
{ name = "Job:", value = offJob, inline = false },
|
||||||
|
{ name = "Job Rank:", value = offRank, inline = false },
|
||||||
|
},
|
||||||
|
footer = {
|
||||||
|
text = "Date: " .. os.date("!%Y-%m-%d %H:%M:%S", os.time()),
|
||||||
|
icon_url = "https://i.imgur.com/CuSyeZT.png",
|
||||||
|
},
|
||||||
|
author = author
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PerformHttpRequest(defwebhook, function() end, 'POST', json.encode({ username = Upload.DiscordLogs.Username, embeds = embedData}), { ['Content-Type'] = 'application/json' })
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
RegisterNetEvent('spy-bodycam:server:deleteVideoDB', function(videoUrl)
|
||||||
|
local src = source
|
||||||
|
if not videoUrl or videoUrl == '' then return end
|
||||||
|
MySQL.Async.execute('DELETE FROM spy_bodycam WHERE videolink = @videolink', {
|
||||||
|
['@videolink'] = videoUrl
|
||||||
|
}, function(rowsChanged)
|
||||||
|
if rowsChanged > 0 then
|
||||||
|
local jobKey
|
||||||
|
local isBoss
|
||||||
|
if Config.Framework == 'qb' then
|
||||||
|
local Player = QBCore.Functions.GetPlayer(src)
|
||||||
|
jobKey = Player.PlayerData.job.name
|
||||||
|
isBoss = Player.PlayerData.job.isboss
|
||||||
|
else
|
||||||
|
local xPlayer = ESX.GetPlayerFromId(src)
|
||||||
|
jobKey = xPlayer.getJob().name
|
||||||
|
isBoss = (xPlayer.getJob().grade_name == 'boss')
|
||||||
|
end
|
||||||
|
MySQL.Async.fetchAll('SELECT * FROM spy_bodycam WHERE job = @job ORDER BY id DESC', {
|
||||||
|
['@job'] = jobKey
|
||||||
|
}, function(records)
|
||||||
|
TriggerClientEvent('spy-bodycam:client:refreshRecords', src, records, isBoss)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
RegisterNetEvent('spy-bodycam:server:showrecordingUI', function()
|
||||||
|
local src = source
|
||||||
|
local offJob
|
||||||
|
local jobKey
|
||||||
|
local isBoss
|
||||||
|
if Config.Framework == 'qb' then
|
||||||
|
local Player = QBCore.Functions.GetPlayer(src)
|
||||||
|
offJob = Player.PlayerData.job.label
|
||||||
|
jobKey = Player.PlayerData.job.name
|
||||||
|
isBoss = Player.PlayerData.job.isboss
|
||||||
|
else
|
||||||
|
local xPlayer = ESX.GetPlayerFromId(src)
|
||||||
|
offJob = xPlayer.getJob().label
|
||||||
|
jobKey = xPlayer.getJob().name
|
||||||
|
if xPlayer.getJob().grade_name == 'boss' then isBoss = true else isBoss = false end
|
||||||
|
end
|
||||||
|
MySQL.Async.fetchAll('SELECT * FROM spy_bodycam WHERE job = @job ORDER BY id DESC', {
|
||||||
|
['@job'] = jobKey
|
||||||
|
}, function(records)
|
||||||
|
TriggerClientEvent('spy-bodycam:client:openRecords', src, records, offJob, isBoss)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
lib.addCommand('recordcam', {
|
||||||
|
help = 'Record bodycam footage.',
|
||||||
|
restricted = false
|
||||||
|
}, function(source, args, raw)
|
||||||
|
local src = source
|
||||||
|
if PlayerOnBodycam[src] then
|
||||||
|
local defwebhook
|
||||||
|
if Upload.ServiceUsed == 'discord' then
|
||||||
|
local jobKey
|
||||||
|
if Config.Framework == 'qb' then
|
||||||
|
local Player = QBCore.Functions.GetPlayer(src)
|
||||||
|
jobKey = Player.PlayerData.job.name
|
||||||
|
else
|
||||||
|
local xPlayer = ESX.GetPlayerFromId(src)
|
||||||
|
jobKey = xPlayer.getJob().name
|
||||||
|
end
|
||||||
|
if Upload.JobUploads[jobKey] then
|
||||||
|
defwebhook = Upload.JobUploads[jobKey].webhook
|
||||||
|
else
|
||||||
|
defwebhook = Upload.DefaultUploads.webhook
|
||||||
|
end
|
||||||
|
elseif Upload.ServiceUsed == 'fivemanage' or Upload.ServiceUsed == 'fivemerr' then
|
||||||
|
defwebhook = Upload.Token
|
||||||
|
end
|
||||||
|
TriggerClientEvent('spy-bodycam:client:startRec',src,defwebhook,Upload.ServiceUsed)
|
||||||
|
else
|
||||||
|
NotifyPlayerSv('Bodycam not turned on!','error',3000,src)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
function NotifyPlayerSv(msg,type,time,src)
|
||||||
|
if Config.Dependency.UseNotify == 'ox' then
|
||||||
|
TriggerClientEvent('ox_lib:notify', src, { type = type or "success", title = '', description = msg, duration = time })
|
||||||
|
elseif Config.Dependency.UseNotify == 'qb' then
|
||||||
|
TriggerClientEvent("QBCore:Notify", src, msg, type,time)
|
||||||
|
elseif Config.Dependency.UseNotify == 'esx' then
|
||||||
|
TriggerClientEvent("esx:showNotification", src, msg, type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- You can remove this if u want more optimization from the script.
|
||||||
|
-- If the vehicle is deleted or send to garage it removes it from the list.
|
||||||
|
Citizen.CreateThread(function()
|
||||||
|
while true do
|
||||||
|
for entityId, _ in pairs(CarsOnBodycam) do
|
||||||
|
local veh = NetworkGetEntityFromNetworkId(entityId)
|
||||||
|
if not DoesEntityExist(veh) then
|
||||||
|
CarsOnBodycam[entityId] = nil
|
||||||
|
GlobalState.CarsOnBodycam = CarsOnBodycam
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Citizen.Wait(60000) -- Wait for 1 min before checking again
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Script Version Checker
|
||||||
|
local localVersion = GetResourceMetadata(GetCurrentResourceName(), 'version')
|
||||||
|
local fxManifestUrl = "https://raw.githubusercontent.com/Your-Spy/spy-bodycam/main/%5Bspy-bodycam%5D/spy-bodycam/fxmanifest.lua"
|
||||||
|
|
||||||
|
local function extractVersion(fxManifestContent)
|
||||||
|
for line in string.gmatch(fxManifestContent, "[^\r\n]+") do
|
||||||
|
if line:find("^version%s+'(.-)'$") then
|
||||||
|
return line:match("^version%s+'(.-)'$")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function checkForUpdates()
|
||||||
|
PerformHttpRequest(fxManifestUrl, function(statusCode, response, headers)
|
||||||
|
print([[^4
|
||||||
|
╔───────────────────────────────────────────────────────────────────────╗
|
||||||
|
____ ______ __ ____ ___ ______ ______ _ __ __
|
||||||
|
/ ___|| _ \ \ / / | __ ) / _ \| _ \ \ / / ___| / \ | \/ |
|
||||||
|
\___ \| |_) \ V / _____ | _ \| | | | | | \ V / | / _ \ | |\/| |
|
||||||
|
___) | __/ | | |_____| | |_) | |_| | |_| || || |___ / ___ \| | | |
|
||||||
|
|____/|_| |_| |____/ \___/|____/ |_| \____/_/ \_\_| |_|
|
||||||
|
|
||||||
|
╚───────────────────────────────────────────────────────────────────────╝
|
||||||
|
]])
|
||||||
|
if statusCode == 200 then
|
||||||
|
local remoteVersion = extractVersion(response)
|
||||||
|
if remoteVersion and remoteVersion ~= localVersion then
|
||||||
|
print("^2NEW UPDATE: ^2" .. remoteVersion .. "^3 | ^1CURRENT: " .. localVersion.." ^9>> Download new version from github")
|
||||||
|
else
|
||||||
|
print("^2You are on latest version: ^2" .. localVersion)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
print("^1Failed to check for updates. Status code: " .. statusCode)
|
||||||
|
end
|
||||||
|
end, "GET", "", {["Content-Type"] = "text/plain"})
|
||||||
|
end
|
||||||
|
|
||||||
|
AddEventHandler('onResourceStart', function(resourceName)
|
||||||
|
if GetCurrentResourceName() == resourceName then
|
||||||
|
checkForUpdates()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
Upload = Upload or {}
|
||||||
|
|
||||||
|
-- U have to start the recording using /recordcam and the recording will be uploaded to any of the service below.
|
||||||
|
|
||||||
|
Upload.ServiceUsed = 'fivemanage' -- discord | fivemanage | fivemerr
|
||||||
|
Upload.Token = 'c6sTqXXzOJksi7hKs0vMFuN8yUmJfcb1' -- fivemanage or fivemerr | [*note - for discord webhook is to be changed below not here]
|
||||||
|
|
||||||
|
-- FOR DISCORD LOGS
|
||||||
|
Upload.DiscordLogs = {
|
||||||
|
Enabled = false,
|
||||||
|
Username = 'Spy Bodycam Records', -- Bot Username
|
||||||
|
Title = 'Bodycam Records', -- Message Title
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Upload Hooks if Upload.ServiceUsed = discord
|
||||||
|
Upload.DefaultUploads = { -- Default Upload of log if job not mentioned in Upload.JobUploads.
|
||||||
|
webhook = 'YOUR_WEBHOOK',
|
||||||
|
author = {
|
||||||
|
name = "Spy Bodycam",
|
||||||
|
icon_url = "https://i.imgur.com/tMyAdkz.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Upload.JobUploads = { -- Job Speific Uploads
|
||||||
|
['police'] = {
|
||||||
|
webhook = 'YOUR_WEBHOOK',
|
||||||
|
author = {
|
||||||
|
name = "Police Department",
|
||||||
|
icon_url = "https://i.imgur.com/tMyAdkz.png"
|
||||||
|
}
|
||||||
|
}, -- Add more here
|
||||||
|
}
|
After Width: | Height: | Size: 84 KiB |
|
@ -0,0 +1,87 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Spy Bodycam</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas style="display: none;" width="1280px" height="720px"></canvas>
|
||||||
|
<div class="overlayCont">
|
||||||
|
<div class="overlayBody">
|
||||||
|
<div class="bodyInfobox">
|
||||||
|
<div class="bodyDate"><i class="fa-solid fa-circle fa-fade bodyIcon"></i></i>08-04 23:58:45-0500</div>
|
||||||
|
<div class="bodyNum">BODY 22 X6070511N</div>
|
||||||
|
<div class="bodyCallsign">(222) Spy Dev</div>
|
||||||
|
</div>
|
||||||
|
<img src="images/brand_logo.png" alt="">
|
||||||
|
<div class="AddonIcons"><i class="fa-solid fa-battery-three-quarters"></i><i class="fa-solid fa-wifi"></i></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="RecordInfo">
|
||||||
|
<div class="recordBox">
|
||||||
|
<div class="HeadText"><i class="fa-solid fa-video fa-fade bodyIcon"></i>Recording Started</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="currWatchCont">
|
||||||
|
<div class="watchInfo">
|
||||||
|
<div class="userLaber">
|
||||||
|
<i class="fa-solid fa-video watchIcon" style="color: #cf0707;"></i>
|
||||||
|
<span class="watchInText typeCam">Bodycam</span>
|
||||||
|
<br>
|
||||||
|
<span class="watchInText plateText">Plate : 726G21</span>
|
||||||
|
<br>
|
||||||
|
<span class="watchInText carText">Car : Crown Vic</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="backInfo">
|
||||||
|
<div class="backLabel"><i class="fa-solid fa-video-slash backIcon"></i><span class="backInText">E</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="recCont">
|
||||||
|
<div class="recMain">
|
||||||
|
<div class="recTitle">
|
||||||
|
<div class="recTitleTxt">Bodycam Recordings</div>
|
||||||
|
<div class="recDesc">POLICE DATABASE</div>
|
||||||
|
<img src="images/brand_logo.png" alt="" class="recLogo">
|
||||||
|
</div>
|
||||||
|
<div class="RecBox">
|
||||||
|
<div class="recInnerScroll"></div>
|
||||||
|
</div>
|
||||||
|
<div class="recFilters">
|
||||||
|
<div class="recSearch">
|
||||||
|
<div class="searchText"><i class="fas fa-search"></i></div>
|
||||||
|
<input type="text" class="searchInput" placeholder="Seach Footage...">
|
||||||
|
</div>
|
||||||
|
<div class="recDateF">
|
||||||
|
<input type="date" class="selectedDate" placeholder="Select Date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vidplaycont">
|
||||||
|
<div class="vidMain">
|
||||||
|
<video class="vidPlayer" controls autoplay loop>
|
||||||
|
<source src="https://cdn.discordapp.com/attachments/1257923291512045568/1263565598277763082/video.webm?ex=669ab2aa&is=6699612a&hm=a6afae0dd5179e186a56fecdb506a85f3769ad9db5b33ec14efede37454a1ba7&" type="video/webm">
|
||||||
|
</video>
|
||||||
|
<div class="vidPlayerIcon"><i class="fa-solid fa-circle-xmark"></i></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="askMain">
|
||||||
|
<div class="askIn">
|
||||||
|
<div class="askIcon"><i class="fa-solid fa-trash fa-bounce"></i></div>
|
||||||
|
<h1 class="askTitle">Are you sure?</h1>
|
||||||
|
<div class="askBtnCont">
|
||||||
|
<div class="askBtn">Yes</div>
|
||||||
|
<div class="askBtn">No</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<audio id="beep-sound" src="audio/on_sound.mp3"></audio>
|
||||||
|
<audio id="off-sound" src="audio/off_sound.mp3"></audio>
|
||||||
|
<script src="js/jquery-3.7.1.min.js"></script>
|
||||||
|
<script type="module" src="js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,394 @@
|
||||||
|
import { GameView } from './gameview.js';
|
||||||
|
const gameview = new GameView();
|
||||||
|
let isRecording = false;
|
||||||
|
let recordingTimeout;
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('.overlayCont').hide();
|
||||||
|
$('.currWatchCont').hide();
|
||||||
|
$('.RecordInfo').hide();
|
||||||
|
$('.recCont').hide();
|
||||||
|
$('.askMain').hide();
|
||||||
|
$('.vidplaycont').hide();
|
||||||
|
const beepSound = document.getElementById('beep-sound');
|
||||||
|
const offSound = document.getElementById('off-sound');
|
||||||
|
|
||||||
|
function updateTime() {
|
||||||
|
const date = new Date();
|
||||||
|
date.setUTCHours(date.getUTCHours() - 5);
|
||||||
|
|
||||||
|
const month = ("0" + (date.getMonth() + 1)).slice(-2);
|
||||||
|
const day = ("0" + date.getDate()).slice(-2);
|
||||||
|
const hours = ("0" + date.getHours()).slice(-2);
|
||||||
|
const minutes = ("0" + date.getMinutes()).slice(-2);
|
||||||
|
const seconds = ("0" + date.getSeconds()).slice(-2);
|
||||||
|
|
||||||
|
const gameTime = `${month}-${day} ${hours}:${minutes}:${seconds}-0500`;
|
||||||
|
|
||||||
|
$('.bodyDate').html('<i class="fa-solid fa-circle fa-fade bodyIcon"></i>' + gameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
let interval;
|
||||||
|
|
||||||
|
window.addEventListener('message', function (event) {
|
||||||
|
const data = event.data;
|
||||||
|
if (data.action === 'open') {
|
||||||
|
updateTime();
|
||||||
|
interval = setInterval(updateTime, 1000);
|
||||||
|
$('.bodyNum').text(data.bodyname);
|
||||||
|
$('.bodyCallsign').text(data.callsign);
|
||||||
|
$('.overlayCont').removeClass('popOut').addClass('popIn').show();
|
||||||
|
beepSound.play();
|
||||||
|
} else if (data.action === 'close') {
|
||||||
|
clearInterval(interval);
|
||||||
|
$('.overlayCont').removeClass('popIn').addClass('popOut').one('animationend', function () {
|
||||||
|
$(this).hide();
|
||||||
|
$(this).removeClass('popOut');
|
||||||
|
});
|
||||||
|
offSound.play();
|
||||||
|
}
|
||||||
|
if (data.action === 'openWatch') {
|
||||||
|
let typeCam;
|
||||||
|
let plateText;
|
||||||
|
let carText;
|
||||||
|
|
||||||
|
if (data.debug) {
|
||||||
|
var controlsHtml = `
|
||||||
|
<span class="watchInText typeCam">Controls</span>
|
||||||
|
<br>
|
||||||
|
<span class="watchInText typeCam">W -> Up</span>
|
||||||
|
<br>
|
||||||
|
<span class="watchInText typeCam">S -> Down</span>
|
||||||
|
<br>
|
||||||
|
<span class="watchInText typeCam">A -> Left</span>
|
||||||
|
<br>
|
||||||
|
<span class="watchInText typeCam">D -> Right</span>
|
||||||
|
<br>
|
||||||
|
<span class="watchInText typeCam">Q -> Forward</span>
|
||||||
|
<br>
|
||||||
|
<span class="watchInText typeCam">E -> Backward</span>
|
||||||
|
`;
|
||||||
|
document.querySelector('.userLaber').innerHTML = controlsHtml;
|
||||||
|
} else {
|
||||||
|
if (data.isbodycam) {
|
||||||
|
typeCam = "BODYCAM";
|
||||||
|
plateText = "CamID: " + data.bodyId;
|
||||||
|
carText = "Name: " + data.name;
|
||||||
|
} else {
|
||||||
|
typeCam = "DASHCAM";
|
||||||
|
plateText = "Plate: " + data.bodyId;
|
||||||
|
carText = "Car: " + data.name;
|
||||||
|
}
|
||||||
|
document.querySelector('.typeCam').textContent = typeCam;
|
||||||
|
document.querySelector('.plateText').textContent = plateText;
|
||||||
|
document.querySelector('.carText').textContent = carText;
|
||||||
|
}
|
||||||
|
$('.backInText').text(data.exitKey);
|
||||||
|
$('.currWatchCont').fadeIn();
|
||||||
|
} else if (data.action === 'closeWatch') {
|
||||||
|
$('.currWatchCont').fadeOut();
|
||||||
|
}
|
||||||
|
if (data.action === 'toggle_record') {
|
||||||
|
clearTimeout(recordingTimeout);
|
||||||
|
if (!isRecording) {
|
||||||
|
// Start recording
|
||||||
|
isRecording = true;
|
||||||
|
$('.HeadText').html('<i class="fa-solid fa-circle fa-fade bodyIcon" style="color: rgb(173, 8, 8);"></i>' + 'Recording Started');
|
||||||
|
$('.RecordInfo').fadeIn();
|
||||||
|
startRecording(data.hook, data.service);
|
||||||
|
recordingTimeout = setTimeout(() => {
|
||||||
|
if (isRecording) {
|
||||||
|
// Stop recording automatically after 30 seconds
|
||||||
|
isRecording = false;
|
||||||
|
$('.HeadText').html('<i class="fa-solid fa-circle bodyIcon" style="color: white;"></i>' + 'Recording Stopped');
|
||||||
|
setTimeout(() => {
|
||||||
|
$('.RecordInfo').fadeOut();
|
||||||
|
}, 2000);
|
||||||
|
stopRecording();
|
||||||
|
}
|
||||||
|
}, data.recTiming * 1000);
|
||||||
|
} else {
|
||||||
|
// Stop recording
|
||||||
|
isRecording = false;
|
||||||
|
$('.HeadText').html('<i class="fa-solid fa-circle bodyIcon" style="color: white;"></i>' + 'Recording Stopped');
|
||||||
|
clearTimeout(recordingTimeout);
|
||||||
|
setTimeout(() => {
|
||||||
|
$('.RecordInfo').fadeOut();
|
||||||
|
}, 2000);
|
||||||
|
stopRecording();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.action === 'cancel_rec_force') {
|
||||||
|
if (isRecording) {
|
||||||
|
// Force stop recording
|
||||||
|
isRecording = false;
|
||||||
|
$('.HeadText').html('<i class="fa-solid fa-circle bodyIcon" style="color: white;"></i>' + 'Recording Stopped');
|
||||||
|
clearTimeout(recordingTimeout);
|
||||||
|
setTimeout(() => {
|
||||||
|
$('.RecordInfo').fadeOut();
|
||||||
|
}, 1000);
|
||||||
|
stopRecording();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RECORDS SHOWING
|
||||||
|
if (data.action === 'show_records') {
|
||||||
|
$('.recInnerScroll').empty();
|
||||||
|
$('.recDesc').text(`${data.jobTitle} Database`);
|
||||||
|
|
||||||
|
if (Array.isArray(data.recordData) && data.recordData.length > 0) {
|
||||||
|
$.each(data.recordData, function (index, record) {
|
||||||
|
var camBox = $(
|
||||||
|
`
|
||||||
|
<div class="camBox">
|
||||||
|
<div class="camInfo">
|
||||||
|
<div class="camTitle">${record.playername}<span class="camStreet"> [${record.street}]</span></div>
|
||||||
|
<div class="camDesc">Date: ${record.date}</div>
|
||||||
|
</div>
|
||||||
|
<div class="camIcons">
|
||||||
|
<div class="camShow" data-stored="${record.videolink}"><i class="fa-solid fa-eye"></i></div>
|
||||||
|
${data.isBoss ? `<div class="camDelete"><i class="fa-solid fa-trash"></i></div>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
$('.recInnerScroll').append(camBox);
|
||||||
|
});
|
||||||
|
$('.recCont').show();
|
||||||
|
} else {
|
||||||
|
$('.recInnerScroll').html('<h1 class="noRecAv">No records available</h1>');
|
||||||
|
$('.recCont').show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh Records
|
||||||
|
if (data.action === 'refreshrec') {
|
||||||
|
$('.recInnerScroll').empty();
|
||||||
|
if (Array.isArray(data.recordData) && data.recordData.length > 0) {
|
||||||
|
$.each(data.recordData, function (index, record) {
|
||||||
|
var camBox = $(
|
||||||
|
`
|
||||||
|
<div class="camBox">
|
||||||
|
<div class="camInfo">
|
||||||
|
<div class="camTitle">${record.playername}<span class="camStreet"> [${record.street}]</span></div>
|
||||||
|
<div class="camDesc">Date: ${record.date}</div>
|
||||||
|
</div>
|
||||||
|
<div class="camIcons">
|
||||||
|
<div class="camShow" data-stored="${record.videolink}"><i class="fa-solid fa-eye"></i></div>
|
||||||
|
${data.isBoss ? `<div class="camDelete"><i class="fa-solid fa-trash"></i></div>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
$('.recInnerScroll').append(camBox);
|
||||||
|
});
|
||||||
|
$('.recCont').show();
|
||||||
|
} else {
|
||||||
|
$('.recInnerScroll').html('<h1 class="noRecAv">No records available</h1>');
|
||||||
|
$('.recCont').show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let deleteUrl = '';
|
||||||
|
|
||||||
|
// SEARCH AND DATE FILTERS :)
|
||||||
|
function updateVisibility(searchText) {
|
||||||
|
$('.camBox').each(function () {
|
||||||
|
var camTitleText = $(this).find('.camTitle').text().toLowerCase();
|
||||||
|
var camDescDate = $(this).find('.camDesc').text().trim().replace('Date: ', '');
|
||||||
|
if ((searchText === '' || camTitleText.includes(searchText)) &&
|
||||||
|
($(this).css('display') !== 'none' || camDescDate === $('.selectedDate').val())
|
||||||
|
) {
|
||||||
|
$(this).show();
|
||||||
|
}
|
||||||
|
else if ((searchText === '' || camTitleText.includes(searchText)) &&
|
||||||
|
($('.selectedDate').val() === '')
|
||||||
|
) {
|
||||||
|
$(this).show();
|
||||||
|
} else {
|
||||||
|
$(this).hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.searchInput').on('input', function () {
|
||||||
|
var searchText = $(this).val().toLowerCase();
|
||||||
|
updateVisibility(searchText);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.selectedDate').on('change', function () {
|
||||||
|
var selectedDate = $(this).val();
|
||||||
|
$('.camBox').each(function () {
|
||||||
|
var camDescDate = $(this).find('.camDesc').text().trim().replace('Date: ', '');
|
||||||
|
if (selectedDate === '') {
|
||||||
|
$(this).show();
|
||||||
|
} else if (selectedDate === camDescDate) {
|
||||||
|
$(this).show();
|
||||||
|
} else {
|
||||||
|
$(this).hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var searchText = $('.searchInput').val().toLowerCase();
|
||||||
|
updateVisibility(searchText);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.camDelete', function () {
|
||||||
|
const camBox = $(this).closest('.camBox');
|
||||||
|
deleteUrl = camBox.find('.camShow').data('stored');
|
||||||
|
$('.askMain').fadeIn();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.askBtn:nth-child(1)', function () {
|
||||||
|
$('.askMain').fadeOut();
|
||||||
|
if (deleteUrl) {
|
||||||
|
$.post(`https://${GetParentResourceName()}/deleteVideo`, JSON.stringify({
|
||||||
|
vidurl: deleteUrl
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.askBtn:nth-child(2)', function () {
|
||||||
|
$('.askMain').fadeOut();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.camShow', function () {
|
||||||
|
var videoUrl = $(this).data('stored');
|
||||||
|
$('.vidPlayer').attr('src', videoUrl);
|
||||||
|
$('.vidplaycont').show();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.vidPlayerIcon', function () {
|
||||||
|
$('.vidplaycont').hide();
|
||||||
|
$('.vidPlayer').attr('src', '');
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', function (event) {
|
||||||
|
if ($(event.target).is('.vidplaycont')) {
|
||||||
|
$('.vidplaycont').hide();
|
||||||
|
$('.vidPlayer').attr('src', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('keydown', function (e) {
|
||||||
|
if (e.which === 27) {
|
||||||
|
$('.askMain').hide();
|
||||||
|
$('.searchInput').val('');
|
||||||
|
$('.selectedDate').val('');
|
||||||
|
$('.recCont').hide();
|
||||||
|
$('.vidplaycont').hide();
|
||||||
|
$('.vidPlayer').attr('src', '');
|
||||||
|
$.post(`https://${GetParentResourceName()}/closeRecUI`, '{}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let mediaRecorder;
|
||||||
|
let audioStream;
|
||||||
|
const canvasElement = document.querySelector('canvas');
|
||||||
|
|
||||||
|
async function uploadBlob(videoBlob, hook, service) {
|
||||||
|
$.post(`https://${GetParentResourceName()}/exitBodyCam`, '{}');
|
||||||
|
const formData = new FormData();
|
||||||
|
try {
|
||||||
|
let response, responseData;
|
||||||
|
if (service === 'fivemanage') {
|
||||||
|
formData.append('video', videoBlob);
|
||||||
|
response = await fetch('https://api.fivemanage.com/api/video', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: hook,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to upload video to FiveManage: ${response.status}`);
|
||||||
|
}
|
||||||
|
responseData = await response.json();
|
||||||
|
$.post(`https://${GetParentResourceName()}/videoLog`, JSON.stringify({
|
||||||
|
vidurl: responseData.url
|
||||||
|
}));
|
||||||
|
} else if (service === 'fivemerr') {
|
||||||
|
formData.append('file', videoBlob, 'video.webm');
|
||||||
|
response = await fetch('https://api.fivemerr.com/v1/media/videos', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: hook,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to upload video to Fivemerr: ${response.status}`);
|
||||||
|
}
|
||||||
|
responseData = await response.json();
|
||||||
|
$.post(`https://${GetParentResourceName()}/videoLog`, JSON.stringify({
|
||||||
|
vidurl: responseData.url
|
||||||
|
}));
|
||||||
|
} else if (service === 'discord') {
|
||||||
|
formData.append('file', videoBlob, 'video.webm');
|
||||||
|
response = await fetch(hook, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to upload video to Discord: ${response.status}`);
|
||||||
|
}
|
||||||
|
responseData = await response.json();
|
||||||
|
$.post(`https://${GetParentResourceName()}/videoLog`, JSON.stringify({
|
||||||
|
vidurl: responseData.attachments[0].url
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('^1[ERROR]:^3', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startMicrophoneCapture() {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
return stream;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Microphone capture failed. Recording video only.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startRecording(hook, service) {
|
||||||
|
audioStream = await startMicrophoneCapture();
|
||||||
|
if (!isRecording){
|
||||||
|
if (audioStream) {
|
||||||
|
audioStream.getAudioTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('Video Recording Started');
|
||||||
|
const gameView = gameview.createGameView(canvasElement);
|
||||||
|
const canvasStream = canvasElement.captureStream(30);
|
||||||
|
|
||||||
|
// Combine audio and video streams
|
||||||
|
const combinedStream = new MediaStream([
|
||||||
|
...canvasStream.getVideoTracks(),
|
||||||
|
...(audioStream ? audioStream.getAudioTracks() : [])
|
||||||
|
]);
|
||||||
|
|
||||||
|
const videoChunks = [];
|
||||||
|
window.gameView = gameView;
|
||||||
|
mediaRecorder = new MediaRecorder(combinedStream, { mimeType: 'video/webm;codecs=vp9' });
|
||||||
|
mediaRecorder.start();
|
||||||
|
mediaRecorder.ondataavailable = (e) => e.data.size > 0 && videoChunks.push(e.data);
|
||||||
|
mediaRecorder.onstop = async () => {
|
||||||
|
const videoBlob = new Blob(videoChunks, { type: 'video/webm' });
|
||||||
|
if (videoBlob.size > 0) {
|
||||||
|
uploadBlob(videoBlob, hook, service);
|
||||||
|
}
|
||||||
|
if (audioStream) {
|
||||||
|
audioStream.getAudioTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopRecording() {
|
||||||
|
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
||||||
|
mediaRecorder.stop();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
export class GameView {
|
||||||
|
constructor() {
|
||||||
|
this.vertexShaderSrc = `
|
||||||
|
attribute vec2 a_position;
|
||||||
|
attribute vec2 a_texcoord;
|
||||||
|
uniform mat3 u_matrix;
|
||||||
|
varying vec2 textureCoordinate;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||||
|
textureCoordinate = a_texcoord;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.fragmentShaderSrc = `
|
||||||
|
varying highp vec2 textureCoordinate;
|
||||||
|
uniform sampler2D external_texture;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_FragColor = texture2D(external_texture, textureCoordinate);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.interval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeShader = (gl, type, src) => {
|
||||||
|
const shader = gl.createShader(type);
|
||||||
|
gl.shaderSource(shader, src);
|
||||||
|
gl.compileShader(shader);
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
createTexture(gl) {
|
||||||
|
const tex = gl.createTexture();
|
||||||
|
|
||||||
|
const texPixels = new Uint8Array([0, 0, 255, 255]);
|
||||||
|
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||||
|
gl.texImage2D(
|
||||||
|
gl.TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
gl.RGBA,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
gl.RGBA,
|
||||||
|
gl.UNSIGNED_BYTE,
|
||||||
|
texPixels,
|
||||||
|
);
|
||||||
|
|
||||||
|
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
|
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
// Magic hook sequence
|
||||||
|
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);
|
||||||
|
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
createBuffers = (gl) => {
|
||||||
|
const vertexBuff = gl.createBuffer();
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuff);
|
||||||
|
gl.bufferData(
|
||||||
|
gl.ARRAY_BUFFER,
|
||||||
|
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
||||||
|
gl.STATIC_DRAW,
|
||||||
|
);
|
||||||
|
|
||||||
|
const texBuff = gl.createBuffer();
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, texBuff);
|
||||||
|
gl.bufferData(
|
||||||
|
gl.ARRAY_BUFFER,
|
||||||
|
new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]),
|
||||||
|
gl.STATIC_DRAW,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { vertexBuff, texBuff };
|
||||||
|
}
|
||||||
|
|
||||||
|
createProgram = (gl) => {
|
||||||
|
const vertexShader = this.makeShader(gl, gl.VERTEX_SHADER, this.vertexShaderSrc);
|
||||||
|
const fragmentShader = this.makeShader(gl, gl.FRAGMENT_SHADER, this.fragmentShaderSrc);
|
||||||
|
|
||||||
|
const program = gl.createProgram();
|
||||||
|
|
||||||
|
gl.attachShader(program, vertexShader);
|
||||||
|
gl.attachShader(program, fragmentShader);
|
||||||
|
gl.linkProgram(program);
|
||||||
|
gl.useProgram(program);
|
||||||
|
|
||||||
|
const vloc = gl.getAttribLocation(program, 'a_position');
|
||||||
|
const tloc = gl.getAttribLocation(program, 'a_texcoord');
|
||||||
|
|
||||||
|
return { program, vloc, tloc };
|
||||||
|
}
|
||||||
|
|
||||||
|
createStuff(gl) {
|
||||||
|
const tex = this.createTexture(gl);
|
||||||
|
const { program, vloc, tloc } = this.createProgram(gl);
|
||||||
|
const { vertexBuff, texBuff } = this.createBuffers(gl);
|
||||||
|
|
||||||
|
gl.useProgram(program);
|
||||||
|
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||||
|
|
||||||
|
gl.uniform1i(gl.getUniformLocation(program, 'external_texture'), 0);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuff);
|
||||||
|
gl.vertexAttribPointer(vloc, 2, gl.FLOAT, false, 0, 0);
|
||||||
|
gl.enableVertexAttribArray(vloc);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, texBuff);
|
||||||
|
gl.vertexAttribPointer(tloc, 2, gl.FLOAT, false, 0, 0);
|
||||||
|
gl.enableVertexAttribArray(tloc);
|
||||||
|
|
||||||
|
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(gl, gameView) {
|
||||||
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||||
|
gl.finish();
|
||||||
|
|
||||||
|
let render = () => {};
|
||||||
|
gameView.animationFrame = requestAnimationFrame(render);
|
||||||
|
}
|
||||||
|
|
||||||
|
createGameView = (canvas) => {
|
||||||
|
this.canvas = canvas;
|
||||||
|
const gl = this.canvas.getContext('webgl', {
|
||||||
|
antialias: false,
|
||||||
|
depth: false,
|
||||||
|
stencil: false,
|
||||||
|
alpha: false,
|
||||||
|
desynchronized: true,
|
||||||
|
failIfMajorPerformanceCaveat: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const gameView = {
|
||||||
|
canvas,
|
||||||
|
gl,
|
||||||
|
animationFrame: undefined,
|
||||||
|
resize: (width, height) => {
|
||||||
|
gl.viewport(0, 0, width, height);
|
||||||
|
gl.canvas.width = width;
|
||||||
|
gl.canvas.height = height;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.createStuff(gl);
|
||||||
|
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
this.render(gl, gameView);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return gameView;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.canvas) {
|
||||||
|
if (this.canvas.style.display != "none") {
|
||||||
|
this.canvas.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
resources/[jobs]/[police]/[spy-bodycam]/spy-bodycam/web/js/jquery-3.7.1.min.js
vendored
Normal file
|
@ -0,0 +1,451 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Varela+Round&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Play:wght@400;700&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Fjalla+One&family=PT+Sans+Narrow:wght@400;700&family=Play:wght@400;700&display=swap');
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0%;
|
||||||
|
padding: 0%;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayCont{
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayBody {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-right: 2rem;
|
||||||
|
background: rgba(0, 0, 0, 0.486);
|
||||||
|
box-shadow: -2px 0px 46px -9px rgba(0, 0, 0, 0.644) inset, 0px 4px 8px rgba(0, 0, 0, 0.418);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RecordInfo{
|
||||||
|
display: flex;
|
||||||
|
margin-top: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.recordBox{
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-right: 2rem;
|
||||||
|
font-family: "Play", sans-serif;
|
||||||
|
background: rgba(0, 0, 0, 0.486);
|
||||||
|
box-shadow: -2px 0px 46px -9px rgba(161, 2, 2, 0.644) inset, 0px 4px 8px rgba(0, 0, 0, 0.418);
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyInfobox {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.overlayBody img{
|
||||||
|
width: 66px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.bodyNum , .bodyCallsign {
|
||||||
|
font-family: "Play", sans-serif;
|
||||||
|
}
|
||||||
|
.bodyIcon{
|
||||||
|
margin-right: 15px;
|
||||||
|
color: rgb(173, 8, 8);
|
||||||
|
}
|
||||||
|
.bodyDate{
|
||||||
|
font-size: 17px;
|
||||||
|
font-family: "Varela Round", sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.AddonIcons{
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 9px;
|
||||||
|
left: 20px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.currWatchCont{
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchInText{
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.watchIcon{
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.userLaber {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
font-family: "Play", sans-serif;
|
||||||
|
font-size: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
text-shadow: -1px 1px 1px rgba(0, 0, 0, 0.55);
|
||||||
|
background: rgba(0, 0, 0, 0.397);
|
||||||
|
box-shadow: -2px 0px 46px -9px rgba(0, 0, 0, 0.57) inset,0px 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.backInText{
|
||||||
|
background: rgb(27, 27, 27);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.backIcon{
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.backLabel {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
font-family: "Play", sans-serif;
|
||||||
|
font-size: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
text-shadow: -1px 1px 1px rgba(0, 0, 0, 0.55);
|
||||||
|
background: rgba(0, 0, 0, 0.397);
|
||||||
|
box-shadow: -2px 0px 46px -9px rgba(0, 0, 0, 0.57) inset,0px 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes popIn {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes popOut {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popIn {
|
||||||
|
animation: popIn 0.5s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popOut {
|
||||||
|
animation: popOut 0.5s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recCont{
|
||||||
|
position: absolute;
|
||||||
|
left: 10%;
|
||||||
|
top: 10%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.recMain{
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 80vw;
|
||||||
|
height: 80vh;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recTitleTxt{
|
||||||
|
font-family: "Bebas Neue", sans-serif;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
.recTitle img{
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 9px;
|
||||||
|
margin-right: 20px;
|
||||||
|
width: 100px;
|
||||||
|
right: 0%;
|
||||||
|
top: 0%;
|
||||||
|
}
|
||||||
|
.recTitle{
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
background: #353b48;
|
||||||
|
box-shadow: -2px 0px 46px -9px rgba(0, 0, 0, 0.644) inset, 0px 4px 8px rgba(0, 0, 0, 0.418);
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
}
|
||||||
|
.recDesc{
|
||||||
|
font-family: "PT Sans Narrow", sans-serif;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
.RecBox {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 63vh;
|
||||||
|
background: #101522;
|
||||||
|
box-shadow: -1px 15px 13px -1px rgba(0,0,0,0.5) inset, 0px -13px 13px -1px rgba(0,0,0,0.5) inset;
|
||||||
|
}
|
||||||
|
.recInnerScroll{
|
||||||
|
display: flex;
|
||||||
|
align-self: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
width: 100%;
|
||||||
|
height: 55vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recInnerScroll::-webkit-scrollbar {
|
||||||
|
width: 3px;
|
||||||
|
}
|
||||||
|
.recInnerScroll::-webkit-scrollbar-thumb {
|
||||||
|
background: #353b48;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recInnerScroll::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #242931;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camBox{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 90%;
|
||||||
|
background: linear-gradient(180deg,#353b48 0%, #23272f 80%);
|
||||||
|
box-shadow: 2px 2px 13px 7px rgba(0, 0, 0, 0.171) inset, -1px 7px 13px -4px rgba(0,0,0,0.42);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #c9cacc18;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camShow:hover{
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 4px 1px 5px rgba(0, 0, 0, 0.315);
|
||||||
|
}
|
||||||
|
.camDelete:hover{
|
||||||
|
color: #ca3333;
|
||||||
|
text-shadow: 4px 1px 5px rgba(0, 0, 0, 0.315);
|
||||||
|
}
|
||||||
|
.camBox:hover{
|
||||||
|
box-shadow: 2px 2px 13px 7px rgba(0, 0, 0, 0.411) inset,-1px 9px 13px -4px rgba(0,0,0,0.5);
|
||||||
|
border: 1px solid #c9cacc3f;
|
||||||
|
}
|
||||||
|
.camIcons{
|
||||||
|
color: #999999;
|
||||||
|
display:flex ;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
text-shadow: 4px 1px 5px rgba(0, 0, 0, 0.123);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.camDesc{
|
||||||
|
font-family: "Play", sans-serif;
|
||||||
|
}
|
||||||
|
.camTitle{
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-family: "Fjalla One", sans-serif;
|
||||||
|
}
|
||||||
|
.camStreet{
|
||||||
|
color: rgb(170, 170, 170);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: "Fjalla One", sans-serif;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.recFilters {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 5rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
color: white;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
background-color: #353b48;
|
||||||
|
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.418);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recSearch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchText i {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput {
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
outline: none;
|
||||||
|
background-color: #2f3640;
|
||||||
|
color: white;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: "Play", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recDateF {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: white;
|
||||||
|
font-family: "Play", sans-serif;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedDate {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
outline: none;
|
||||||
|
background-color: #2f3640;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: "Play", sans-serif;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedDate::-webkit-calendar-picker-indicator {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vidplaycont {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vidMain {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(180deg,#353b48 0%, #23272f 80%);
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 63vw;
|
||||||
|
height: 60vh;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vidPlayer {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vidPlayerIcon {
|
||||||
|
position: absolute;
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
top: 19px;
|
||||||
|
right: 17px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vidPlayerIcon:hover {
|
||||||
|
color: #ca3333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.askMain{
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 80vw;
|
||||||
|
height: 80vh;
|
||||||
|
left: 10%;
|
||||||
|
top: 10%;
|
||||||
|
}
|
||||||
|
.askIn{
|
||||||
|
background: linear-gradient(180deg,#353b48 0%, #23272f 80%);
|
||||||
|
box-shadow: 2px 2px 13px 7px rgba(0, 0, 0, 0.171) inset, -1px 7px 13px -4px rgba(0,0,0,0.42);
|
||||||
|
border: 1px solid #c9cacc18;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.askIn:hover{
|
||||||
|
box-shadow: 2px 2px 13px 7px rgba(0, 0, 0, 0.411) inset,-1px 9px 13px -4px rgba(0,0,0,0.5);
|
||||||
|
border: 1px solid #c9cacc3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.askIcon{
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #ca3333;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.askBtnCont{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: 100%;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.askBtn{
|
||||||
|
color: white;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-family: "Play", sans-serif;
|
||||||
|
background: #1015225d;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.askBtn:hover{
|
||||||
|
background: #101522bb;
|
||||||
|
}
|
||||||
|
.askTitle{
|
||||||
|
color: white;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-family: "Fjalla One", sans-serif;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.noRecAv{
|
||||||
|
color: rgba(255, 255, 255, 0.26);
|
||||||
|
font-size: 3rem;
|
||||||
|
font-family: "Fjalla One", sans-serif;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
fx_version 'cerulean'
|
||||||
|
|
||||||
|
game 'gta5'
|
||||||
|
lua54 'yes'
|
||||||
|
author "aarjey0_0"
|
||||||
|
|
||||||
|
data_file 'DLC_ITYP_REQUEST' 'stream/rj_bodycam.ytyp'
|