mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:01:31 +00:00
587 lines
17 KiB
Lua
587 lines
17 KiB
Lua
SkyPhoneClient = {}
|
|
|
|
local is_open = false
|
|
local open_requested = false
|
|
local open_without_focus = false
|
|
local device_payload = nil
|
|
local equipped_phone_number = nil
|
|
local nui_generation = 0
|
|
local live_activity_active = false
|
|
local open_home_requested = false
|
|
local admin_panel_open = false
|
|
local suggested_admin_command = nil
|
|
local suggested_test_data_command = nil
|
|
local active_development_command = nil
|
|
local registered_development_commands = {}
|
|
local active_key_mapping_command = nil
|
|
local active_key_mapping_key = nil
|
|
local key_mapping_revision = 0
|
|
local refresh_development_command
|
|
local refresh_phone_key_mapping
|
|
local refresh_test_data_command_suggestion
|
|
|
|
local function get_equipped_phone_number()
|
|
if not device_payload or not device_payload.device.sim then
|
|
return nil
|
|
end
|
|
|
|
return device_payload.device.sim.number
|
|
end
|
|
|
|
local function set_equipped_phone_number(next_number)
|
|
if next_number ~= nil and type(next_number) ~= "string" then
|
|
next_number = nil
|
|
end
|
|
if next_number == equipped_phone_number then
|
|
return
|
|
end
|
|
|
|
equipped_phone_number = next_number
|
|
TriggerEvent("sky_phone:client:phoneNumberChanged", next_number)
|
|
end
|
|
|
|
local function update_equipped_phone_number(data)
|
|
set_equipped_phone_number(type(data) == "table"
|
|
and type(data.device) == "table"
|
|
and type(data.device.sim) == "table"
|
|
and data.device.sim.number
|
|
or nil)
|
|
end
|
|
|
|
local function get_authoritative_phone_number()
|
|
local result = Bridge.Callbacks.Trigger("sky_phone:device:equipped-number", {})
|
|
if type(result) ~= "table" or not result.success or type(result.data) ~= "table" then
|
|
return nil
|
|
end
|
|
|
|
local phone_number = result.data.phoneNumber
|
|
set_equipped_phone_number(phone_number)
|
|
return phone_number
|
|
end
|
|
|
|
local function get_phone_state()
|
|
return {
|
|
inCall = SkyPhoneCalls.IsActive(),
|
|
onScreen = is_open,
|
|
open = is_open,
|
|
phoneNumber = get_equipped_phone_number(),
|
|
}
|
|
end
|
|
|
|
SkyPhoneClient.GetState = get_phone_state
|
|
SkyPhoneClient.GetEquippedPhoneNumber = get_authoritative_phone_number
|
|
|
|
Bridge.Debug("debug", "[sky_phone] Client script initialized.", { always = true })
|
|
|
|
local locale, locale_name = SkyPhoneLocales.Resolve(Config.Bridge.Locale)
|
|
|
|
local function apply_disabled_apps(payload)
|
|
if type(payload) ~= "table" then
|
|
return
|
|
end
|
|
|
|
local disabled = {}
|
|
for app_id, enabled in pairs(Config.Apps or {}) do
|
|
if type(app_id) == "string" and enabled == false then
|
|
disabled[#disabled + 1] = app_id
|
|
end
|
|
end
|
|
table.sort(disabled)
|
|
payload.disabledApps = disabled
|
|
end
|
|
|
|
local function send_admin_panel_open()
|
|
SendNUIMessage({
|
|
type = "admin:open",
|
|
data = {
|
|
lang = locale_name,
|
|
locales = locale.Nui,
|
|
fallbackLocales = Locales.en.Nui,
|
|
},
|
|
})
|
|
end
|
|
|
|
local function close_admin_panel()
|
|
if not admin_panel_open then
|
|
return
|
|
end
|
|
admin_panel_open = false
|
|
SkyPhoneFocus.SetAdminPanel(false)
|
|
SendNUIMessage({ type = "admin:close" })
|
|
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,
|
|
openHome = open_home_requested,
|
|
})
|
|
open_home_requested = false
|
|
end
|
|
|
|
local function refresh_admin_command_suggestion()
|
|
if suggested_admin_command then
|
|
TriggerEvent("chat:removeSuggestion", "/" .. suggested_admin_command)
|
|
suggested_admin_command = nil
|
|
end
|
|
if Config.AdminPanel.Enabled then
|
|
suggested_admin_command = Config.AdminPanel.Command
|
|
TriggerEvent(
|
|
"chat:addSuggestion",
|
|
"/" .. suggested_admin_command,
|
|
locale.AdminCommand.CommandDescription
|
|
)
|
|
end
|
|
end
|
|
|
|
AddEventHandler("sky_phone:configurator:updated", function()
|
|
locale, locale_name = SkyPhoneLocales.Resolve(Config.Bridge.Locale)
|
|
refresh_development_command()
|
|
refresh_phone_key_mapping()
|
|
refresh_test_data_command_suggestion()
|
|
refresh_admin_command_suggestion()
|
|
SkyPhoneApps.SendCatalog()
|
|
if is_open and device_payload then
|
|
apply_disabled_apps(device_payload)
|
|
device_payload.lang = locale_name
|
|
device_payload.locales = locale.Nui
|
|
device_payload.fallbackLocales = Locales.en.Nui
|
|
SendNUIMessage({ type = "device:updated", data = device_payload })
|
|
end
|
|
if admin_panel_open then
|
|
send_admin_panel_open()
|
|
end
|
|
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
|
|
open_without_focus = false
|
|
TriggerEvent("sky_phone:animation:phone", false)
|
|
is_open = false
|
|
if was_open then
|
|
SkyPhoneApps.SetPhoneOpen(false)
|
|
TriggerEvent("sky_phone:nuiClosed")
|
|
TriggerEvent("sky_phone:client:phoneToggled", false)
|
|
end
|
|
SkyPhoneFocus.SetPhone(false)
|
|
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
|
|
|
|
local function request_phone_open(callback_name)
|
|
if is_open or open_requested then
|
|
return true
|
|
end
|
|
|
|
open_requested = true
|
|
local result = Bridge.Callbacks.Trigger(callback_name, {})
|
|
if type(result) == "table" and result.success == true then
|
|
return true
|
|
end
|
|
|
|
open_requested = false
|
|
open_without_focus = false
|
|
return false
|
|
end
|
|
|
|
local function toggle_phone(open, no_focus)
|
|
if open ~= nil and type(open) ~= "boolean" then
|
|
Bridge.Debug("error", "[sky_phone] Rejected invalid phone toggle state.")
|
|
return false
|
|
end
|
|
if no_focus ~= nil and type(no_focus) ~= "boolean" then
|
|
Bridge.Debug("error", "[sky_phone] Rejected invalid phone toggle focus state.")
|
|
return false
|
|
end
|
|
|
|
local should_open = open
|
|
if should_open == nil then
|
|
should_open = not (is_open or open_requested)
|
|
end
|
|
if not should_open then
|
|
if open == nil and open_requested and not is_open then
|
|
return true
|
|
end
|
|
close_phone()
|
|
return true
|
|
end
|
|
|
|
open_without_focus = no_focus == true
|
|
if is_open or open_requested then
|
|
if is_open then
|
|
SkyPhoneFocus.SetPhone(true, open_without_focus)
|
|
end
|
|
return true
|
|
end
|
|
|
|
return request_phone_open("sky_phone:device:open-request")
|
|
end
|
|
|
|
SkyPhoneClient.Toggle = toggle_phone
|
|
|
|
AddEventHandler("sky_phone:client:forceClose", function()
|
|
close_phone()
|
|
end)
|
|
|
|
local function run_development_command()
|
|
if is_open then
|
|
close_phone()
|
|
return
|
|
end
|
|
if open_requested then
|
|
return
|
|
end
|
|
|
|
open_without_focus = false
|
|
request_phone_open("sky_phone:device:development-open")
|
|
end
|
|
|
|
refresh_development_command = function()
|
|
local command_name = Config.Phone.DevelopmentCommand and Config.Command or nil
|
|
if command_name ~= nil and (type(command_name) ~= "string" or command_name == "") then
|
|
error("[sky_phone] Config.Command must be a non-empty command name.")
|
|
end
|
|
|
|
if active_development_command then
|
|
TriggerEvent("chat:removeSuggestion", "/" .. active_development_command)
|
|
end
|
|
|
|
active_development_command = command_name
|
|
if not command_name then
|
|
return
|
|
end
|
|
|
|
if not registered_development_commands[command_name] then
|
|
registered_development_commands[command_name] = true
|
|
RegisterCommand(command_name, function()
|
|
if active_development_command == command_name and Config.Phone.DevelopmentCommand then
|
|
run_development_command()
|
|
end
|
|
end, false)
|
|
end
|
|
|
|
TriggerEvent("chat:addSuggestion", "/" .. command_name, locale.CommandDescription)
|
|
end
|
|
|
|
refresh_development_command()
|
|
|
|
local function run_phone_toggle()
|
|
if is_open then
|
|
close_phone()
|
|
return
|
|
end
|
|
if open_requested then
|
|
return
|
|
end
|
|
|
|
open_without_focus = false
|
|
request_phone_open("sky_phone:device:open-request")
|
|
end
|
|
|
|
RegisterCommand("sky_phone_live_activity_open", function()
|
|
if not live_activity_active or is_open or open_requested then
|
|
return
|
|
end
|
|
|
|
open_home_requested = true
|
|
if not request_phone_open("sky_phone:device:open-request") then
|
|
open_home_requested = false
|
|
end
|
|
end, false)
|
|
|
|
RegisterCommand("sky_phone_toggle", run_phone_toggle, false)
|
|
|
|
refresh_phone_key_mapping = function()
|
|
local key_name = Config.Phone.Keybind
|
|
if key_name ~= false and key_name ~= nil and (type(key_name) ~= "string" or key_name == "") then
|
|
error("[sky_phone] Config.Phone.Keybind must be a non-empty keyboard key name or false.")
|
|
end
|
|
if key_name == active_key_mapping_key then
|
|
return
|
|
end
|
|
|
|
active_key_mapping_key = key_name
|
|
active_key_mapping_command = nil
|
|
if not key_name then
|
|
return
|
|
end
|
|
|
|
key_mapping_revision = key_mapping_revision + 1
|
|
local command_name = "sky_phone_toggle_config_" .. key_mapping_revision
|
|
active_key_mapping_command = command_name
|
|
RegisterCommand(command_name, function()
|
|
if active_key_mapping_command == command_name then
|
|
run_phone_toggle()
|
|
end
|
|
end, false)
|
|
RegisterKeyMapping(command_name, locale.Controls.OpenPhone, "keyboard", key_name)
|
|
end
|
|
|
|
refresh_phone_key_mapping()
|
|
|
|
RegisterKeyMapping(
|
|
"sky_phone_live_activity_open",
|
|
locale.Controls.OpenPhone,
|
|
"keyboard",
|
|
"SPACE"
|
|
)
|
|
|
|
refresh_test_data_command_suggestion = function()
|
|
if suggested_test_data_command then
|
|
TriggerEvent("chat:removeSuggestion", "/" .. suggested_test_data_command)
|
|
suggested_test_data_command = nil
|
|
end
|
|
if Config.TestData.Enabled then
|
|
suggested_test_data_command = Config.TestData.Command
|
|
TriggerEvent("chat:addSuggestion", "/" .. suggested_test_data_command, locale.TestData.CommandDescription)
|
|
end
|
|
end
|
|
|
|
refresh_test_data_command_suggestion()
|
|
|
|
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. Browser-owned focus claims
|
|
-- cannot survive unless their UI is replayed as part of this handshake.
|
|
SkyPhoneFocus.BeginNuiHydration()
|
|
Bridge.Debug("debug", "[sky_phone] NUI reported ready.", { always = true })
|
|
SkyPhoneApps.SendCatalog()
|
|
if open_requested and device_payload then
|
|
send_open_message()
|
|
end
|
|
if admin_panel_open then
|
|
send_admin_panel_open()
|
|
end
|
|
SkyPhoneCalls.ReplayNui()
|
|
SkyPhoneSimPicker.ReplayNui()
|
|
SkyPhoneFocus.Reapply()
|
|
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" })
|
|
SkyPhoneFocus.Reapply()
|
|
cb({ success = false, error = "open_not_requested" })
|
|
return
|
|
end
|
|
|
|
local was_open = is_open
|
|
is_open = true
|
|
SkyPhoneApps.SetPhoneOpen(true)
|
|
if not was_open then
|
|
TriggerEvent("sky_phone:client:phoneToggled", true)
|
|
end
|
|
SkyPhoneFocus.SetPhone(true, open_without_focus)
|
|
TriggerEvent("sky_phone:animation:phone", true)
|
|
cb({ success = true })
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:admin:launch", function()
|
|
if is_open or open_requested then
|
|
close_phone()
|
|
end
|
|
admin_panel_open = true
|
|
SkyPhoneFocus.SetAdminPanel(true)
|
|
send_admin_panel_open()
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:admin:command-error", function(error_code)
|
|
local messages = locale.AdminCommand.Errors
|
|
Bridge.Framework.Notify(
|
|
"iFruit",
|
|
messages[error_code] or messages.default,
|
|
"error",
|
|
5000
|
|
)
|
|
end)
|
|
|
|
RegisterNUICallback("admin:close", function(data, cb)
|
|
if type(data) ~= "table" then
|
|
cb({ success = false, error = "invalid_request" })
|
|
return
|
|
end
|
|
close_admin_panel()
|
|
cb({ success = true })
|
|
end)
|
|
|
|
RegisterNUICallback("ui:input-focus", function(data, cb)
|
|
if type(data) ~= "table" or type(data.active) ~= "boolean" then
|
|
cb({ success = false, error = "invalid_request" })
|
|
return
|
|
end
|
|
SkyPhoneFocus.SetTextInputFocused(
|
|
data.active and (is_open or open_requested or admin_panel_open)
|
|
)
|
|
cb({ success = true })
|
|
end)
|
|
|
|
RegisterNUICallback("ui:live-activity", function(data, cb)
|
|
if type(data) ~= "table" or type(data.active) ~= "boolean" then
|
|
cb({ success = false, error = "invalid_request" })
|
|
return
|
|
end
|
|
live_activity_active = data.active
|
|
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)
|
|
|
|
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.")
|
|
if not is_open then
|
|
open_requested = false
|
|
open_without_focus = false
|
|
end
|
|
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 }
|
|
)
|
|
apply_disabled_apps(data)
|
|
device_payload = data
|
|
update_equipped_phone_number(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
|
|
apply_disabled_apps(data)
|
|
device_payload = data
|
|
update_equipped_phone_number(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
|
|
update_equipped_phone_number(nil)
|
|
live_activity_active = false
|
|
open_home_requested = false
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:device:error", function(error_code)
|
|
if not is_open then
|
|
open_requested = false
|
|
open_without_focus = false
|
|
open_home_requested = false
|
|
end
|
|
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)
|
|
|
|
CreateThread(function()
|
|
refresh_admin_command_suggestion()
|
|
end)
|
|
|
|
AddEventHandler("onResourceStop", function(resource_name)
|
|
if resource_name ~= GetCurrentResourceName() then
|
|
return
|
|
end
|
|
|
|
is_open = false
|
|
open_requested = false
|
|
open_without_focus = false
|
|
admin_panel_open = false
|
|
|
|
TriggerEvent("sky_phone:animation:reset")
|
|
SkyPhoneCalls.Reset()
|
|
SkyPhoneSimPicker.Reset()
|
|
SkyPhoneFocus.Reset()
|
|
|
|
if active_development_command then
|
|
TriggerEvent("chat:removeSuggestion", "/" .. active_development_command)
|
|
end
|
|
if suggested_test_data_command then
|
|
TriggerEvent("chat:removeSuggestion", "/" .. suggested_test_data_command)
|
|
end
|
|
if suggested_admin_command then
|
|
TriggerEvent("chat:removeSuggestion", "/" .. suggested_admin_command)
|
|
end
|
|
end)
|