Methods
(static) buildReplayRoutes(stashed) → {object}
Pure: turn a guard-stashed path into the ordered route chain to replay. Home is always first (back-stack root); a /play path additionally inserts its Details screen between Home and the player (the details path is the play path minus the trailing "/play").
| Name | Type | Description |
|---|---|---|
stashed | dynamic |
- Source
- Type:
- object
(static) deepLinkDetailsRoute(deepLink) → {string}
Build the /details route for a deep link. The :type is just a display hint (ItemDetails resolves the REAL type from the fetched item). The ?deeplink=
| Name | Type | Description |
|---|---|---|
deepLink | object |
- Source
- Type:
- string
(static) findSavedServerByGuid(guid) → {object}
Look up a saved server entry by its Jellyfin server id (GUID). Returns the entry ({ name, id, baseUrl, originalUrl, ... }) or invalid. The pure match logic lives in config.bs (findServerInList) next to the saved_servers shape it matches.
| Name | Type | Description |
|---|---|---|
guid | string |
- Source
- Type:
- object
(static) isDeepLinkPlayerView(subtype) → {boolean}
Active routed views that ARE a media player (a deep link arriving over one must tear it down before launching the next, not stack on top).
| Name | Type | Description |
|---|---|---|
subtype | string |
- Source
- Type:
- boolean
(static) isPlausibleDeepLinkItemId(id) → {boolean}
A deep-link itemId is concatenated raw into the details route AND the metadata-fetch URL ("/Items/
| Name | Type | Description |
|---|---|---|
id | string |
- Source
- Type:
- boolean
(static) isPlausibleDeepLinkToken(value) → {boolean}
Safe-token check for deep-link fields that ride UNENCODED into the details route — mediaType in the PATH ("/details/
| Name | Type | Description |
|---|---|---|
value | string |
- Source
- Type:
- boolean
(static) notifyDeepLinkDeferred() → {void}
Toast shown when a deep link arrives but the user must authenticate first — it explains why nothing happened yet and that the content will open after sign-in. No-op if nothing stashed.
- Source
- Type:
- void
(static) onRuntimeDeepLink() → {void}
RUNTIME (app already signed in): resolve the stashed deep link's server and act.
- no/absent server, or it's the active one -> replay now.
- a different SAVED server -> prompt to switch.
- an unknown server -> can't connect; toast + discard. When NOT signed in (user at pre-login), do nothing: the stash rides until login and validates on whatever server they sign into.
- Source
- Type:
- void
(static) onServerProbeDone(reachable) → {void}
Continuation of performServerSwitch once the off-thread reachability probe returns. On an unreachable target, surface the error and discard the deep link. On success, reuse the Change-Server machinery (resetRouter -> point at the new server -> SignOut -> reenterLogin); the stashed deep link survives the teardown and replays once the new server's login completes.
| Name | Type | Description |
|---|---|---|
reachable | boolean |
- Source
- Type:
- void
(static) onServerSwitchDialogResult(dialog) → {void}
The server-switch prompt was answered (or dismissed). On "Switch", run the pre-flighted switch; on cancel, discard the stashed deep link.
IDENTITY, not a flag. Every dialog delivers its result on its OWN node, so holding the node we opened and comparing it against the event's is what tells us the event is ours — and it stays correct if a second main-thread flow ever observes result on the same port. The old path could not do this: every dialog wrote one shared SceneManager field, so it needed an isPendingServerSwitch flag to keep routed views' dialogs from cross-firing here.
It is also what makes a SECOND cast arriving mid-prompt correct with no code here, and the mechanism is worth stating because it rests on port delivery being ASYNCHRONOUS. Cast B overwrites the stash and calls promptServerSwitch again; presentOverlayDialog supersedes dialog A, which resolves as cancelled inside that call. But that result only reaches us at Main()'s next wait(0, m.port) — by which time promptServerSwitch has already reassigned m.serverSwitchDialog to dialog B. So A's event arrives and this guard rejects it: no toast, no stash wipe, and the prompt on screen is the one whose state the stash actually matches. Were delivery synchronous the guard would not save us: A's cancel would run while m.serverSwitchDialog still POINTED at A, so it would be accepted as the live dialog and wipe m.pendingSwitchServer and the stash — out from under the prompt B is about to show.
| Name | Type | Description |
|---|---|---|
dialog | object |
- Source
- Type:
- void
(static) parseDeepLinkContentId(raw) → {object}
Parse a Roku deep-link contentId into { itemId, serverGuid, action }. contentId is the only deep-link field that survives BOTH Roku ingress paths intact (cold-start /launch standardizes only contentId+mediaType; /input forwards arbitrary params but /launch does not), and Roku forbids "&" inside it — so the whole contract rides inside contentId, pipe-delimited (Roku's own documented convention, e.g. series=x|Season=1). We mint it as "id=
| Name | Type | Description |
|---|---|---|
raw | string |
- Source
- Type:
- object
(static) performServerSwitch(entry) → {void}
Perform the confirmed switch. Pre-flight the target FIRST (non-destructive) so an offline server never strands the user. The probe runs on a Task thread (ServerReachableTask) so the up-to-8s reachability check can't freeze remote input; the switch itself continues in onServerProbeDone once the verdict lands back on the main loop.
| Name | Type | Description |
|---|---|---|
entry | object |
- Source
- Type:
- void
(static) promptServerSwitch(entry) → {void}
Ask before switching servers (it signs the user out of the current one).
This runs on the MAIN thread, which has no component scope for observeFieldScoped, so the result comes back through Main()'s message port instead of a scoped observer — see the two-channel contract at the top of source/utils/dialogs.bs. main.bs routes every result event on its port to onServerSwitchDialogResult below.
| Name | Type | Description |
|---|---|---|
entry | object |
- Source
- Type:
- void
(static) replayAfterLogin() → {void}
Read + clear whatever is stashed and replay it after a successful login (or immediately when already authed). Deep link wins over a guard path. Always navigates Home first so it is the back-stack root.
- Source
- Type:
- void
(static) replayDeepLinkRuntime() → {void}
Runtime replay (app already signed in, deep link for the active server). Unlike the post-login path above, this does NOT prepend Home — an in-app cast shouldn't flash through the Home screen.
- already on the target item's LOADED details -> launch the action in place (no fetch, no re-mount); for "open" we're already showing it, so nothing to do. The itemContent guard matters: a lingering empty details would otherwise swallow the cast.
- otherwise -> hand the intent to JRScene.resolveDeepLink, which VALIDATES the id (a fetch) BEFORE navigating. An invalid id never disturbs the active session — it just toasts. A valid id then navigates (replacing an active player for a playback cast, else a plain push).
- Source
- Type:
- void
(static) stashDeepLink(rawContentId, mediaType, itemNameopt) → {void}
Stash a deep link (cold-start or runtime) as a structured intent for replayAfterLogin. Parses the contentId; records itemId + mediaType (+ serverGuid for the server resolver). Replaces the old queue-seed approach — ItemDetails now builds the queue from the validated item (real type + bookmark), so no queue work happens here. itemName is an OPTIONAL display title the sender (a casting client) may include so the server-switch prompt can name what's being cast — we can't fetch it ourselves before the switch (it lives on a server we're not yet authed to). Empty for bare/unknown senders.
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
rawContentId | string | |||
mediaType | string | |||
itemName | string | <optional> | "" |
- Source
- Type:
- void
(static) steerColdStartDeepLinkServer() → {void}
COLD start: if the stashed deep link names a saved server, point the login at it BEFORE beginLogin runs (no prompt — there is no active session to disrupt). An unknown/absent GUID is left alone: login proceeds normally and the deep link validates on whatever server we land on (graceful error -> Home if the item isn't there).
- Source
- Type:
- void
(static) wouldStackOverActivePlayer(action, activeSubtype) → {boolean}
True when a non-playback ("open") deep link would stack a details screen on TOP of an active media player, and so must be dropped rather than enacted. jellyfin-web's display-mirroring fires a DisplayContent ("open") on EVERY item-detail browse while JellyRock is the cast target, and the Roku OS can hand us an "open" deep link mid-playback — neither is an intent to interrupt playback, so the controller's browsing must never yank the cast target off the video. A PLAYBACK action (play/shuffle/trailer/instantmix) is exempt: it legitimately REPLACES the player (replacePlayer below). activeSubtype is the active routed view's subtype ("" / invalid when there's no active view). Pure so the rule is unit-testable without a live node.
| Name | Type | Description |
|---|---|---|
action | string | |
activeSubtype | dynamic |
- Source
- Type:
- boolean