components_JRScene.bs

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