components_search_SearchResults.bs

' bsc-disable-file print-locations — legacy print() sites; migration to m.log.* tracked by tech-debt.md#legacy-print-statements
import "pkg:/source/api/ApiClient.bs"
import "pkg:/source/api/apiPool.bs"
import "pkg:/source/api/baseRequest.bs"
import "pkg:/source/api/image.bs"
import "pkg:/source/api/items.bs"
import "pkg:/source/constants/imageSize.bs"
import "pkg:/source/roku_modules/log/LogMixin.brs"
import "pkg:/source/translationKeys.bs"
import "pkg:/source/utils/config.bs"
import "pkg:/source/utils/deviceCapabilities.bs"
import "pkg:/source/utils/itemImageUrl.bs"
import "pkg:/source/utils/misc.bs"
import "pkg:/source/utils/textureManager.bs"
import "pkg:/source/utils/translate.bs"

sub init()
  m.log = new log.Logger("SearchResults")
  ' Clear backdrop immediately when search screen opens
  m.global.sceneManager.callFunc("setBackgroundImage", "")

  m.top.isOptionsAvailable = false
  m.searchSelect = m.top.findnode("searchSelect")
  m.searchTask = CreateObject("roSGNode", "SearchTask")

  m.searchHelpText = m.top.findNode("SearchHelpText")
  m.searchHelpText.text = translate(translationKeys.MessageYouCanSearchForTitlesPeople)

  ' Cache search keyboard reference and observe focus changes
  ' to adjust alphabet layout when voice button popup appears
  searchBox = m.top.findNode("SearchBox")
  m.searchAlphabox = searchBox.findNode("searchKey")
  m.searchAlphabox.observeField("focusedChild", "onKeyboardFocusChange")

  ' Observe row item focus for backdrop updates
  m.searchSelect.observeField("rowItemFocused", "onSearchItemFocused")

  ' Set initial focus for scene navigation
  m.top.lastFocus = searchBox

end sub

sub onScreenShown()
  if m.top.shouldSkipInitialFocus
    m.top.shouldSkipInitialFocus = false
    ' Caller will set focus after pushScene — skip here to avoid a double-focus
    ' race on the DynamicMiniKeyboard that prevents the voice prompt overlay.
    return
  end if

  ' Restore texture management — reactivate and restore buffer range so cells reload.
  if isValid(m.searchSelect) and isValid(m.searchSelect.content)
    updateTextureBufferRange(m.searchSelect.content, m.searchSelect.rowItemFocused[0], m.searchSelect.rowItemFocused[1], m.searchSelect.numRows)
    activateTextureManager(m.searchSelect.content)
  end if

  ' Restore focus for scene navigation
  if isValid(m.top.lastFocus)
    m.top.lastFocus.setFocus(true)
  else
    m.top.setFocus(true)
  end if
end sub

sub onScreenHidden()
  m.log.info("onScreenHidden")
  if isValid(m.searchSelect) and isValid(m.searchSelect.content)
    hideTextureManager(m.searchSelect.content)
  end if
end sub

' onKeyboardFocusChange: Fires when focus changes within the keyboard subtree (including entry/exit).
' Clears stale backdrop when keyboard gains focus, and adjusts alphabet layout when the
' voice button popup appears (textEditBox focused moves the textbox up to avoid overlap).
sub onKeyboardFocusChange()
  if not isValid(m.searchAlphabox) then return

  ' Clear stale backdrop whenever keyboard gains focus
  if m.searchAlphabox.isInFocusChain()
    m.global.sceneManager.callFunc("setBackgroundImage", "")
  end if

  ' Check if the textEditBox has focus (voice button popup is visible)
  if m.searchAlphabox.textEditBox.hasFocus()
    ' Move textbox up so voice button popup doesn't cover alphabet rows below
    m.searchAlphabox.textEditBox.translation = "[0, -150]"
  else
    ' Reset textbox to original position
    m.searchAlphabox.textEditBox.translation = "[0, 0]"
  end if
end sub

