Skip to content

Phone Integration

DooDesch edited this page Jul 27, 2026 · 2 revisions

Phone Integration

🛟 Need help or found a bug? Get support at support.doodesch.de/sideload.

What being on the phone gives your app, and the short list of things it does not.

The app icon

Ship icon.png in your bundle: a square PNG, 256x256 is plenty, transparent outside the rounded square, drawn the way the vanilla app icons are. Without one your app gets a flat coloured square derived from its id.

The caption under it is iconLabel from Apps.Register, defaulting to the title.

Both orientations

An app names the orientations it supports, in preference order. The first is what it opens in; naming two is what lets the player turn the phone.

.Orientation("landscape")               // landscape only, the phone never turns
.Orientation("landscape", "portrait")   // both, opens landscape
.Orientation("portrait", "landscape")   // both, opens portrait
// no call at all                       -> landscape only

Saying nothing locks you to landscape, which is the only safe reading of silence: an app that never styled portrait must not be turned into it.

Do not build a turn button. The player turns the phone with the game's own rotate keys - Q and E out of the box, gamepad triggers too, rebindable in the game's controls screen - and Sideload adds a "Rotate Phone" line to the game's key-hint strip while a turnable app is open. Your app's screen stays yours.

The choice is remembered per app. Do not store it yourself.

s1.setOrientation(v) exists for the rare app that must turn itself for a reason of its own - a video, a map. It is refused with a log line when the app never declared that orientation. s1.orientation reads the current one.

A turn re-measures the viewport and lays the page out again; the document and the script survive, so scroll positions and field contents are kept. Style both from one sheet:

@media (orientation: portrait) {
  .app { flex-direction: column; }
  .sidebar { width: 100%; flex: 1; }
  .detail { display: none; }
  .app.on-detail .sidebar { display: none; }
  .app.on-detail .detail { display: flex; }
}

A two-pane split rarely survives 400px, so portrait usually wants push navigation - the script marks which pane is showing, the stylesheet decides what that means. Pair it with orientationchange and back, both in JavaScript, DOM and Events.

Unread badge

app.Badge(unreadCount);   // zero clears it

The same red badge the vanilla apps use. Counts above 99 read as "99+". Set it whenever your own count changes, not on a timer: the value survives the phone being rebuilt, so setting it once is enough and setting it again is cheap.

Notifications

app.Notify("Jessi Waters", "on my way");

Raises one of the game's own phone notifications - the slide-in the vanilla apps use - carrying your app's icon. Nothing happens if the app is not on a phone yet.

This interrupts whatever the player is doing, so spend it on what they would want to be interrupted for. A count that can wait belongs in Badge.

Check before you interrupt

if (!app.IsOnScreen) app.Notify(sender, text);

True while your app is the one the phone shows. Ask before interrupting: an event the player is already watching happen does not deserve a notification, and the same event with the phone in their pocket does.

Images

<img src="..."> paints a file from your bundle:

<img class="mark" src="glyph.png" alt="">
.mark { width: 20px; height: 20px; color: #FFFFFF; }

Both dimensions are required. The layout runs without Unity and cannot open a PNG to learn an intrinsic size the way a browser does, so an image with no width and height is a box of nothing. The aspect ratio is preserved inside whatever box you give it, and color tints the image - so one white glyph works on a dark bar and on a light one.

For a picture your mod produces at runtime - fetched, generated, per player - hand it over as PNG bytes and reference it by name:

app.Image("avatar/" + steamId, pngBytes);   // null or empty bytes remove it
<img class="avatar" src="s1://avatar/76561198000000001">

Sprites are cached per app. Supplying the same name again replaces the picture.

What the phone does NOT give a page

Hard walls, not gaps you can work around from the page. Check here before promising a feature.

You want Reality
read or set a scroll offset No. scrollToEnd() is the only scroll member. You cannot restore a per-view scroll position, and you cannot ask "is the reader at the bottom". The host restores offsets across a rebuild by itself.
know the app was opened or closed No from the page. orientationchange is the only signal about its own state. Your mod can ask with app.IsOnScreen.
a key other than Enter No. keydown fires for Enter in a field and nothing else. No shortcuts, no arrow keys, no modifiers.
focus a control from script No. Focus follows the player's click.
the clipboard, a file, a time zone No. There is no window and no navigator.

Anything a page cannot do, your mod usually can - it is ordinary C# with the whole game in reach. The question is not "how do I do this in JavaScript" but "does this belong behind an s1.call".

Clone this wiki locally