GKSHOP
GKSPHONE V2

Custom App

GKS Tablet lets you add custom iframe apps at runtime. Create a separate resource with a NUI page, register it with AddCustomApp, and it appears in the App Store / home screen like a built-in app.

The API matches GKSPhone Custom App. Use the starter template: gksphone-app on GitHub.

Custom apps using UI

Create a separate resource and set appurl to your HTML file (https://cfx-nui-{resource}/{path}).

When the app opens, the tablet loads your page in an iframe and injects window.gkstablet (and window.gksphone as an alias for shared phone/tablet UIs).

Adding the app

Use the AddCustomApp export from your client script:

exports['gks-tablet']:AddCustomApp({
    name = "FleetTracker", -- unique app name
    appurl = "https://cfx-nui-my-tablet-app/ui/index.html", -- your NUI URL
    icons = "https://cfx-nui-my-tablet-app/ui/icon.png", -- app icon
    description = "Track department vehicles", -- App Store description
    show = true, -- show in App Store
    startapp = false, -- auto-install on home screen
    allowjob = { "police", "lspd" }, -- job whitelist (optional)
    blockedjobs = {}, -- job blacklist (optional)
    labelLangs = { -- localized name (optional)
        en = "Fleet Tracker",
        tr = "Filo Takip",
    },
    onOpen = function(tabletUniqueId) -- runs when app opens (optional)
        print("Opened on tablet:", tabletUniqueId)
    end,
    onClose = function(tabletUniqueId) -- runs when app closes (optional)
        print("Closed")
    end,
})

Call AddCustomApp again after a resource restart — install state is saved in the database, but metadata (URL, icon, jobs) comes from your export each session.

When the iframe loads, the tablet POSTs to https://{your-resource}/getInfo. Handle it to sync data on open:

RegisterNUICallback('getInfo', function(data, cb)
    cb('ok')
end)

You can also register lifecycle callbacks by resource name:

exports['gks-tablet']:onAppOpen('my-tablet-app', function(tabletUniqueId) end)
exports['gks-tablet']:onAppClose('my-tablet-app', function(tabletUniqueId) end)
exports['gks-tablet']:removeAppCallbacks('my-tablet-app')

InputChange

Prevent the player from walking while typing in iframe inputs.

exports['gks-tablet']:InputChange(true)  -- allow movement
exports['gks-tablet']:InputChange(false) -- block movement

Or use the bridge in HTML:

<input onfocus="window.gkstablet?.inputFocused(false)" onblur="window.gkstablet?.inputFocused(true)">

Sending a message to the UI

Use NuiSendMessage instead of SendNUIMessage:

exports['gks-tablet']:NuiSendMessage({ event = 'update', value = 42 })

Listen in your frontend the same way as a normal NUI message:

window.addEventListener('message', (event) => {
    if (event.data?.event === 'update') {
        console.log(event.data.value)
    }
})

Imported functions

When the iframe loads, the tablet injects a bridge into window.gkstablet. window.gksphone points to the same object (for shared phone/tablet code).

NameTypeDescription
urlstringYour resource name (from appurl)
isDarkMode()functionReturns tablet dark mode state
onChangeDarkMode(cb)functionCalled when theme changes
loadingPopup(text)functionShow loading dialog
closeLoadingPopup()functionClose loading dialog
notify(text, timeout)functionShow toast
fetchNui(event, data)functionSend NUI callback
inputFocused(allowWalk)functionBlock/unblock movement while typing
GetGallery(...)functionOpen gallery picker
FullScreenImage(url)functionOpen image viewer
SelectEmoji(open)functionOpen emoji picker
setStatusBarColor(color)functionSet page background color
calling(number, anon)functionNot supported on tablet (no-op)
videoCall(number)functionNot supported on tablet (no-op)
CameraOpen(...)functionReturns null on tablet

fetchNui

window.gkstablet.fetchNui('my-event', { foo: 'bar' })

isDarkMode

const dark = window.gkstablet.isDarkMode()

notify

window.gkstablet.notify('Saved!', 2000)

GetGallery

const result = await window.gkstablet.GetGallery(false, true, false, false)
// { data: 'url', opencamera: false }

The tablet's picker ignores onlyVideo, onlyPicture and multiple — it always shows the same sheet. Only the fourth argument does anything: pass camera = true and it returns { data: false, opencamera: true } straight away, without opening the picker at all.

FullScreenImage

window.gkstablet.FullScreenImage('https://example.com/photo.jpg')

SelectEmoji

window.gkstablet.SelectEmoji(true)

window.addEventListener('message', (event) => {
    if (event.data?.type === 'emojiSelected') {
        console.log(event.data.eventData)
    }
})

setStatusBarColor

window.gkstablet.setStatusBarColor('#000000')

On this page