> For the complete documentation index, see [llms.txt](https://grp-development.gitbook.io/titan-scripts/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://grp-development.gitbook.io/titan-scripts/resources/titan-tablet/custom-apps.md).

# Custom apps

Add your own apps to TITAN Tablet from any resource.

Any resource can add its own app to TITAN Tablet. The app gets a Home Screen icon, opens in its own window, works in Split View and the app switcher, and can send notifications and set a badge, just like a built-in app. The tablet loads your app's web page inside its screen, and your resource stays in full control of what the page does.

```lua
Config.CustomApps = {
    Enabled = true,
    MaxApps = 24,
    AllowExternalUi = false,
}
```

| Setting           | Description                                                                                   |
| ----------------- | --------------------------------------------------------------------------------------------- |
| `Enabled`         | `false` ignores every app other resources try to add.                                         |
| `MaxApps`         | Most custom apps the tablet lists at the same time.                                           |
| `AllowExternalUi` | `true` lets apps load their page from any `https://` address instead of only from a resource. |

#### Templates

The quickest way to start is a ready-made template. Each one is a complete resource with an icon, a finished starter screen and a tablet preview that runs in your browser, so you can build the page without joining the server.

{% embed url="<https://github.com/svgAPOLLO/titan-tablet-app-template>" %}

| Template          | Stack                                   |
| ----------------- | --------------------------------------- |
| `titan-vanillajs` | HTML, CSS and JavaScript, no build step |
| `titan-reactjs`   | React and Vite                          |
| `titan-reactts`   | React, TypeScript and Vite              |
| `titan-vuejs`     | Vue 3 and Vite                          |
| `titan-svelte`    | Svelte 5 and Vite                       |

Copy the template you like into your resources, rename the folder and follow its README. `ttn-tablet-example-app`, included with the tablet, shows every feature in one app.

#### Adding an app

{% stepper %}
{% step %}
**Register the app**

Register the app from a client script, and again on `ttn-tablet:client:ready` so it comes back when the tablet restarts.

{% code title="your-resource/client.lua" %}

```lua
local function registerApp()
    exports['ttn-tablet']:RegisterApp({
        id = 'fleet',
        name = 'Fleet',
        icon = 'icon.svg',
        ui = 'web/index.html',
        tint = '#FF9F0A',
        developer = 'Your Studio',
        onMessage = function(data)
            return { ok = true }
        end,
    })
end

AddEventHandler('ttn-tablet:client:ready', registerApp)

CreateThread(function()
    while GetResourceState('ttn-tablet') ~= 'started' do
        Wait(500)
    end
    registerApp()
end)
```

{% endcode %}
{% endstep %}

{% step %}
**Add a ui\_page**

Your resource needs a `ui_page` so the page can reach your NUI callbacks. An empty HTML page is enough.

{% code title="your-resource/fxmanifest.lua" %}

```lua
ui_page 'blank.html'

files {
    'blank.html',
    'icon.svg',
    'web/**/*',
}

dependency 'ttn-tablet'
```

{% endcode %}
{% endstep %}

{% step %}
**Build the page**

Create `web/index.html` and load the page SDK, as shown in The page below. Restart your resource and open the tablet.
{% endstep %}
{% endstepper %}

`RegisterApp` returns `true`, or `false` and a reason. Registering the same `id` again updates the app, and apps are removed when the resource that added them stops. Apps can also be registered from the server, which adds them for every player; a server `onMessage` gets `(source, data)`. See the [server exports](https://grp-development.gitbook.io/titan-scripts/resources/titan-tablet/exports-and-events/server-exports).

#### App fields

| Field           | Type     | Description                                                                          |
| --------------- | -------- | ------------------------------------------------------------------------------------ |
| `id`            | string   | Required. Unique id, 2 to 32 characters of `a-z`, `0-9`, `_` or `-`                  |
| `name`          | string   | Required. Name under the icon, up to 24 characters                                   |
| `ui`            | string   | Required. Page inside your resource, for example `web/index.html`                    |
| `icon`          | string   | Square icon inside your resource, or a full link                                     |
| `tint`          | string   | Accent colour as `#RRGGBB`                                                           |
| `developer`     | string   | Shown as "By ..." while the app loads                                                |
| `description`   | string   | Short description, up to 240 characters                                              |
| `version`       | string   | Your app's version                                                                   |
| `dark`          | boolean  | Dark window behind the page. Default `false`                                         |
| `sharp`         | boolean  | Draw the page at the tablet's real resolution. Default `true`                        |
| `split`         | boolean  | `false` keeps the app out of Split View. Default `true`                              |
| `removable`     | boolean  | `false` stops players from hiding it. Default `true`                                 |
| `notifications` | boolean  | `false` blocks notifications from this app. Default `true`                           |
| `badge`         | number   | Starting badge on the icon, 0 to 999                                                 |
| `jobs`          | table    | Only these jobs see the app: `{ 'mechanic' }`, or with a minimum grade `{ tow = 1 }` |
| `items`         | table    | Only players with one of these items see the app                                     |
| `canOpen`       | function | Client only. Extra check, return `false` to hide the app                             |
| `onOpen`        | function | Called when the player opens the app                                                 |
| `onClose`       | function | Called when the player leaves the app or closes the tablet                           |
| `onMessage`     | function | Receives `TitanTablet.message(data)` from the page. Its return value is sent back    |

#### The page

Load the page SDK from the tablet. It adds `window.TitanTablet`.

{% code title="your-resource/web/index.html" %}

```html
<script src="https://cfx-nui-ttn-tablet/html/app-sdk.js"></script>
<script>
    TitanTablet.on('ready', function (context) {
        document.getElementById('name').textContent = context.player.name;
    });

    TitanTablet.on('message', function (data) {
        console.log('from Lua', data);
    });

    TitanTablet.ready();
</script>
```

{% endcode %}

| Function                               | Description                                                                                                 |
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `on(event, fn)` / `off(event, fn)`     | Listen to `ready`, `context` (any change, like theme or screen size) and `message` (from `SendAppMessage`). |
| `ready()`                              | Tells the tablet your page has loaded.                                                                      |
| `message(data)`                        | Sends data to your `onMessage`. Returns a promise with its reply, or `null` after 10 seconds.               |
| `fetch(name, data)`                    | Calls your resource's NUI callback `name` and returns its reply.                                            |
| `notify({ title, message })`           | Shows a tablet notification.                                                                                |
| `toast(text, kind)`                    | Shows a short message at the top of the screen.                                                             |
| `badge(count)`                         | Sets the badge on your icon.                                                                                |
| `open(app, params)`                    | Opens a built-in app, like `maps` or `mail`.                                                                |
| `close()` / `home()` / `closeTablet()` | Leaves your app, goes to the Home Screen, or puts the tablet away.                                          |
| `context`                              | The latest context.                                                                                         |

| Context field                       | Description                               |
| ----------------------------------- | ----------------------------------------- |
| `id` / `name` / `resource` / `tint` | Your app.                                 |
| `screen`                            | `full` or `compact` (a Split View pane).  |
| `theme`                             | `light` or `dark`.                        |
| `textSize`                          | The player's text size setting.           |
| `language`                          | The tablet's language.                    |
| `safeTop`                           | Space taken by the status bar, in pixels. |
| `player`                            | `{ name, email, number }` of the player.  |

The SDK also sets `data-theme`, `data-screen` and `data-text` on `<html>`, and the CSS variables `--tablet-tint` and `--safe-top`, so you can style the page with plain CSS.

{% hint style="info" %}
Pages written for TITAN Phone's `TitanPhone` SDK also run on the tablet when TITAN Phone is on your server. Screen Cast shows a custom app as its icon and name, because the tablet can't copy another resource's page.
{% endhint %}
