SkyPhoneCompatibility = {} function SkyPhoneCompatibility.RegisterExportAlias(resource_name, export_name, handler) assert(type(resource_name) == "string" and resource_name ~= "", "Export alias resource must be a non-empty string") assert(type(export_name) == "string" and export_name ~= "", "Export alias name must be a non-empty string") assert(type(handler) == "function", "Export alias handler must be a function") AddEventHandler(("__cfx_export_%s_%s"):format(resource_name, export_name), function(set_callback) set_callback(function(...) local debug_custom_app = SkyPhoneApps and SkyPhoneApps.Debug local invoking_resource = GetInvokingResource and GetInvokingResource() or nil if debug_custom_app then debug_custom_app( "export", "call provider=%s export=%s invoking_resource=%s argument_count=%s", resource_name, export_name, tostring(invoking_resource), select("#", ...) ) end local results = table.pack(handler(...)) if debug_custom_app then debug_custom_app( "export", "result provider=%s export=%s invoking_resource=%s success=%s error=%s", resource_name, export_name, tostring(invoking_resource), tostring(results[1]), tostring(results[2]) ) end return table.unpack(results, 1, results.n) end) end) end SkyPhoneCompatibility.Providers = { lb = "lb_phone", seventeen = "17mov", high = "high_phone", quasar = "quasar_v3", yseries = "yseries", } local function normalize_at_resource_url(owner_resource, url, error_prefix) if type(url) ~= "string" or url == "" then return nil, "invalid_" .. error_prefix end if url:sub(1, 7) == "http://" then return nil, "insecure_" .. error_prefix end if url:sub(1, 1) ~= "@" then return url end local url_owner, url_path = url:match("^@([^/]+)/(.+)$") if url_owner ~= owner_resource or not url_path then return nil, error_prefix .. "_owner_mismatch" end return owner_resource .. "/" .. url_path end local function resolve_lb_asset_resource(owner_resource, app_data) local ui = app_data.ui if type(ui) ~= "string" then return nil end local explicit_ui_resource = ui:match("^https://cfx%-nui%-([^/]+)/") or ui:match("^nui://([^/]+)/") local asset_resource = explicit_ui_resource or ui:match("^([%w][%w._-]*)/") if not asset_resource or asset_resource == owner_resource then return nil end if explicit_ui_resource or app_data.resource == asset_resource then return asset_resource end local icon = app_data.icon local icon_resource = type(icon) == "string" and ( icon:match("^https://cfx%-nui%-([^/]+)/") or icon:match("^nui://([^/]+)/") ) or nil if icon_resource == asset_resource then return asset_resource end return nil end local function build_locale_maps(app_name, locales) local names = {} local descriptions = {} if type(locales) == "table" then for locale, locale_data in pairs(locales) do if type(locale) == "string" and type(locale_data) == "table" then if type(locale_data.label) == "string" and locale_data.label ~= "" then names[locale] = locale_data.label end if type(locale_data.description) == "string" and locale_data.description ~= "" then descriptions[locale] = locale_data.description end end end end if not next(names) then return app_name, app_name end for locale, label in pairs(names) do if not descriptions[locale] then descriptions[locale] = label end end return names, descriptions end function SkyPhoneCompatibility.CopyQuasarData(app_data) local copy = {} for key, value in pairs(app_data) do if key == "iframe" and type(value) == "table" then copy.iframe = {} for iframe_key, iframe_value in pairs(value) do copy.iframe[iframe_key] = iframe_value end else copy[key] = value end end return copy end function SkyPhoneCompatibility.BuildLbDefinition(owner_resource, app_data) if type(app_data) ~= "table" or type(app_data.identifier) ~= "string" then return nil, "invalid_app_data" end local ui, ui_error = normalize_at_resource_url(owner_resource, app_data.ui, "ui") if not ui then return nil, ui_error end return { schemaVersion = 1, id = app_data.identifier, name = app_data.name, description = app_data.description, developer = app_data.developer, category = app_data.game and "games" or "utilities", ui = ui, assetResource = resolve_lb_asset_resource(owner_resource, app_data), bridgeMode = "legacy", icon = app_data.icon, defaultInstalled = app_data.defaultApp or false, removable = not app_data.defaultApp, orientation = app_data.landscape and "landscape" or "portrait", onInstall = app_data.onInstall, onOpen = app_data.onOpen or app_data.onUse, onClose = app_data.onClose, compatibility = { provider = SkyPhoneCompatibility.Providers.lb, apiVersion = 1, fixBlur = app_data.fixBlur == true, resourceName = type(app_data.resource) == "string" and app_data.resource or owner_resource, }, } end function SkyPhoneCompatibility.Build17MovDefinition(app_data) if type(app_data) ~= "table" or type(app_data.name) ~= "string" then return nil, "invalid_app_data" end return { schemaVersion = 1, id = app_data.name, name = app_data.label, description = app_data.description or app_data.label, category = "utilities", ui = app_data.ui, bridgeMode = "legacy", icon = app_data.icon, iconBackground = type(app_data.iconBackground) == "string" and app_data.iconBackground or nil, defaultInstalled = app_data.default or app_data.preInstalled or false, removable = not app_data.default, orientation = "portrait", compatibility = { provider = SkyPhoneCompatibility.Providers.seventeen, apiVersion = 1, }, } end function SkyPhoneCompatibility.BuildHighDefinition(owner_resource, app_name, data, locales) if type(owner_resource) ~= "string" or type(app_name) ~= "string" or type(data) ~= "table" then return nil, "invalid_app_data" end if #app_name > 64 or not app_name:match("^[a-z0-9][a-z0-9._-]+$") then return nil, "invalid_app_id" end local external_url, url_error = normalize_at_resource_url( owner_resource, data.externalUrl, "external_url" ) if not external_url then return nil, url_error end local name, description = build_locale_maps(app_name, locales) local icon = type(data.icon) == "table" and data.icon.imageUrl or data.icon return { schemaVersion = 1, id = app_name, name = name, description = description, developer = data.developer, category = "utilities", ui = external_url, bridgeMode = "legacy", icon = icon, iconBackground = type(data.icon) == "table" and data.icon.background or nil, defaultInstalled = data.preAdded or false, removable = data.removable ~= false, orientation = "portrait", compatibility = { provider = SkyPhoneCompatibility.Providers.high, apiVersion = 1, }, } end function SkyPhoneCompatibility.BuildQuasarDefinition(app_data) local iframe_url = type(app_data) == "table" and type(app_data.iframe) == "table" and app_data.iframe.url or nil if type(app_data) ~= "table" or type(app_data.id) ~= "string" or type(app_data.label) ~= "string" or type(iframe_url) ~= "string" then return nil, "invalid_app_data" end return { schemaVersion = 1, id = app_data.id, name = app_data.label, description = app_data.description or app_data.label, category = "utilities", ui = iframe_url, bridgeMode = "legacy", icon = app_data.icon, defaultInstalled = app_data.defaultInstalled or false, removable = true, orientation = "portrait", compatibility = { provider = SkyPhoneCompatibility.Providers.quasar, apiVersion = 1, }, } end function SkyPhoneCompatibility.BuildYSeriesDefinition(app_data) if type(app_data) ~= "table" or type(app_data.key) ~= "string" then return nil, "invalid_app_data" end local icon = app_data.icon if type(icon) == "table" then icon = icon.yos or icon.humanoid end return { schemaVersion = 1, id = app_data.key, name = app_data.name, description = app_data.description or app_data.name, category = app_data.game and "games" or "utilities", ui = app_data.ui, bridgeMode = "legacy", icon = icon, defaultInstalled = app_data.defaultApp or false, removable = true, orientation = "portrait", compatibility = { provider = SkyPhoneCompatibility.Providers.yseries, apiVersion = 1, }, } end