import "pkg:/source/roku_modules/log/LogMixin.brs"
import "pkg:/source/utils/backdrop.bs"
import "pkg:/source/utils/misc.bs"
sub init()
m.log = new log.Logger("JRScene")
m.top.backgroundColor = m.global.constants.colorBackgroundPrimary
m.top.backgroundURI = ""
m.loadingText = m.top.findNode("loadingText")
m.spinner = m.top.findNode("spinner")
m.imageFader = m.top.findNode("imageFader")
m.toast = m.top.findNode("toast")
m.lastBackdropUri = "" ' Track last URI to skip redundant imageFader assignments
' Hide backdrop until setting is resolved on first backdrop request (lazy initialization)
m.imageFader.visible = false
' set text manually to AVOID translation
defaultFont = m.top.findNode("defaultFont")
fallbackFont = m.top.findNode("fallbackFont")
defaultFont.text = "Ag"
fallbackFont.text = "Ag"
' Test toast trigger — set from BrightScript console for quick visual verification
m.top.observeField("testToast", "onTestToast")
' Debug-only: cheat code state for toast testing — compiled out in production
#if debug
m.debugCodeSequence = ["up", "up", "down", "down"]
m.debugCodeProgress = 0
m.debugLastKeyTime = 0
m.debugToastIndex = 0
#end if
end sub
' DEBUG: Triggered when testToast field is set from the BrightScript console.
' Format: "type|message" where type is "error", "success", "warning", or "info".
' Falls back to "error" type if no pipe delimiter is found.
'
' Usage from BrightScript console (port 8085):
' m.top.getScene().testToast = "error|Something went wrong"
' m.top.getScene().testToast = "success|Item saved"
' m.top.getScene().testToast = "info|Loading filters..."
' m.top.getScene().testToast = "Just a message"
sub onTestToast()
value = m.top.testToast
if value = "" then return
parts = value.split("|")
if parts.count() >= 2
toastType = parts[0]
message = value.mid(toastType.len() + 1)
else
toastType = "error"
message = value
end if
showToast(message, toastType)
end sub
sub onLoadingTextChanged()
m.loadingText.text = m.top.loadingText
end sub
sub onBackgroundImageUriChanged()
if not isValid(m.imageFader) then return
' Resolve backdrop setting on first use (lazy initialization)
if m.top.shouldShowBackdrop = invalid
localUser = m.global.user
if isValid(localUser)
m.top.shouldShowBackdrop = resolveShowBackdrop(localUser.settings, localUser.config)
else
m.top.shouldShowBackdrop = false ' Don't show backdrops if called before login
end if
m.imageFader.visible = m.top.shouldShowBackdrop
end if
' Only update URI if backdrops are enabled (performance optimization)
if m.top.shouldShowBackdrop
m.imageFader.isAnimated = true
m.imageFader.uri = m.top.backgroundImageUri
end if
end sub
' Set the background image with animation control
' @param {string} uri - The image URI to display
' @param {boolean} isAnimated - Whether to animate the transition
' @param {boolean} forceBackdrop - Force show backdrop regardless of user setting (used for login splashscreen)
sub setBackgroundImage(uri as string, isAnimated = true as boolean, forceBackdrop = false as boolean)
if not isValid(m.imageFader) then return
' Force backdrop mode bypasses user settings (used for login splashscreen)
if forceBackdrop
' For empty URI in force mode, clear URI and reset state for next screen
if uri = invalid or uri = ""
m.imageFader.uri = ""
' Reset shouldShowBackdrop to invalid so next screen can properly initialize
m.top.shouldShowBackdrop = invalid
' Explicitly hide imageFader to ensure a clean state if next screen does not reinitialize backdrop visibility
m.imageFader.visible = false
else
m.imageFader.isAnimated = isAnimated
m.imageFader.uri = uri
m.imageFader.visible = true
end if
return
end if
' Resolve backdrop setting on first use (lazy initialization)
if m.top.shouldShowBackdrop = invalid
localUser = m.global.user
if isValid(localUser)
m.top.shouldShowBackdrop = resolveShowBackdrop(localUser.settings, localUser.config)
else
m.top.shouldShowBackdrop = false ' Don't show backdrops if called before login
end if
m.imageFader.visible = m.top.shouldShowBackdrop
end if
' Only update URI if backdrops are enabled (performance optimization)
if m.top.shouldShowBackdrop
' Skip if URI has not changed - BackdropFader would deduplicate anyway, but skipping here
' avoids the field assignment and unnecessary observer call entirely.
if m.lastBackdropUri = uri then return
m.lastBackdropUri = uri
m.imageFader.isAnimated = isAnimated
m.imageFader.uri = uri
end if
end sub
' Re-evaluate and apply the backdrop visibility setting
' Useful when settings change at runtime (e.g., from Settings screen)
sub refreshBackdropSetting()
if not isValid(m.imageFader) then return
localUser = m.global.user
if isValid(localUser)
m.top.shouldShowBackdrop = resolveShowBackdrop(localUser.settings, localUser.config)
m.imageFader.visible = m.top.shouldShowBackdrop
' Clear backdrop URI if hiding to free memory
if not m.top.shouldShowBackdrop
m.top.backgroundImageUri = ""
end if
end if
end sub
' Triggered when the isLoading boolean component field is changed
sub onIsLoadingChanged()
' toggle visibility of active view/group
group = m.global.sceneManager.callFunc("getActiveScene")
if isValid(group)
group.visible = not m.top.isRemoteDisabled
end if
' toggle visibility of loading spinner
m.spinner.visible = m.top.isLoading
' toggle visibility of loading text
m.loadingText.visible = m.top.isLoading
end sub
' showToast: Display a transient toast notification.
' @param {string} message - The message to display
' @param {string} [toastType="error"] - "error", "success", or "info"
sub showToast(message as string, toastType = "error" as string)
if not isValid(m.toast) then return
m.toast.message = message
m.toast.toastType = toastType
m.toast.shouldShow = true
end sub
function onKeyEvent(key as string, press as boolean) as boolean
' Debug-only: up-up-down-down cheat code on key UP events to cycle test toasts.
' Key UP (press=false) always bubbles to JRScene because all child components
' return false for press=false. This guarantees the sequence is tracked regardless
' of which screen has focus. Compiled out in production (bs_const=debug=false).
' See docs/dev/debug-flags.md.
#if debug
if not press
now = CreateObject("roDateTime").asSeconds()
if now - m.debugLastKeyTime > 2
m.debugCodeProgress = 0
end if
m.debugLastKeyTime = now
if key = m.debugCodeSequence[m.debugCodeProgress]
m.debugCodeProgress++
if m.debugCodeProgress >= m.debugCodeSequence.count()
m.debugCodeProgress = 0
debugToasts = [
{ type: "error", msg: "[DEBUG] Error toast test" },
{ type: "success", msg: "[DEBUG] Success toast test" },
{ type: "warning", msg: "[DEBUG] Warning toast test" },
{ type: "info", msg: "[DEBUG] Info toast test" }
]
toast = debugToasts[m.debugToastIndex]
m.debugToastIndex = (m.debugToastIndex + 1) mod 4
showToast(toast.msg, toast.type)
return true
end if
else
m.debugCodeProgress = 0
end if
end if
#end if
if not press then return false
if m.top.isRemoteDisabled then return true
if key = "back"
m.global.sceneManager.callFunc("popScene")
return true
else if key = "options"
group = m.global.sceneManager.callFunc("getActiveScene")
if isValid(group) and isValid(group.isOptionsAvailable) and group.isOptionsAvailable
group.lastFocus = group.focusedChild
panel = group.findNode("options")
panel.visible = true
panel.findNode("panelList").setFocus(true)
end if
return true
end if
return false
end function