mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-31 18:28:55 +00:00
36b859cabb
* FIX - layer dynamic island above phone camera The phone screen stacking context kept the Dynamic Island below the physical frame and its camera lens. Render the island at device-shell level, position it around the lens, and keep expanded notification spacing in sync. * ADD - preview all dynamic island states Reuse the runtime component for a development-only gallery and size expanded previews to the actual phone display so they cannot be clipped. * FIX - match dynamic island live activity behavior Show each activity only outside its owning app, use reference-sized activity layouts, and remove the separate expand glyph in favor of direct island taps. * FIX - refine dynamic island live controls Keep background activity state separate from foreground visibility so the island remains visible in the compact closed-phone peek. Match the timer, stopwatch, music, collapse, and spacing behavior to the supplied references. * FIX - keep business call availability after closing Stop treating the closed phone UI as the end of business-call readiness. Route background service-line calls only after revalidating the employee, owned device, and registered SIM. * FIX - remove dock apps from home grid The stored Home layout treated dock and grid shortcuts independently, so default dock apps appeared twice. Let the dock own those default placements and migrate saved layouts by removing matching grid shortcuts. * FIX - keep memo island active after closing Memo recording was cancelled as soon as the UI closed, while uploads required an active UI session and reopening a locked phone forced the home route. Keep the recorder bound to the physical device, revalidate device ownership server-side for uploads, preserve the last route behind the lock screen, and render only a compact frame, display, and live Dynamic Island while closed. * ENH - align live activity controls with phone hardware Report compact live-activity state to the client so Space opens Home or the enabled lock screen contextually. Apply the phone hardware volume as a shared output level for app media, games, YouTube playback, radio, and auxiliary sounds, and render the volume speaker icon in grey. * FIX - hide paused music live activity The music island previously depended only on the selected track, which remains set after playback pauses. Require active playback as well so the island closes when music stops while preserving the track for later resume. --------- Co-authored-by: DerEchteAlec <bycraky@gmail.com>
385 lines
11 KiB
Lua
385 lines
11 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 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 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 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 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
|
|
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
|
|
|
|
local result = Bridge.Callbacks.Trigger("sky_phone:device:open-request", {})
|
|
if type(result) ~= "table" or result.success ~= true then
|
|
open_without_focus = false
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
SkyPhoneClient.Toggle = toggle_phone
|
|
|
|
AddEventHandler("sky_phone:client:forceClose", function()
|
|
close_phone()
|
|
end)
|
|
|
|
if Config.Phone.DevelopmentCommand then
|
|
RegisterCommand(Config.Command, function()
|
|
if is_open or open_requested then
|
|
close_phone()
|
|
return
|
|
end
|
|
open_without_focus = false
|
|
Bridge.Callbacks.Trigger("sky_phone:device:development-open", {})
|
|
end, false)
|
|
end
|
|
|
|
RegisterCommand("sky_phone_toggle", function()
|
|
if is_open or open_requested then
|
|
close_phone()
|
|
return
|
|
end
|
|
|
|
open_without_focus = false
|
|
Bridge.Callbacks.Trigger("sky_phone:device:open-request", {})
|
|
end, false)
|
|
|
|
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
|
|
local result = Bridge.Callbacks.Trigger("sky_phone:device:open-request", {})
|
|
if not result or not result.success then
|
|
open_home_requested = false
|
|
end
|
|
end, false)
|
|
|
|
RegisterKeyMapping(
|
|
"sky_phone_live_activity_open",
|
|
locale.Controls.OpenPhone,
|
|
"keyboard",
|
|
"SPACE"
|
|
)
|
|
|
|
if Config.Phone.Keybind then
|
|
if type(Config.Phone.Keybind) ~= "string" or Config.Phone.Keybind == "" then
|
|
error("[sky_phone] Config.Phone.Keybind must be a non-empty keyboard key name or false.")
|
|
end
|
|
|
|
RegisterKeyMapping("sky_phone_toggle", locale.Controls.OpenPhone, "keyboard", Config.Phone.Keybind)
|
|
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. 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
|
|
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)
|
|
|
|
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))
|
|
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.")
|
|
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
|
|
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
|
|
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 and not open_requested then
|
|
open_without_focus = 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()
|
|
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
|
|
open_without_focus = false
|
|
|
|
TriggerEvent("sky_phone:animation:reset")
|
|
SkyPhoneCalls.Reset()
|
|
SkyPhoneSimPicker.Reset()
|
|
SkyPhoneFocus.Reset()
|
|
|
|
if Config.Phone.DevelopmentCommand then
|
|
TriggerEvent("chat:removeSuggestion", "/" .. Config.Command)
|
|
end
|
|
if Config.TestData.Enabled then
|
|
TriggerEvent("chat:removeSuggestion", "/" .. Config.TestData.Command)
|
|
end
|
|
end)
|