' onSearchItemFocused: Update backdrop when search result is focused
sub onSearchItemFocused()
  if not isValid(m.searchSelect.rowItemFocused) or m.searchSelect.rowItemFocused[0] = -1 or m.searchSelect.rowItemFocused[1] = -1
    return
  end if

  updateTextureBufferRange(m.searchSelect.content, m.searchSelect.rowItemFocused[0], m.searchSelect.rowItemFocused[1], m.searchSelect.numRows)

  ' Get focused item from search results
  rowContent = m.searchSelect.content.getChild(m.searchSelect.rowItemFocused[0])
  if isValid(rowContent)
    focusedItem = rowContent.getChild(m.searchSelect.rowItemFocused[1])

    if isValid(focusedItem) and isValidAndNotEmpty(focusedItem.id)
      ' Pass device resolution so the URL matches other screens showing the same item,
      ' allowing BackdropFader to deduplicate and avoid unnecessary reloads/flicker.
      deviceRes = m.global.device.uiResolution
      backdropUrl = getItemBackdropUrl(focusedItem, { width: deviceRes[0], height: deviceRes[1] })
      m.global.sceneManager.callFunc("setBackgroundImage", backdropUrl)
    else
      ' Item has no backdrop - set transparent
      m.global.sceneManager.callFunc("setBackgroundImage", "")
    end if
  end if
end sub

sub searchMedias()
  query = m.top.searchAlpha
  'if user deletes the search string hide the spinner
  if query.len() = 0
    stopLoadingSpinner()
  end if
  'if search task is running and user selectes another letter stop the search and load the next letter
  m.searchTask.control = "stop"
  if isValid(query) and query <> ""
    m.searchHelpText.visible = false
    startLoadingSpinner(false)
  end if
  m.searchTask.observeField("results", "loadResults")
  m.searchTask.query = query
  m.top.overhangTitle = translate(translationKeys.ButtonSearch) + ": " + query
  m.searchTask.control = "RUN"

end sub

sub loadResults()
  m.searchTask.unobserveField("results")

  stopLoadingSpinner()
  m.searchSelect.itemdata = m.searchTask.results
  m.searchSelect.query = m.top.SearchAlpha

  if m.searchTask.results.TotalRecordCount = 0
    ' make sure focus is on the keyboard
    if m.searchSelect.isinFocusChain()
      m.searchAlphabox.setFocus(true)
    end if
    return
  end if
end sub

function onKeyEvent(key as string, press as boolean) as boolean
  if not press then return false

  if key = "left" and m.searchSelect.isinFocusChain()
    m.searchAlphabox.setFocus(true)
    return true
  else if key = "right" and isValid(m.searchSelect.content) and m.searchSelect.content.getChildCount() > 0
    m.searchSelect.setFocus(true)
    return true
  else if key = "play" and m.searchSelect.isinFocusChain() and m.searchSelect.rowItemFocused.count() > 0
    print "play was pressed from search results"
    if isValid(m.searchSelect.rowItemFocused)
      selectedContent = m.searchSelect.content.getChild(m.searchSelect.rowItemFocused[0])
      if isValid(selectedContent)
        selectedItem = selectedContent.getChild(m.searchSelect.rowItemFocused[1])
        if isValid(selectedItem)
          m.top.quickPlayNode = selectedItem
          m.top.quickPlayNode = invalid
          return true
        end if
      end if
    end if
  end if

  return false
end function

' onDestroy: Full teardown releasing all resources before component removal
' Called automatically by SceneManager.popScene() / clearScenes()
sub onDestroy()
  m.log.verbose("onDestroy")
  destroyTextureManager(m.searchSelect.content)

  ' Fully release the voice route before teardown — the DynamicMiniKeyboard's
  ' textEditBox claims the firmware's global voice route via voiceEnabled + active.
  ' Both must be cleared: active=false stops input capture, voiceEnabled=false
  ' releases the "only one voiceEnabled at a time" slot so the returning screen's
  ' VoiceTextEditBox can reclaim it.
  if isValid(m.searchAlphabox) and isValid(m.searchAlphabox.textEditBox)
    m.searchAlphabox.textEditBox.voiceEnabled = false
    m.searchAlphabox.textEditBox.active = false
  end if

  ' Unobserve child node observers
  if isValid(m.searchAlphabox) then m.searchAlphabox.unobserveField("focusedChild")
  m.searchSelect.unobserveField("rowItemFocused")

  ' Stop and release task node (observer may already be cleared by loadResults())
  m.searchTask.unobserveField("results")
  m.searchTask.control = "STOP"
  m.searchTask = invalid

  ' Clear node references
  m.searchSelect = invalid
  m.searchAlphabox = invalid
  m.searchHelpText = invalid
end sub