components_ui_button_ResumeButton.bs
' bsc-disable-file print-locations (legacy print() sites; migration to m.log.* tracked by tech-debt.md#legacy-print-statements)
sub init()
m.top.observeField("playbackPositionTicks", "onTicksChanged")
m.top.observeField("runtimeTicks", "onTicksChanged")
' The progress bar is drawn only once the icon has loaded (see updateProgressBar())
m.buttonIcon = m.top.findNode("buttonIcon")
if isValid(m.buttonIcon)
m.buttonIcon.observeField("loadStatus", "onIconLoaded")
end if
end sub
' onIconLoaded: Called when button icon finishes loading
'
sub onIconLoaded()
' Only proceed if icon is fully loaded
if m.buttonIcon.loadStatus <> "ready" then return
' Ticks set before the icon loaded were held back; draw them now
updateProgressBar()
' Clean up observer to prevent memory leaks. This is a genuine ONE-SHOT: the icon
' URI is set once when the button is built and never reassigned, and a ResumeButton is created and
' removed per use (ItemDetails.manageResumeButton) rather than recycled as a list
' cell, so `loadStatus` reaches "ready" exactly once for this node and there is
' nothing further to hear. Those two facts are what make detaching here safe, and
' neither is visible to the gate, which is why this is suppressed rather than
' carved out of the rule.
' bsc-disable-next-line register-once-observer
m.buttonIcon.unobserveField("loadStatus")
end sub
' onTicksChanged: Updates progress bar when playback position or runtime changes
'
sub onTicksChanged()
updateProgressBar()
end sub
' updateProgressBar: Creates/updates the progress bar based on current tick values
'
sub updateProgressBar()
' Need valid runtime to calculate percentage
if m.top.runtimeTicks = 0 then return
' The bar appears with the icon, not ahead of it. Asked of the icon itself: the
' backdrop is sized from init() (IconButton), so its width no longer says whether the
' icon has loaded. onIconLoaded() draws the bar once it has.
if not isValid(m.buttonIcon) or m.buttonIcon.loadStatus <> "ready" then return
buttonBg = m.top.findNode("buttonBackground")
if not isValid(buttonBg) then return
' Calculate percentage (0-100) - force float division to avoid integer truncation
' Multiply by 100.0 (float literal) first to maintain precision
percentage = (m.top.playbackPositionTicks * 100.0) / m.top.runtimeTicks
' Clamp percentage to valid range and warn if out of bounds
if percentage < 0 or percentage > 100
? "[ResumeButton] Warning: Progress percentage out of range (" ; percentage ; "%), clamping to 0-100"
if percentage < 0
percentage = 0
else if percentage > 100
percentage = 100
end if
end if
' Find or create progress bar
progressBar = m.top.findNode("resumeProgressBar")
if not isValid(progressBar)
progressBar = createProgressBar()
end if
' Update progress bar size and percentage
' Width: reduce by 12px to account for 6px border on left and right
barMargin = 3
progressBar.width = buttonBg.width - (12 + barMargin * 2)
progressBar.playedPercentage = percentage
' Position at bottom of button, inside the 6px focus border
' X: 6px (textExtension offset) + 6px (left border) = 12px from left
' Y: buttonBg.height - 9px (progress height) - 6px (bottom border) = 15px from bottom
progressBar.translation = [12 + barMargin, buttonBg.height - (15 + barMargin)]
end sub
' createProgressBar: Creates and appends progress bar to button
'
function createProgressBar() as object
progressBar = CreateObject("roSGNode", "VideoProgressBar")
progressBar.id = "resumeProgressBar"
m.top.appendChild(progressBar)
return progressBar
end function