components_config_SetServerScreen.bs

import "pkg:/source/enums/ServerAction.bs"
import "pkg:/source/roku_modules/log/LogMixin.brs"
import "pkg:/source/translationKeys.bs"
import "pkg:/source/utils/config.bs"
import "pkg:/source/utils/misc.bs"
import "pkg:/source/utils/translate.bs"

sub init()
  m.log = new log.Logger("SetServerScreen")
  ' The router hands this view focus via the JRScreen lifecycle bridge (handleFocus).
  ' onScanForServersComplete sets the precise focus target (picker vs. textbox) once the SSDP
  ' scan returns.
  m.top.isOptionsAvailable = true
  ' Overhang config — declarative field projected onto the overhang by JRScene (registerOverhangData).
  m.top.isLogoVisible = true

  m.top.findNode("prompt").text = translate(translationKeys.LabelConnectToServer)
  m.top.findNode("serverPickerHint").text = translate(translationKeys.LabelPickAJellyfinServerFromThe)
  m.top.findNode("manualEntryHint").text = translate(translationKeys.LabelOrEnterServerUrlManually)
  m.top.findNode("submit").text = translate(translationKeys.ButtonSubmit)

  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")

  ' Prefill the previously-entered server URL.
  if isValid(m.global.server.serverUrl)
    m.top.serverUrl = m.global.server.serverUrl
  end if

  m.top.observeField("serverUrl", "clearErrorMessage")

  ' Submit drives the connect via the loginRouter coordinator (serverSubmitted intent); the
  ' options sidepanel offers "Delete Saved" for saved picker entries.
  m.buttons.observeField("buttonSelected", "onButtonSelected")
  setupDeleteSavedOption()

  ScanForServers()
end sub

' Build the "Delete Saved" options sidepanel entry (was CreateServerGroup 346-355).
sub setupDeleteSavedOption()
  sidepanel = m.top.findNode("options")
  opt = CreateObject("roSGNode", "OptionsButton")
  opt.title = translate(translationKeys.LabelDeleteSaved)
  opt.id = ServerAction.DELETE_SAVED
  opt.observeField("optionSelected", "onDeleteSaved")
  sidepanel.options = [opt]
  sidepanel.observeField("closeSidePanel", "onCloseSidePanel")
end sub

' Submit pressed → publish the entered URL and emit the serverSubmitted intent.
' The coordinator (loginRouter.onServerSubmitted) runs the blocking connect.
sub onButtonSelected(event as object)
  buttonGroup = event.getRoSGNode()
  buttonSelected = buttonGroup.getChild(event.getData())
  if buttonSelected.id = "submit"
    m.top.enteredUrl = m.top.serverUrl
    m.top.getScene().preLoginIntent = "serverSubmitted"
  end if
end sub

' Delete the focused saved server from the picker + registry (was CreateServerGroup 420-435).
sub onDeleteSaved()
  itemToDelete = m.serverPicker.content.getChild(m.serverPicker.itemFocused)
  if not isValid(itemToDelete) then return
  ' Prefer id for deletion — robust when the picker item's baseUrl differs from the
  ' saved entry's baseUrl (e.g. SSDP HTTP item merged from an HTTPS saved entry).
  idOrUrl = itemToDelete.id
  if not isValidAndNotEmpty(idOrUrl)
    idOrUrl = itemToDelete.baseUrl
  end if
  if isValidAndNotEmpty(idOrUrl)
    DeleteFromServerList(idOrUrl)
    m.serverPicker.content.removeChild(itemToDelete)
    m.top.findNode("options").visible = false
    m.serverPicker.setFocus(true)
  end if
end sub

' Sidepanel closed without a selection (was CreateServerGroup 366-369).
sub onCloseSidePanel()
  ' setFocus on the child focuses up the chain, so focusing m.top first is redundant.
  m.serverPicker.setFocus(true)
end sub

sub ScanForServers()
  startLoadingSpinner(false)

  m.ssdpScanner = CreateObject("roSGNode", "ServerDiscoveryTask")
  'run the task
  m.ssdpScanner.observeField("content", "onScanForServersComplete")
  m.ssdpScanner.control = "RUN"
end sub

sub onScanForServersComplete(event)
  m.servers = event.getData()

  items = CreateObject("roSGNode", "ContentNode")
  stopLoadingSpinner()

  ' Load saved servers upfront — used in both passes below
  savedServers = { serverList: [] }
  saved = getSetting("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
    isAlreadyListed = false
    for each listed in m.servers
      if isValidAndNotEmpty(listed.id) and isValidAndNotEmpty(savedServer.id)
        if listed.id = savedServer.id
          isAlreadyListed = true
          exit for
        end if
      else if LCase(listed.baseUrl) = savedServer.baseUrl
        isAlreadyListed = true
        exit for
      end if
    end for
    if not isAlreadyListed
      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 = translate(translationKeys.LabelEnterTheServerNameOrIp)
  dialog.buttons = [translate(translationKeys.ButtonOk), translate(translationKeys.ButtonCancel)]
  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

  if d.buttonSelected = 0 ' OK
    m.serverUrlTextbox.text = d.text
    m.dialog.close = true
    return true
  else if d.buttonSelected = 1 ' Cancel
    m.dialog.close = true
    return true
  end if

  return false
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", "")
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
  isHandled = 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 (isHandled elsewhere)
        isHandled = false
      end if
    end if
  else
    isHandled = false
  end if
  'show/hide input box outline
  m.serverUrlOutline.visible = m.serverUrlContainer.isInFocusChain()

  return isHandled
end function

' onDestroy: Full teardown releasing all resources before component removal
' Called automatically via JRScreen.beforeViewClose when sgRouter permanently closes this view.
sub onDestroy()
  m.log.verbose("onDestroy")

  ' Unobserve m.top observer
  m.top.unobserveField("serverUrl")

  ' Release the submit + options sidepanel observers
  if isValid(m.buttons) then m.buttons.unobserveField("buttonSelected")
  sidepanel = m.top.findNode("options")
  if isValid(sidepanel)
    sidepanel.unobserveField("closeSidePanel")
    for each opt in sidepanel.options
      opt.unobserveField("optionSelected")
    end for
  end if

  ' 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