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")
' SearchResults owns its own navigation — a selected result navigates to its route.
m.searchSelect.observeField("itemSelected", "onSearchItemSelected")
' A non-navigable result (plays directly) sets m.top.quickPlayNode; this self-observer
' forwards it to the QueueManager launcher.
m.top.observeField("quickPlayNode", "onQuickPlayLaunch")
' Set initial focus for scene navigation
m.top.lastFocus = searchBox
' First show focuses the keyboard to type immediately; later shows (keepAlive resume, e.g.
' back from a result detail) restore the prior focus.
m.isFirstShow = true
' Last voice query applied via applyRouteQuery — guards against re-applying (and re-grabbing
' focus) on a keepAlive back-resume that replays the same route context.
m.lastAppliedQuery = ""
end sub
sub onScreenShown()
' Voice search arrives as route.context.query — pre-populate the search box and focus its
' text entry, skipping normal focus restoration.
if applyRouteQuery() then return
' 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
' On first open, focus + activate the keyboard so the user can type immediately (the router
' mounts this view). Subsequent shows (keepAlive resume) restore the prior focus.
if m.isFirstShow
m.isFirstShow = false
if isValid(m.searchAlphabox)
m.searchAlphabox.setFocus(true)
m.searchAlphabox.active = true
return
end if
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
' If the route carries a voice query in its context, pre-populate the search box (which
' triggers the search via the searchAlpha alias) and focus the text entry. Returns true when
' a query was applied so onScreenShown can skip normal focus.
'
' onScreenShown fires on first open AND on keepAlive resume, and the router replays the
' ORIGINAL route (with its query context) on a back-resume (Router.brs reuses view.route) — so
' applying unconditionally would re-grab focus to the text box every time the user backs out of
' a result. Guard on the last-applied value: a back-resume carries the same query we already
' applied (skip), while a genuinely new voice search carries a different query (apply). The box
' already holds the prior term in the keepAlive case, so skipping a repeat is also the right UX.
function applyRouteQuery() as boolean
route = m.top.route
if not isValid(route) or not isValid(route.context) then return false
query = route.context.query
if not isValidAndNotEmpty(query) then return false
if query = m.lastAppliedQuery then return false
m.lastAppliedQuery = query
m.searchAlphabox.text = query
m.searchAlphabox.textEditBox.setFocus(true)
return true
end function
' A search result was selected — navigate to its route (rich node as context). Non-navigable
' results fall through to playback.
sub onSearchItemSelected(msg)
item = getMsgPicker(msg)
if not isValid(item) then return
route = routeForItem(item)
if isValid(route)
sgrouter.navigateTo(route, { context: { item: item } })
else
m.top.quickPlayNode = item
end if
end sub
' Forward a non-navigable result to the playback launcher.
sub onQuickPlayLaunch(msg)
node = msg.getData()
if isValid(node) then m.global.queueManager.callFunc("launchItem", node)
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 via JRScreen.beforeViewClose when sgRouter permanently closes this view.
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")
m.searchSelect.unobserveField("itemSelected") ' self-navigation observer
m.top.unobserveField("quickPlayNode") ' playback launcher
' 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