mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 02:01:40 +00:00
e58f1b6c18
Resolve configured and regional locales through a centralized English fallback for Lua and NUI consumers. Complete the German and English app translation keys, localize remaining picker and attachment copy, and add contract tests that detect missing locale entries.
1043 lines
32 KiB
Lua
1043 lines
32 KiB
Lua
local is_open = false
|
|
local open_requested = false
|
|
local notification_focus = false
|
|
local call_focus = false
|
|
local payphone_focus = false
|
|
local camera_active = false
|
|
local camera_nui_focused = true
|
|
local device_payload = nil
|
|
local sim_picker_open = false
|
|
local sim_picker_payload = nil
|
|
local active_call_payload = nil
|
|
local call_channel = 0
|
|
local nui_generation = 0
|
|
local activity_suspended = false
|
|
local phone_block_game = false
|
|
local phone_block_look = false
|
|
local phone_game_input = false
|
|
local phone_cursor_disabled = false
|
|
|
|
Bridge.Debug("debug", "[sky_phone] Client script initialized.", { always = true })
|
|
|
|
local server_callbacks = {
|
|
"security:unlock",
|
|
"security:set-passcode",
|
|
"security:change-passcode",
|
|
"security:disable-passcode",
|
|
"device:save",
|
|
"device:notification-open",
|
|
"notifications:save",
|
|
"custom-app:storage:get",
|
|
"custom-app:storage:set",
|
|
"device:factory-reset",
|
|
"account:login",
|
|
"account:register",
|
|
"account:logout",
|
|
"account:devices",
|
|
"account:remove-device",
|
|
"notes:list",
|
|
"notes:create",
|
|
"notes:update",
|
|
"notes:delete",
|
|
"mail:register",
|
|
"mail:login",
|
|
"mail:logout",
|
|
"mail:counts",
|
|
"mail:mailboxes",
|
|
"mail:create-mailbox",
|
|
"mail:delete-mailbox",
|
|
"mail:list",
|
|
"mail:get",
|
|
"mail:get-draft",
|
|
"mail:save-draft",
|
|
"mail:delete-draft",
|
|
"mail:delete-many",
|
|
"mail:send",
|
|
"mail:set-read",
|
|
"mail:move",
|
|
"mail:trash",
|
|
"mail:restore",
|
|
"mail:delete-forever",
|
|
"mail:empty-trash",
|
|
"marketplace:list",
|
|
"marketplace:profile",
|
|
"marketplace:profile-save",
|
|
"marketplace:get",
|
|
"marketplace:list-own",
|
|
"marketplace:create",
|
|
"marketplace:update",
|
|
"marketplace:set-status",
|
|
"marketplace:favorite",
|
|
"marketplace:counts",
|
|
"marketplace:list-inquiries",
|
|
"marketplace:get-inquiry",
|
|
"marketplace:send-message",
|
|
"marketplace:make-offer",
|
|
"marketplace:respond-offer",
|
|
"marketplace:report",
|
|
"marketplace:block",
|
|
"pages:list",
|
|
"pages:get",
|
|
"pages:profile",
|
|
"pages:profile-save",
|
|
"pages:list-own",
|
|
"pages:create",
|
|
"pages:share-citymarkt",
|
|
"pages:react",
|
|
"pages:delete",
|
|
"weazel-news:context",
|
|
"weazel-news:list",
|
|
"weazel-news:get",
|
|
"weazel-news:manage-list",
|
|
"weazel-news:create",
|
|
"weazel-news:update",
|
|
"weazel-news:delete",
|
|
"fliptok:register",
|
|
"fliptok:login",
|
|
"fliptok:logout",
|
|
"fliptok:bootstrap",
|
|
"fliptok:feed",
|
|
"fliptok:video",
|
|
"fliptok:discover",
|
|
"fliptok:music-metadata",
|
|
"fliptok:publish",
|
|
"fliptok:react",
|
|
"fliptok:follow",
|
|
"fliptok:comments",
|
|
"fliptok:comment",
|
|
"fliptok:comment-react",
|
|
"fliptok:view",
|
|
"fliptok:share",
|
|
"fliptok:profile",
|
|
"fliptok:connections",
|
|
"fliptok:update-profile",
|
|
"fliptok:activities",
|
|
"fliptok:mark-activities",
|
|
"fliptok:report",
|
|
"fliptok:admin-reports",
|
|
"fliptok:admin-resolve-report",
|
|
"fliptok:block",
|
|
"fliptok:delete",
|
|
"picstagram:register",
|
|
"picstagram:login",
|
|
"picstagram:logout",
|
|
"picstagram:bootstrap",
|
|
"picstagram:feed",
|
|
"picstagram:post",
|
|
"picstagram:explore",
|
|
"picstagram:search",
|
|
"picstagram:saved",
|
|
"picstagram:profile",
|
|
"picstagram:connections",
|
|
"picstagram:update-profile",
|
|
"picstagram:publish-post",
|
|
"picstagram:update-post",
|
|
"picstagram:set-post-status",
|
|
"picstagram:react",
|
|
"picstagram:follow",
|
|
"picstagram:respond-follow",
|
|
"picstagram:comments",
|
|
"picstagram:comment",
|
|
"picstagram:comment-react",
|
|
"picstagram:remove-comment",
|
|
"picstagram:publish-story",
|
|
"picstagram:stories",
|
|
"picstagram:view-story",
|
|
"picstagram:story-viewers",
|
|
"picstagram:remove-story",
|
|
"picstagram:activities",
|
|
"picstagram:mark-activities",
|
|
"picstagram:block",
|
|
"picstagram:report",
|
|
"picstagram:admin-reports",
|
|
"picstagram:admin-resolve-report",
|
|
"feather:bootstrap",
|
|
"feather:create-profile",
|
|
"feather:update-profile",
|
|
"feather:feed",
|
|
"feather:explore",
|
|
"feather:network",
|
|
"feather:create-post",
|
|
"feather:react",
|
|
"feather:follow",
|
|
"feather:connections",
|
|
"feather:remove-connection",
|
|
"feather:profile",
|
|
"feather:bookmarks",
|
|
"feather:thread",
|
|
"feather:activities",
|
|
"feather:mark-activities",
|
|
"feather:delete",
|
|
"feather:block",
|
|
"feather:report",
|
|
"calendar:list",
|
|
"calendar:create",
|
|
"calendar:update",
|
|
"calendar:delete",
|
|
"music:bootstrap",
|
|
"music:add-youtube",
|
|
"music:remove-youtube",
|
|
"music:create-playlist",
|
|
"music:rename-playlist",
|
|
"music:delete-playlist",
|
|
"music:add-to-playlist",
|
|
"music:remove-from-playlist",
|
|
"map:markers",
|
|
"map:create-marker",
|
|
"map:delete-marker",
|
|
"crewlink:bootstrap",
|
|
"crewlink:login",
|
|
"crewlink:register",
|
|
"crewlink:logout",
|
|
"crewlink:update-profile",
|
|
"crewlink:create-group",
|
|
"crewlink:update-group",
|
|
"crewlink:delete-group",
|
|
"crewlink:set-active",
|
|
"crewlink:join-code",
|
|
"crewlink:rotate-code",
|
|
"crewlink:nearby",
|
|
"crewlink:invite-nearby",
|
|
"crewlink:respond-invite",
|
|
"crewlink:update-member",
|
|
"crewlink:transfer-owner",
|
|
"crewlink:remove-member",
|
|
"crewlink:leave",
|
|
"crewlink:create-ping",
|
|
"crewlink:remove-ping",
|
|
"companies:list",
|
|
"companies:get",
|
|
"companies:my-requests",
|
|
"companies:get-request",
|
|
"companies:work-context",
|
|
"companies:work-queue",
|
|
"companies:list-members",
|
|
"companies:create-request",
|
|
"companies:cancel-request",
|
|
"companies:send-message",
|
|
"companies:claim-request",
|
|
"companies:assign-request",
|
|
"companies:update-request-status",
|
|
"companies:update-availability",
|
|
"companies:update-profile",
|
|
"companies:update-hours",
|
|
"companies:update-services",
|
|
"companies:publish-announcement",
|
|
"companies:set-call-availability",
|
|
"companies:call-customer",
|
|
"companies:dial-service-line",
|
|
"sim:insert",
|
|
"sim:eject",
|
|
"contacts:list",
|
|
"contacts:save",
|
|
"contacts:delete",
|
|
"contacts:favorite",
|
|
"calls:recents",
|
|
"calls:dial",
|
|
"calls:answer",
|
|
"calls:set-speaker",
|
|
"calls:decline",
|
|
"calls:hangup",
|
|
"calls:block",
|
|
"banking:overview",
|
|
"banking:transfer",
|
|
"billing:overview",
|
|
"billing:list",
|
|
"billing:detail",
|
|
"billing:markRead",
|
|
"billing:pay",
|
|
"billing:dispute",
|
|
"messages:conversations",
|
|
"messages:thread",
|
|
"messages:send",
|
|
"messages:media",
|
|
"messages:delete",
|
|
"messages:gifs",
|
|
"memos:list",
|
|
"memos:update",
|
|
"memos:delete",
|
|
"easyshare:bootstrap",
|
|
"easyshare:own-contact",
|
|
"easyshare:set-visibility",
|
|
"easyshare:request",
|
|
"easyshare:respond",
|
|
"easyshare:cancel",
|
|
"darkchat:bootstrap",
|
|
"darkchat:create-profile",
|
|
"darkchat:delete-profile",
|
|
"darkchat:update-profile",
|
|
"darkchat:start",
|
|
"darkchat:thread",
|
|
"darkchat:send",
|
|
"darkchat:media",
|
|
"darkchat:react",
|
|
"darkchat:message-action",
|
|
"darkchat:update-conversation",
|
|
"darkchat:add-contact",
|
|
"darkchat:remove-contact",
|
|
"darkchat:block",
|
|
"darkchat:report",
|
|
"darkchat:clear",
|
|
"flare:bootstrap",
|
|
"flare:delete-profile",
|
|
"flare:save-profile",
|
|
"flare:set-discovery",
|
|
"flare:swipe",
|
|
"flare:rewind",
|
|
"flare:unmatch",
|
|
"flare:thread",
|
|
"flare:send",
|
|
"gallery:list",
|
|
"gallery:counts",
|
|
"gallery:favorite",
|
|
"media:config",
|
|
"media:import:sources",
|
|
"media:import:list",
|
|
"media:import:commit",
|
|
"media:import:url",
|
|
}
|
|
|
|
local locale, locale_name = SkyPhoneLocales.Resolve(Config.Bridge.Locale)
|
|
|
|
local function update_nui_focus()
|
|
local focus = SkyPhoneFocus.Resolve({
|
|
activity_suspended = activity_suspended,
|
|
allow_movement = Config.Phone.AllowMovement,
|
|
call_focus = call_focus,
|
|
camera_active = camera_active,
|
|
camera_nui_focused = camera_nui_focused,
|
|
cursor_disabled = phone_cursor_disabled,
|
|
is_open = is_open,
|
|
notification_focus = notification_focus,
|
|
payphone_focus = payphone_focus,
|
|
sim_picker_open = sim_picker_open,
|
|
})
|
|
SetNuiFocus(focus.focused, focus.cursor)
|
|
SetNuiFocusKeepInput(focus.keep_input)
|
|
phone_block_game = focus.block_game == true
|
|
phone_block_look = focus.block_look == true
|
|
phone_game_input = focus.game_input
|
|
if not phone_game_input and not phone_block_game then
|
|
phone_cursor_disabled = false
|
|
end
|
|
TriggerEvent("sky_phone:client:cameraFocusApplied", {
|
|
active = camera_active,
|
|
cursor = focus.cursor,
|
|
focused = focus.focused,
|
|
gameInput = focus.keep_input,
|
|
})
|
|
end
|
|
|
|
CreateThread(function()
|
|
while true do
|
|
if phone_game_input or phone_block_game then
|
|
if phone_block_game then
|
|
SkyPhoneFocus.ApplyFocusedControls()
|
|
else
|
|
SkyPhoneFocus.ApplyGameInputControls(phone_block_look)
|
|
end
|
|
if IsDisabledControlJustPressed(0, 19) then
|
|
phone_cursor_disabled = not phone_cursor_disabled
|
|
update_nui_focus()
|
|
end
|
|
Wait(0)
|
|
else
|
|
Wait(250)
|
|
end
|
|
end
|
|
end)
|
|
|
|
AddEventHandler("sky_phone:client:setSuspended", function(suspended)
|
|
activity_suspended = suspended == true
|
|
update_nui_focus()
|
|
end)
|
|
|
|
AddEventHandler("sky_phone:client:setPayphoneFocus", function(focused)
|
|
payphone_focus = focused == true
|
|
update_nui_focus()
|
|
end)
|
|
|
|
AddEventHandler("sky_phone:client:setCameraFocus", function(data)
|
|
if type(data) ~= "table" or type(data.active) ~= "boolean" or type(data.nuiFocused) ~= "boolean" then
|
|
Bridge.Debug("error", "[sky_phone] Rejected invalid camera focus claim.")
|
|
return
|
|
end
|
|
camera_active = data.active
|
|
camera_nui_focused = data.nuiFocused
|
|
update_nui_focus()
|
|
end)
|
|
|
|
local function send_open_message()
|
|
if not device_payload then
|
|
return
|
|
end
|
|
|
|
local payload = device_payload
|
|
payload.lang = locale_name
|
|
payload.locales = locale.Nui
|
|
payload.fallbackLocales = Locales.en.Nui
|
|
SkyPhoneApps.SendCatalog()
|
|
SendNUIMessage({
|
|
type = "app:open",
|
|
data = payload,
|
|
})
|
|
end
|
|
|
|
local function open_phone()
|
|
if is_open or not device_payload then
|
|
return
|
|
end
|
|
|
|
send_open_message()
|
|
end
|
|
|
|
local function close_phone(close_device_session)
|
|
local was_requested = open_requested
|
|
local was_open = is_open
|
|
open_requested = false
|
|
call_focus = false
|
|
activity_suspended = false
|
|
TriggerEvent("sky_phone:animation:phone", false)
|
|
is_open = false
|
|
if was_open then
|
|
SkyPhoneApps.SetPhoneOpen(false)
|
|
TriggerEvent("sky_phone:nuiClosed")
|
|
end
|
|
update_nui_focus()
|
|
if was_requested or was_open then
|
|
SendNUIMessage({ type = "app:close" })
|
|
if close_device_session ~= false then
|
|
Bridge.Callbacks.Trigger("sky_phone:device:close", {})
|
|
end
|
|
end
|
|
end
|
|
|
|
AddEventHandler("sky_phone:client:forceClose", function()
|
|
close_phone()
|
|
end)
|
|
|
|
local function leave_call_voice()
|
|
if call_channel == 0 then
|
|
return
|
|
end
|
|
Bridge.Calls.Leave()
|
|
call_channel = 0
|
|
end
|
|
|
|
local function join_call_voice(channel)
|
|
local next_channel = tonumber(channel) or 0
|
|
if not Bridge.Calls.Join(next_channel) then
|
|
return false
|
|
end
|
|
call_channel = next_channel
|
|
return true
|
|
end
|
|
|
|
if Config.Phone.DevelopmentCommand then
|
|
RegisterCommand(Config.Command, function()
|
|
if is_open then
|
|
close_phone()
|
|
return
|
|
end
|
|
Bridge.Callbacks.Trigger("sky_phone:device:development-open", {})
|
|
end, false)
|
|
end
|
|
|
|
RegisterNetEvent("sky_phone:testdata:feedback", function(success, detail)
|
|
local test_data_locale = locale.TestData
|
|
local message = success and test_data_locale.Success or test_data_locale.Failed
|
|
if success and type(detail) == "string" and detail ~= "" then
|
|
message = message:gsub("{email}", detail)
|
|
end
|
|
Bridge.Framework.Notify("iFruit", message, success and "success" or "error", 7000)
|
|
end)
|
|
|
|
RegisterNUICallback("ui:ready", function(data, cb)
|
|
if type(data) ~= "table" or data.protocolVersion ~= 1 then
|
|
cb({ success = false, error = "unsupported_protocol" })
|
|
return
|
|
|
|
end
|
|
nui_generation = nui_generation + 1
|
|
-- Browser state is recreated on a CEF reload. A notification focus claim
|
|
-- cannot survive unless its notification is replayed as part of this handshake.
|
|
notification_focus = false
|
|
Bridge.Debug("debug", "[sky_phone] NUI reported ready.", { always = true })
|
|
SkyPhoneApps.SendCatalog()
|
|
if open_requested and device_payload then
|
|
send_open_message()
|
|
end
|
|
if active_call_payload then
|
|
SendNUIMessage({ type = "call:state", data = active_call_payload })
|
|
end
|
|
if sim_picker_open and sim_picker_payload then
|
|
SendNUIMessage({ type = "sim:picker", data = sim_picker_payload })
|
|
else
|
|
SendNUIMessage({ type = "sim:picker-close" })
|
|
end
|
|
|
|
update_nui_focus()
|
|
TriggerEvent("sky_phone:client:nuiReady", {
|
|
generation = nui_generation,
|
|
protocolVersion = 1,
|
|
})
|
|
|
|
cb({
|
|
success = true,
|
|
data = {
|
|
generation = nui_generation,
|
|
protocolVersion = 1,
|
|
},
|
|
})
|
|
end)
|
|
|
|
RegisterNUICallback("ui:opened", function(data, cb)
|
|
if type(data) ~= "table" then
|
|
cb({ success = false, error = "invalid_request" })
|
|
return
|
|
end
|
|
if not open_requested or not device_payload then
|
|
Bridge.Debug(
|
|
"warn",
|
|
"[sky_phone] Ignored a NUI open confirmation without a pending device open.",
|
|
{ always = true }
|
|
)
|
|
SendNUIMessage({ type = "app:close" })
|
|
update_nui_focus()
|
|
cb({ success = false, error = "open_not_requested" })
|
|
return
|
|
end
|
|
|
|
is_open = true
|
|
SkyPhoneApps.SetPhoneOpen(true)
|
|
notification_focus = false
|
|
call_focus = false
|
|
update_nui_focus()
|
|
TriggerEvent("sky_phone:animation:phone", true)
|
|
cb({ success = true })
|
|
end)
|
|
|
|
RegisterNUICallback("close", function(data, cb)
|
|
if type(data) ~= "table" then
|
|
cb({ success = false, error = "invalid_request" })
|
|
return
|
|
end
|
|
close_phone()
|
|
cb({ success = true })
|
|
end)
|
|
|
|
RegisterNUICallback("notification:focus", function(data, cb)
|
|
if type(data) ~= "table" or type(data.active) ~= "boolean" then
|
|
cb({ success = false, error = "invalid_request" })
|
|
return
|
|
end
|
|
notification_focus = data.active == true and not is_open
|
|
update_nui_focus()
|
|
cb({ success = true })
|
|
end)
|
|
|
|
RegisterNUICallback("sim:picker-close", function(data, cb)
|
|
if type(data) ~= "table" then
|
|
cb({ success = false, error = "invalid_request" })
|
|
return
|
|
end
|
|
sim_picker_open = false
|
|
sim_picker_payload = nil
|
|
update_nui_focus()
|
|
SendNUIMessage({ type = "sim:picker-close" })
|
|
local result = Bridge.Callbacks.Trigger("sky_phone:sim:picker-close", {})
|
|
cb(type(result) == "table" and result or { success = false, error = "request_failed" })
|
|
end)
|
|
|
|
RegisterNUICallback("map:getPlayerCoords", function(data, cb)
|
|
if type(data) ~= "table" then
|
|
cb({ success = false, error = "invalid_request" })
|
|
return
|
|
end
|
|
local coords = GetEntityCoords(PlayerPedId())
|
|
cb({
|
|
success = true,
|
|
data = {
|
|
coords = {
|
|
x = coords.x,
|
|
y = coords.y,
|
|
z = coords.z
|
|
}
|
|
}
|
|
})
|
|
end)
|
|
|
|
RegisterNUICallback("map:setWaypoint", function(data, cb)
|
|
local coords = type(data) == "table" and data.coords or nil
|
|
local x = type(coords) == "table" and tonumber(coords.x) or nil
|
|
local y = type(coords) == "table" and tonumber(coords.y) or nil
|
|
if not x or not y or x ~= x or y ~= y or math.abs(x) > 10000.0 or math.abs(y) > 10000.0 then
|
|
cb({ success = false, error = "invalid_marker" })
|
|
return
|
|
end
|
|
|
|
SetNewWaypoint(x, y)
|
|
cb({ success = true })
|
|
end)
|
|
|
|
local weather_types = {
|
|
[joaat("EXTRASUNNY")] = "sunny",
|
|
[joaat("CLEAR")] = "clear",
|
|
[joaat("CLOUDS")] = "partly_cloudy",
|
|
[joaat("OVERCAST")] = "cloudy",
|
|
[joaat("RAIN")] = "rain",
|
|
[joaat("CLEARING")] = "rain",
|
|
[joaat("THUNDER")] = "thunder",
|
|
[joaat("SMOG")] = "fog",
|
|
[joaat("FOGGY")] = "fog",
|
|
[joaat("NEUTRAL")] = "cloudy",
|
|
[joaat("SNOW")] = "snow",
|
|
[joaat("BLIZZARD")] = "snow",
|
|
[joaat("SNOWLIGHT")] = "snow",
|
|
[joaat("XMAS")] = "snow",
|
|
[joaat("HALLOWEEN")] = "cloudy",
|
|
}
|
|
|
|
local function weather_region(coords)
|
|
if coords.x > 2500.0 and coords.y < -3000.0 then
|
|
return "cayo_perico"
|
|
end
|
|
if coords.y > 900.0 then
|
|
return "blaine_county"
|
|
end
|
|
return "los_santos"
|
|
end
|
|
|
|
RegisterNUICallback("weather:get", function(data, cb)
|
|
if type(data) ~= "table" then
|
|
cb({ success = false, error = "invalid_request" })
|
|
return
|
|
end
|
|
local coords = GetEntityCoords(PlayerPedId())
|
|
local weather_hash = GetPrevWeatherTypeHashName()
|
|
local next_weather_hash = GetNextWeatherTypeHashName()
|
|
cb({
|
|
success = true,
|
|
data = {
|
|
condition = weather_types[weather_hash] or "clear",
|
|
nextCondition = weather_types[next_weather_hash] or weather_types[weather_hash] or "clear",
|
|
region = weather_region(coords),
|
|
clock = {
|
|
year = GetClockYear(),
|
|
month = GetClockMonth() + 1,
|
|
day = GetClockDayOfMonth(),
|
|
hour = GetClockHours(),
|
|
minute = GetClockMinutes(),
|
|
},
|
|
windSpeed = math.max(0.0, GetWindSpeed()),
|
|
rainLevel = math.max(0.0, math.min(1.0, GetRainLevel())),
|
|
},
|
|
})
|
|
end)
|
|
|
|
local function garage_vehicle_kind(model_hash, fallback)
|
|
if IsThisModelABoat(model_hash) then
|
|
return "boat"
|
|
end
|
|
if IsThisModelAPlane(model_hash) then
|
|
return "plane"
|
|
end
|
|
if IsThisModelAHeli(model_hash) then
|
|
return "helicopter"
|
|
end
|
|
if IsThisModelABike(model_hash) or IsThisModelABicycle(model_hash) then
|
|
return "bike"
|
|
end
|
|
return fallback
|
|
end
|
|
|
|
RegisterNUICallback("garage:vehicles", function(data, cb)
|
|
if type(data) ~= "table" then
|
|
cb({ success = false, error = "invalid_request" })
|
|
return
|
|
end
|
|
local result = Bridge.Callbacks.Trigger("sky_phone:garage:vehicles", data)
|
|
if type(result) ~= "table" or not result.success or type(result.data) ~= "table" then
|
|
cb(type(result) == "table" and result or { success = false, error = "request_failed" })
|
|
return
|
|
end
|
|
for _, vehicle in ipairs(result.data.vehicles or {}) do
|
|
local model_hash = tonumber(vehicle.model)
|
|
if not model_hash and type(vehicle.model) == "string" and vehicle.model ~= "" then
|
|
model_hash = joaat(vehicle.model)
|
|
end
|
|
if model_hash then
|
|
local display_name = GetDisplayNameFromVehicleModel(model_hash)
|
|
local label = display_name and GetLabelText(display_name) or nil
|
|
if label and label ~= "NULL" and label ~= "CARNOTFOUND" then
|
|
vehicle.name = label
|
|
elseif type(vehicle.model) == "string" and vehicle.model ~= "" then
|
|
vehicle.name = vehicle.model
|
|
end
|
|
vehicle.kind = garage_vehicle_kind(model_hash, vehicle.kind)
|
|
end
|
|
end
|
|
cb(result)
|
|
end)
|
|
|
|
for _, callback_name in ipairs(server_callbacks) do
|
|
RegisterNUICallback(callback_name, function(data, cb)
|
|
if type(data) ~= "table" then
|
|
cb({ success = false, error = "invalid_request" })
|
|
return
|
|
end
|
|
local result = Bridge.Callbacks.Trigger("sky_phone:" .. callback_name, data)
|
|
if callback_name:match("^media:import:") and (not result or not result.success) then
|
|
Bridge.Debug(
|
|
"warn",
|
|
"[sky_phone] NUI callback '%s' failed: %s.",
|
|
callback_name,
|
|
tostring(result and result.error or "no server response"),
|
|
{ always = true }
|
|
)
|
|
end
|
|
if type(result) == "table" then
|
|
cb(result)
|
|
return
|
|
end
|
|
|
|
cb({ success = false, error = "request_failed" })
|
|
end)
|
|
end
|
|
|
|
RegisterNetEvent("sky_phone:device:open", function(data)
|
|
if type(data) ~= "table" or type(data.device) ~= "table" or type(data.device.imei) ~= "string" then
|
|
Bridge.Debug("error", "[sky_phone] Rejected invalid device open data.")
|
|
return
|
|
end
|
|
Bridge.Debug(
|
|
"debug",
|
|
"[sky_phone] Client received device open for IMEI %s account_linked=%s.",
|
|
tostring(data.device.imei),
|
|
tostring(data.account ~= nil),
|
|
{ always = true }
|
|
)
|
|
device_payload = data
|
|
open_requested = true
|
|
if is_open then
|
|
SkyPhoneApps.SendCatalog()
|
|
SendNUIMessage({ type = "device:updated", data = data })
|
|
return
|
|
end
|
|
open_phone()
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:device:updated", function(data)
|
|
if type(data) ~= "table" or type(data.device) ~= "table" or type(data.device.imei) ~= "string" then
|
|
Bridge.Debug("error", "[sky_phone] Rejected invalid device update data.")
|
|
return
|
|
end
|
|
device_payload = data
|
|
SendNUIMessage({ type = "device:updated", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:device:invalidated", function()
|
|
TriggerEvent("sky_phone:animation:reset")
|
|
close_phone(false)
|
|
device_payload = nil
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:device:error", function(error_code)
|
|
Bridge.Debug(
|
|
"debug",
|
|
"[sky_phone] Client received device error: %s.",
|
|
tostring(error_code),
|
|
{ always = true }
|
|
)
|
|
local message = locale.DeviceErrors[error_code] or locale.DeviceErrors.default
|
|
Bridge.Framework.Notify("iFruit", message, "error", 5000)
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:mail:changed", function(data)
|
|
SendNUIMessage({ type = "mail:changed", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:easyshare:changed", function(data)
|
|
SendNUIMessage({ type = "easyshare:changed", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:gallery:changed", function()
|
|
SendNUIMessage({ type = "gallery:changed" })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:mail:new", function(data)
|
|
local mail_locale = locale.Nui.Apps.mail
|
|
data.title = mail_locale.name
|
|
data.text = mail_locale.newMessage:gsub("{sender}", tostring(data.sender))
|
|
SendNUIMessage({ type = "mail:new", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:marketplace:changed", function(data)
|
|
SendNUIMessage({ type = "marketplace:changed", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:companies:changed", function(data)
|
|
SendNUIMessage({ type = "companies:changed", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:companies:notification", function(data)
|
|
local companies_locale = locale.Nui.Apps.companies
|
|
data.title = companies_locale.name
|
|
data.text = companies_locale.notifications[data.kind]
|
|
or companies_locale.notifications.requestUpdated
|
|
SendNUIMessage({ type = "companies:notification", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:fliptok:command-feedback", function(data)
|
|
Bridge.Framework.Notify("FlipTok", data.message, data.notificationType, 5000)
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:fliptok:verification-changed", function(data)
|
|
SendNUIMessage({ type = "fliptok:verification-changed", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:fliptok:new", function(data)
|
|
local fliptok_locale = locale.Nui.Apps.fliptok
|
|
local notification_text = fliptok_locale.notifications[data.kind] or fliptok_locale.notifications.default
|
|
data.title = fliptok_locale.name
|
|
data.text = notification_text:gsub("{actor}", tostring(data.actor or ""))
|
|
SendNUIMessage({ type = "fliptok:new", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:picstagram:command-feedback", function(data)
|
|
Bridge.Framework.Notify("Picstagram", data.message, data.notificationType, 5000)
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:picstagram:verification-changed", function(data)
|
|
SendNUIMessage({ type = "picstagram:verification-changed", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:picstagram:new", function(data)
|
|
local picstagram_locale = locale.Nui.Apps.picstagram
|
|
local notification_text = picstagram_locale.notifications[data.kind] or picstagram_locale.notifications.default
|
|
data.title = picstagram_locale.name
|
|
data.text = notification_text:gsub("{actor}", tostring(data.actor or ""))
|
|
SendNUIMessage({ type = "picstagram:new", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:feather:new", function(data)
|
|
local feather_locale = locale.Nui.Apps.feather
|
|
local notification_text = feather_locale.notifications[data.kind] or feather_locale.notifications.default
|
|
data.title = feather_locale.name
|
|
data.text = notification_text:gsub("{actor}", tostring(data.actor or ""))
|
|
SendNUIMessage({ type = "feather:new", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:marketplace:new-message", function(data)
|
|
local marketplace_locale = locale.Nui.Apps.citymarkt
|
|
data.title = marketplace_locale.name
|
|
if data.kind == "offer" then
|
|
data.text = marketplace_locale.newOffer
|
|
:gsub("{sender}", tostring(data.sender))
|
|
:gsub("{price}", tostring(data.amount))
|
|
elseif data.kind == "offer-response" and data.action == "accepted" then
|
|
data.text = marketplace_locale.offerAcceptedNotification
|
|
:gsub("{sender}", tostring(data.sender))
|
|
:gsub("{price}", tostring(data.amount))
|
|
elseif data.kind == "offer-response" and data.action == "rejected" then
|
|
data.text = marketplace_locale.offerRejectedNotification
|
|
:gsub("{sender}", tostring(data.sender))
|
|
:gsub("{price}", tostring(data.amount))
|
|
else
|
|
data.text = marketplace_locale.newMessage:gsub("{sender}", tostring(data.sender))
|
|
end
|
|
SendNUIMessage({ type = "marketplace:new-message", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:calendar:reminder", function(data)
|
|
local calendar_locale = locale.Nui.Apps.calendar
|
|
data.title = calendar_locale.name
|
|
data.text = calendar_locale.reminder:gsub("{title}", tostring(data.eventTitle))
|
|
SendNUIMessage({ type = "calendar:reminder", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:flare:match", function(data)
|
|
local flare_locale = locale.Nui.Apps.flare
|
|
data.title = flare_locale.name
|
|
data.text = flare_locale.newMatchNotification:gsub("{sender}", tostring(data.sender))
|
|
SendNUIMessage({ type = "flare:new-match", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:flare:message", function(data)
|
|
local flare_locale = locale.Nui.Apps.flare
|
|
data.title = flare_locale.name
|
|
data.text = flare_locale.newMessageNotification:gsub("{sender}", tostring(data.sender))
|
|
SendNUIMessage({ type = "flare:new-message", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:sim:picker", function(data)
|
|
if type(data) ~= "table" or type(data.number) ~= "string" or type(data.choices) ~= "table" then
|
|
Bridge.Debug("error", "[sky_phone] Rejected invalid SIM picker data.")
|
|
return
|
|
end
|
|
sim_picker_open = true
|
|
sim_picker_payload = data
|
|
update_nui_focus()
|
|
SendNUIMessage({ type = "sim:picker", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:sim:picker-close", function()
|
|
sim_picker_open = false
|
|
sim_picker_payload = nil
|
|
update_nui_focus()
|
|
SendNUIMessage({ type = "sim:picker-close" })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:contacts:changed", function()
|
|
SendNUIMessage({ type = "contacts:changed" })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:calls:changed", function()
|
|
SendNUIMessage({ type = "calls:changed" })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:banking:changed", function(data)
|
|
SendNUIMessage({ type = "banking:changed", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:billing:changed", function()
|
|
SendNUIMessage({ type = "billing:changed" })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:billing:new", function(data)
|
|
local billing_locale = locale.Nui.Apps.billing
|
|
data.title = billing_locale.name
|
|
data.text = billing_locale.notifications.newInvoice
|
|
:gsub("{issuer}", tostring(data.issuer))
|
|
:gsub("{amount}", ("%s %s"):format(tostring(data.amount), Config.Billing.Currency))
|
|
SendNUIMessage({ type = "billing:new", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:crewlink:changed", function(data)
|
|
SendNUIMessage({ type = "crewlink:changed", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:crewlink:notification", function(data)
|
|
local crewlink_locale = locale.Nui.Apps.crewlink
|
|
local notification_text = crewlink_locale.notifications[data.kind]
|
|
or crewlink_locale.notifications.default
|
|
data.title = crewlink_locale.name
|
|
data.text = notification_text
|
|
:gsub("{actor}", tostring(data.actor or ""))
|
|
:gsub("{group}", tostring(data.groupName or ""))
|
|
:gsub("{ping}", tostring(data.pingLabel or ""))
|
|
SendNUIMessage({ type = "crewlink:notification", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:messages:changed", function(data)
|
|
SendNUIMessage({ type = "messages:changed", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:messages:new", function(data)
|
|
local messages_locale = locale.Nui.Apps.messages
|
|
data.title = messages_locale.name
|
|
data.text = messages_locale.newMessage:gsub("{sender}", tostring(data.sender))
|
|
SendNUIMessage({ type = "messages:new", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:darkchat:changed", function(data)
|
|
SendNUIMessage({ type = "darkchat:changed", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:darkchat:new", function(data)
|
|
local darkchat_locale = locale.Nui.Apps.darkchat
|
|
data.title = darkchat_locale.name
|
|
if data.notificationMode == "private" then
|
|
data.sender = nil
|
|
data.text = darkchat_locale.privateNotification
|
|
elseif data.notificationMode == "hidden" then
|
|
data.sender = nil
|
|
data.text = ""
|
|
else
|
|
data.text = darkchat_locale.newMessage:gsub("{sender}", tostring(data.sender))
|
|
end
|
|
SendNUIMessage({ type = "darkchat:new", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:call:incoming", function(data)
|
|
if type(data) ~= "table" or type(data.id) ~= "string" or data.state ~= "ringing" then
|
|
Bridge.Debug("error", "[sky_phone] Rejected invalid incoming call data.")
|
|
return
|
|
end
|
|
active_call_payload = data
|
|
call_focus = true
|
|
update_nui_focus()
|
|
TriggerEvent("sky_phone:animation:call", data)
|
|
SendNUIMessage({ type = "call:incoming", data = data })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:call:state", function(data)
|
|
if type(data) ~= "table" or type(data.id) ~= "string" or type(data.state) ~= "string" then
|
|
Bridge.Debug("error", "[sky_phone] Rejected invalid call state data.")
|
|
return
|
|
end
|
|
if data.state == "ringing" or data.state == "connected" then
|
|
active_call_payload = data
|
|
end
|
|
if data.state ~= "ringing" then
|
|
local focus_changed = call_focus
|
|
call_focus = false
|
|
if focus_changed then
|
|
update_nui_focus()
|
|
end
|
|
end
|
|
if data.state ~= "ringing" and data.state ~= "connected" then
|
|
active_call_payload = nil
|
|
end
|
|
if data.state == "connected" and data.channel then
|
|
if not join_call_voice(data.channel) then
|
|
TriggerEvent("sky_phone:device:error", "voice_unavailable")
|
|
end
|
|
elseif data.state ~= "ringing" then
|
|
leave_call_voice()
|
|
end
|
|
TriggerEvent("sky_phone:animation:call", data)
|
|
SendNUIMessage({ type = "call:state", data = data })
|
|
end)
|
|
|
|
CreateThread(function()
|
|
if Config.Phone.DevelopmentCommand then
|
|
TriggerEvent("chat:addSuggestion", "/" .. Config.Command, locale.CommandDescription)
|
|
end
|
|
if Config.TestData.Enabled then
|
|
TriggerEvent("chat:addSuggestion", "/" .. Config.TestData.Command, locale.TestData.CommandDescription)
|
|
end
|
|
end)
|
|
|
|
AddEventHandler("onResourceStop", function(resource_name)
|
|
if resource_name ~= GetCurrentResourceName() then
|
|
return
|
|
end
|
|
|
|
is_open = false
|
|
open_requested = false
|
|
notification_focus = false
|
|
call_focus = false
|
|
payphone_focus = false
|
|
camera_active = false
|
|
camera_nui_focused = true
|
|
sim_picker_open = false
|
|
sim_picker_payload = nil
|
|
active_call_payload = nil
|
|
activity_suspended = false
|
|
phone_game_input = false
|
|
phone_cursor_disabled = false
|
|
SetNuiFocusKeepInput(false)
|
|
SetNuiFocus(false, false)
|
|
|
|
TriggerEvent("sky_phone:animation:reset")
|
|
leave_call_voice()
|
|
|
|
if Config.Phone.DevelopmentCommand then
|
|
TriggerEvent("chat:removeSuggestion", "/" .. Config.Command)
|
|
end
|
|
if Config.TestData.Enabled then
|
|
TriggerEvent("chat:removeSuggestion", "/" .. Config.TestData.Command)
|
|
end
|
|
end)
|