components_photos_PhotoDetails.bs
import "pkg:/source/api/image.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"
' How many photos in a row may fail before we stop skipping and make the user acknowledge it.
' Bounds the wholesale-failure case (e.g. no server URL): random mode has no end-of-list stop,
' so an unbounded skip would cycle silently forever.
const MAX_CONSECUTIVE_PHOTO_FAILURES = 3
sub init()
m.log = new log.Logger("PhotoDetails")
m.consecutivePhotoFailures = 0
m.top.isOptionsAvailable = true
m.top.isOverhangVisible = false
m.slideshowTimer = m.top.findNode("slideshowTimer")
m.slideshowTimer.observeField("fire", "nextSlide")
m.status = m.top.findNode("status")
m.textBackground = m.top.findNode("background")
m.statusTimer = m.top.findNode("statusTimer")
m.statusTimer.observeField("fire", "statusUpdate")
userSettings = m.global.user.settings
m.slideshow = userSettings.photosSlideshow = true
m.random = userSettings.photosRandom = true
m.showStatusAnimation = m.top.findNode("showStatusAnimation")
m.hideStatusAnimation = m.top.findNode("hideStatusAnimation")
onItemContentChanged()
end sub
' The router creates this view; its launch data arrives as route.context (set by
' JRScene.onPhotoLaunchRequested). Apply it on first mount, setting
' itemIndex LAST so onItemContentChanged fires with itemsNode/itemsArray + slideshow flags
' already in place. itemIndex defaults to -1, so this runs once (subsequent shows are no-ops).
sub onScreenShown()
if m.top.itemIndex = -1 and isValid(m.top.route) and isValid(m.top.route.context)
ctx = m.top.route.context
if isValid(ctx.itemsNode) then m.top.itemsNode = ctx.itemsNode
if isValid(ctx.itemsArray) then m.top.itemsArray = ctx.itemsArray
if isValid(ctx.isSlideshow) then m.top.isSlideshow = ctx.isSlideshow
if isValid(ctx.isRandom) then m.top.isRandom = ctx.isRandom
startIndex = 0
if isValid(ctx.itemIndex) then startIndex = ctx.itemIndex
m.top.itemIndex = startIndex
end if
' Mirror JRScreen.onScreenShown's focus rule (this override replaces the base).
if isValid(m.top.lastFocus)
m.top.lastFocus.setFocus(true)
else
m.top.setFocus(true)
end if
end sub
sub onItemContentChanged()
if not isValidToContinue(m.top.itemIndex) then return
item = invalid
if isValid(m.top.itemsNode)
' itemsNode IS the ContentNode containing photo children
if m.top.itemsNode.getChildCount() > 0
item = m.top.itemsNode.getChild(m.top.itemIndex)
else if isValidAndNotEmpty(m.top.itemsNode.id)
' Single photo node passed directly
item = m.top.itemsNode
end if
else if isValid(m.top.itemsArray)
item = m.top.itemsArray[m.top.itemIndex]
else
return
end if
' ImageURL does no I/O, so this resolves inline rather than in a Task per slideshow tick.
' The Id guard is not redundant: ImageURL feeds it to Substitute(), whose args are declared
' String, and an itemsArray entry is a plain AA that can be missing the key. On the task
' thread that throw was contained; on the render thread it would take the app down.
' item itself can still be invalid when itemsNode has neither children nor an id.
uri = ""
if isValid(item) and isValidAndNotEmpty(item.Id)
uri = ImageURL(item.Id, "Primary", { maxHeight: 1080, maxWidth: 1920 })
end if
showPhoto(uri)
end sub
' Render the resolved photo, or handle an unresolvable one. Nothing else draws on this screen
' — no title, no overhang (see PhotoDetails.xml) — so a failed photo shows the user nothing
' new; the previous frame stays up, or black on the first one. During a slideshow we therefore
' toast and advance rather than stalling on a frame they never chose, and after
' MAX_CONSECUTIVE_PHOTO_FAILURES we stop advancing so a wholesale failure can't cycle silently.
'
' A toast rather than a blocking dialog, and not only for interruption reasons: a dialog needs
' text metrics, and roFontRegistry is a MAIN|TASK-only component Roku refuses to create on the
' render thread. Toast goes through the scene's showToast, which is just field writes.
sub showPhoto(uri as string)
stopLoadingSpinner()
if isValidAndNotEmpty(uri)
m.consecutivePhotoFailures = 0
photo = m.top.findNode("photo")
photo.uri = uri
if m.slideshow = true or m.random = true
' user has requested either a slideshow or random...
m.slideshowTimer.control = "start"
end if
return
end if
m.consecutivePhotoFailures++
m.log.warn("photo uri unresolvable", m.top.itemIndex, m.consecutivePhotoFailures)
displayToast(translate(translationKeys.ErrorImageTypeNotSupported))
' Keep advancing while failures look incidental; once they don't, stop the slideshow rather
' than cycling a screen the user can't see anything on. Manual left/right still works.
if (m.slideshow = true or m.random = true) and m.consecutivePhotoFailures < MAX_CONSECUTIVE_PHOTO_FAILURES
m.slideshowTimer.control = "start"
else
m.slideshowTimer.control = "stop"
end if
end sub
sub nextSlide()
m.slideshowTimer.control = "stop"
if m.slideshow = true
if isValidToContinue(m.top.itemIndex + 1)
m.top.itemIndex++
m.slideshowTimer.control = "start"
end if
else if m.random = true
index = invalid
if isValid(m.top.itemsNode)
' itemsNode IS the ContentNode containing photo children
if m.top.itemsNode.getChildCount() > 0
index = rnd(m.top.itemsNode.getChildCount() - 1)
else
' we're dealing with a single photo
return
end if
else if isValid(m.top.itemsArray)
if m.top.itemsArray.count() > 0
index = rnd(m.top.itemsArray.count() - 1)
end if
end if
if isValid(index) and isValidToContinue(index)
m.top.itemIndex = index
m.slideshowTimer.control = "start"
end if
end if
end sub
sub statusUpdate()
m.statusTimer.control = "stop"
m.hideStatusAnimation.control = "start"
end sub
' JRScreen hook.
' Used to ensure tasks are stopped
sub onScreenHidden()
m.slideshowTimer.control = "stop"
m.statusTimer.control = "stop"
end sub
' isSlideshow component field has changed
sub onIsSlideshowChanged()
m.slideshow = m.top.isSlideshow
end sub
' isRandom component field has changed
sub onIsRandomChanged()
m.random = m.top.isRandom
end sub
function onKeyEvent(key as string, press as boolean) as boolean
if not press then return false
if key = "right"
if isValidToContinue(m.top.itemIndex + 1)
m.slideshowTimer.control = "stop"
m.top.itemIndex++
end if
return true
end if
if key = "left"
if isValidToContinue(m.top.itemIndex - 1)
m.slideshowTimer.control = "stop"
m.top.itemIndex--
end if
return true
end if
if key = "play"
if m.slideshowTimer.control = "start"
' stop the slideshow if the user hits "pause"
m.slideshowTimer.control = "stop"
m.status.text = translate(translationKeys.LabelSlideshowPaused)
if m.textBackground.opacity = 0
m.showStatusAnimation.control = "start"
end if
m.statusTimer.control = "start"
else
' start the slideshow if the user hits "play"
m.status.text = translate(translationKeys.LabelSlideshowResumed)
if m.textBackground.opacity = 0
m.showStatusAnimation.control = "start"
end if
m.slideshow = true
m.statusTimer.control = "start"
m.slideshowTimer.control = "start"
end if
return true
end if
if key = "options"
' Options (random etc) is done on itemGrid
return true
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")
' Unobserve timer observers
m.slideshowTimer.unobserveField("fire")
m.statusTimer.unobserveField("fire")
' Stop timers (may already be stopped by onScreenHidden or user interaction)
m.slideshowTimer.control = "stop"
m.statusTimer.control = "stop"
' Clear node references
m.slideshowTimer = invalid
m.status = invalid
m.textBackground = invalid
m.statusTimer = invalid
m.showStatusAnimation = invalid
m.hideStatusAnimation = invalid
end sub
function isValidToContinue(index as integer)
if isValid(m.top.itemsNode)
' itemsNode IS the ContentNode containing photo children
childCount = m.top.itemsNode.getChildCount()
if childCount > 0
if index >= 0 and index < childCount
return true
end if
else if isValidAndNotEmpty(m.top.itemsNode.id) and index = 0
' Single photo node passed directly
return true
end if
else if isValidAndNotEmpty(m.top.itemsArray)
if index >= 0 and index < m.top.itemsArray.count()
return true
end if
end if
return false
end function