components_config_SetServerScreen.bs
import "pkg:/source/roku_modules/log/LogMixin.brs"
import "pkg:/source/utils/config.bs"
import "pkg:/source/utils/misc.bs"
sub init()
m.log = log.Logger("SetServerScreen")
m.top.setFocus(true)
m.serverPicker = m.top.findNode("serverPicker")
m.serverUrlTextbox = m.top.findNode("serverUrlTextbox")
m.serverUrlContainer = m.top.findNode("serverUrlContainer")
m.serverUrlOutline = m.top.findNode("serverUrlOutline")
m.serverUrlOutline.blendColor = m.global.constants.colorPrimary
m.buttons = m.top.findNode("buttons")
m.buttons.callFunc("center")
m.top.observeField("serverUrl", "clearErrorMessage")
ScanForServers()
end sub
sub ScanForServers()
startLoadingSpinner(false)
m.ssdpScanner = CreateObject("roSGNode", "ServerDiscoveryTask")
'run the task
m.ssdpScanner.observeField("content", "ScanForServersComplete")
m.ssdpScanner.control = "RUN"
end sub
sub ScanForServersComplete(event)
m.servers = event.getData()
items = CreateObject("roSGNode", "ContentNode")
stopLoadingSpinner()
' Load saved servers upfront — used in both passes below
savedServers = { serverList: [] }
saved = get_setting("saved_servers")
if isValid(saved)
parsed = ParseJson(saved)
if isValid(parsed) and isValid(parsed.serverList)
savedServers = parsed
end if
end if
' Pass 1: Add all SSDP-discovered servers.
' If a matching saved entry exists, inject its originalUrl so the user sees their own
' URL (e.g. "192.168.1.100:8096" or an HTTPS address) instead of the raw SSDP URL.
' Dedup uses id first (robust to URL changes), then falls back to canonical baseUrl
' for backward compatibility with saved entries that predate this change.
for each serverItem in m.servers
serverItem.subtype = "ContentNode"
for each savedServer in savedServers.serverList
isMatch = false
if isValidAndNotEmpty(serverItem.id) and isValidAndNotEmpty(savedServer.id)
isMatch = (serverItem.id = savedServer.id)
else if LCase(serverItem.baseUrl) = savedServer.baseUrl
isMatch = true
end if
if isMatch
' Merge: keep SSDP name and connection URL, restore saved originalUrl
savedOriginalUrl = savedServer.originalUrl
if not isValidAndNotEmpty(savedOriginalUrl)
savedOriginalUrl = savedServer.baseUrl ' backward compat: old entries have no originalUrl
end if
serverItem.originalUrl = savedOriginalUrl
serverItem.isSaved = true ' mark as deletable — this server exists in saved_servers
exit for
end if
end for
items.update([serverItem], true)
end for
' Pass 2: Add saved servers not found by SSDP (e.g. remote servers)
for each savedServer in savedServers.serverList
alreadyListed = false
for each listed in m.servers
if isValidAndNotEmpty(listed.id) and isValidAndNotEmpty(savedServer.id)
if listed.id = savedServer.id
alreadyListed = true
exit for
end if
else if LCase(listed.baseUrl) = savedServer.baseUrl
alreadyListed = true
exit for
end if
end for
if not alreadyListed
savedServer.isSaved = true ' mark as deletable — this server exists in saved_servers
items.update([savedServer], true)
m.servers.push(savedServer)
end if
end for
m.serverPicker.content = items
'if we have at least one server, focus on the server picker
if m.servers.Count() > 0
m.serverPicker.setFocus(true)
'no servers found...focus on the input textbox
else
m.serverUrlContainer.setFocus(true)
'show/hide input box outline
m.serverUrlOutline.visible = true
end if
end sub
sub ShowKeyboard()
dialog = createObject("roSGNode", "StandardKeyboardDialog")
dialog.title = tr("Enter the server name or ip address")
dialog.buttons = [tr("OK"), tr("Cancel")]
dialog.text = m.serverUrlTextbox.text
greenPalette = createObject("roSGNode", "RSGPalette")
greenPalette.colors = { DialogBackgroundColor: "#2A2B2A" }
dialog.palette = greenPalette
m.top.getscene().dialog = dialog
m.dialog = dialog
dialog.observeField("buttonSelected", "onDialogButton")
end sub
function onDialogButton()
d = m.dialog
button_text = d.buttons[d.buttonSelected]
if button_text = tr("OK")
m.serverUrlTextbox.text = d.text
m.dialog.close = true
return true
else if button_text = tr("Cancel")
m.dialog.close = true
return true
else
return false
end if
end function
sub clearErrorMessage()
m.top.errorMessage = ""
end sub
' JRScreen hook called when the screen is displayed by the screen manager
sub OnScreenShown()
' Clear backdrop on config screens
m.global.sceneManager.callFunc("setBackgroundImage", "")
scene = m.top.getScene()
overhang = scene.findNode("overhang")
if isValid(overhang)
overhang.isLogoVisible = true
overhang.currentUser = ""
overhang.title = ""
end if
end sub
function onKeyEvent(key as string, press as boolean) as boolean
m.log.debug("SetServerScreen onKeyEvent", key, press)
if not press then return true
handled = true
if key = "OK" and m.serverPicker.hasFocus()
item = m.serverPicker.content.getChild(m.serverPicker.itemFocused)
' Prefer originalUrl (user-entered or scheme-stripped SSDP URL) so inferServerUrl()
' can re-discover the correct protocol rather than locking in the canonical form
selectedUrl = item.originalUrl
if not isValidAndNotEmpty(selectedUrl)
selectedUrl = item.baseUrl
end if
m.top.serverUrl = selectedUrl
m.buttons.setFocus(true)
'if the user pressed the down key and we are already at the last child of server picker, then change focus to the url textbox
else if key = "down" and m.serverPicker.hasFocus() and m.serverPicker.content.getChildCount() > 0 and m.serverPicker.itemFocused = m.serverPicker.content.getChildCount() - 1
m.serverUrlContainer.setFocus(true)
'user navigating up to the server picker from the input box (it's only focusable if it has items)
else if key = "up" and m.serverUrlContainer.hasFocus() and m.servers.Count() > 0
m.serverPicker.setFocus(true)
else if key = "up" and m.serverUrlContainer.hasFocus() and m.servers.Count() = 0
ScanForServers()
else if key = "back" and m.serverUrlContainer.hasFocus() and m.servers.Count() > 0
m.serverPicker.setFocus(true)
else if key = "OK" and m.serverUrlContainer.hasFocus()
ShowKeyboard()
else if key = "back" and m.buttons.isInFocusChain() and m.servers.Count() > 0
m.serverPicker.setFocus(true)
else if key = "back" and m.buttons.isInFocusChain() and m.servers.Count() = 0
m.serverUrlContainer.setFocus(true)
else if key = "back" and m.serverUrlContainer.hasFocus() and m.servers.Count() = 0
ScanForServers()
else if key = "back" and m.serverPicker.hasFocus() and m.servers.Count() > 0
ScanForServers()
' On "back" with or without available local servers, will rescan for servers
else if key = "up" and m.buttons.isInFocusChain()
m.serverUrlContainer.setFocus(true)
'focus the submit button from serverUrl
else if key = "down" and m.serverUrlContainer.hasFocus()
m.buttons.setFocus(true)
else if key = "options"
if m.serverPicker.itemFocused >= 0 and m.serverPicker.itemFocused < m.serverPicker.content.getChildCount()
item = m.serverPicker.content.getChild(m.serverPicker.itemFocused)
if m.servers.Count() > 0 and item.isSaved = true
'Can only delete previously saved servers, not locally discovered ones
'So if we are on a saved item, let the options dialog be shown (handled elsewhere)
handled = false
end if
end if
else
handled = false
end if
'show/hide input box outline
m.serverUrlOutline.visible = m.serverUrlContainer.isInFocusChain()
return handled
end function
' destroy: Full teardown releasing all resources before component removal
' Called automatically by SceneManager.popScene() / clearScenes()
sub destroy()
m.log.verbose("destroy")
' Unobserve m.top observer
m.top.unobserveField("serverUrl")
' Dialog may or may not be open — unobserve and release if present
if isValid(m.dialog)
m.dialog.unobserveField("buttonSelected")
m.dialog = invalid
end if
' Stop and release task node (created by ScanForServers on init)
if isValid(m.ssdpScanner)
m.ssdpScanner.unobserveField("content")
m.ssdpScanner.control = "STOP"
m.ssdpScanner = invalid
end if
' Clear node references
m.serverPicker = invalid
m.serverUrlTextbox = invalid
m.serverUrlContainer = invalid
m.serverUrlOutline = invalid
m.buttons = invalid
' Release server list data
m.servers = invalid
end sub