ENH - add persistent dynamic island activities (#17)

* 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>
This commit is contained in:
Leon.Schmidt
2026-08-20 18:59:51 +02:00
committed by GitHub
parent d0317a35c0
commit 36b859cabb
36 changed files with 1644 additions and 315 deletions
+34
View File
@@ -6,6 +6,8 @@ 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
@@ -75,7 +77,9 @@ local function send_open_message()
SendNUIMessage({
type = "app:open",
data = payload,
openHome = open_home_requested,
})
open_home_requested = false
end
local function open_phone()
@@ -169,6 +173,25 @@ RegisterCommand("sky_phone_toggle", function()
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.")
@@ -255,6 +278,15 @@ RegisterNUICallback("ui:input-focus", function(data, cb)
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" })
@@ -302,6 +334,8 @@ RegisterNetEvent("sky_phone:device:invalidated", function()
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)
+10 -12
View File
@@ -679,18 +679,16 @@ function SkyPhoneCompanies.GetCallTargets(company_id)
end
for source, readiness in pairs(call_availability) do
local member = online[source] and call_member(source) or nil
local device = member and current_device(source, true) or nil
if not member or not device or member.company_id ~= readiness.company_id
or readiness.company_id ~= company_id or readiness.imei ~= device.imei
or readiness.sim_id ~= device.sim_id
then
if not member or not device or member.company_id ~= readiness.company_id
or readiness.imei ~= (device and device.imei)
or readiness.sim_id ~= (device and device.sim_id)
then
call_availability[source] = nil
end
else
local device = member and SkyPhone.LoadDevice(readiness.imei) or nil
local device_slots = device and SkyPhone.FindDeviceSlots(source, readiness.imei) or {}
local readiness_valid = member and device and device_slots[1]
and member.company_id == readiness.company_id
and device.sim_id == readiness.sim_id
and device.sim_type == "registered"
and device.registered_at ~= nil
if not readiness_valid then
call_availability[source] = nil
elseif readiness.company_id == company_id then
targets[#targets + 1] = {
source = source,
simId = device.sim_id,
+19 -2
View File
@@ -51,6 +51,23 @@ local function session_owner(source)
}
end
local function device_owner(source, imei)
if not SkyPhoneImei.IsValid(imei) then
return nil, { success = false, error = "invalid_request" }
end
if not SkyPhone.FindDeviceSlots(source, imei)[1] then
return nil, { success = false, error = "owner_changed" }
end
local device = SkyPhone.LoadDevice(imei)
if not device then
return nil, { success = false, error = "device_not_found" }
end
return {
account_id = device.account_id and tonumber(device.account_id) or nil,
imei = imei,
}
end
local function owner_condition(owner, alias)
local prefix = alias and ("`%s`."):format(alias) or ""
if owner.account_id then
@@ -317,7 +334,7 @@ RegisterNetEvent("sky_phone:memos:request-upload", function(data)
upload_result(src, type(data) == "table" and data.correlationId or nil, false, "invalid_memo")
return
end
local owner, error_response = session_owner(src)
local owner, error_response = device_owner(src, data.deviceImei)
if not owner then
upload_result(src, memo.correlation_id, false, error_response.error)
return
@@ -392,7 +409,7 @@ RegisterNetEvent("sky_phone:memos:complete-upload", function(data)
return
end
state.completing = true
local owner, error_response = session_owner(src)
local owner, error_response = device_owner(src, state.owner.imei)
if not owner or owner.imei ~= state.owner.imei or owner.account_id ~= state.owner.account_id then
pending_uploads[request_id] = nil
local rejected, _, trusted_remote = SkyPhoneMedia.VerifyRemoteUpload(state, data.remoteId, data.url)
-1
View File
@@ -958,7 +958,6 @@ Bridge.Debug(
)
Bridge.Callbacks.Register("sky_phone:device:close", function(source)
SkyPhoneCompanies.ClearCallAvailability(source)
sessions[source] = nil
return { success = true }
end)