Skip to content

Commit 365c127

Browse files
chore(docs): update code docs
1 parent 2096250 commit 365c127

12 files changed

Lines changed: 381 additions & 11 deletions

docs/components_extras_ExtrasRowList.bs.html

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@
110110
' Track the last focused row index so horizontal item scrolling within a row
111111
' does not trigger an unnecessary panel position recalculation.
112112
m.lastFocusedRowIndex = -1
113+
114+
' Refetch the Channel Programs row (TvChannel "Up Next" / Program "More on
115+
' this Channel") when the base class's progress tick detects an expired
116+
' broadcast. m.isRefetchingChannelPrograms debounces repeated expiry signals
117+
' while a refetch is already in flight.
118+
m.isRefetchingChannelPrograms = false
119+
m.top.observeField("programsExpired", "onProgramsExpired")
113120
end sub
114121

115122
sub updateSize()
@@ -921,6 +928,72 @@
921928
m.LikeThisTask.control = "RUN"
922929
end sub
923930

931+
' Fires when JRRowList's progress tick detects at least one expired Program
932+
' among this list's rows. Triggers a targeted refetch of the Channel Programs
933+
' row (TvChannel "Up Next" or Program "More on this Channel") so finished
934+
' broadcasts roll off and new ones take their place.
935+
'
936+
' Deliberately does NOT re-chain to LikeThisTask — related content is stable
937+
' over a viewing session and should not be refetched just because a program
938+
' expired. m.isRefetchingChannelPrograms debounces repeated expiry signals.
939+
sub onProgramsExpired()
940+
if m.isRefetchingChannelPrograms then return
941+
if not isValid(m.rowChannelPrograms) then return
942+
943+
itemType = m.top.type
944+
handlerName = ""
945+
channelId = invalid
946+
947+
if itemType = "TvChannel"
948+
handlerName = "onChannelProgramsRefetched"
949+
channelId = m.top.parentId
950+
else if itemType = "Program"
951+
handlerName = "onProgramChannelProgramsRefetched"
952+
channelId = m.programChannelId
953+
end if
954+
955+
if handlerName = "" or not isValidAndNotEmpty(channelId) then return
956+
957+
m.isRefetchingChannelPrograms = true
958+
m.LoadChannelProgramsTask.control = "STOP"
959+
m.LoadChannelProgramsTask.unobserveField("content")
960+
m.LoadChannelProgramsTask.itemId = channelId
961+
m.LoadChannelProgramsTask.observeField("content", handlerName)
962+
m.LoadChannelProgramsTask.control = "RUN"
963+
end sub
964+
965+
' Refetch handler for TvChannel "Up Next". Replaces the row content in place
966+
' without re-chaining to LikeThisTask (unlike the initial-load handler).
967+
sub onChannelProgramsRefetched()
968+
items = m.LoadChannelProgramsTask.content
969+
m.LoadChannelProgramsTask.unobserveField("content")
970+
m.LoadChannelProgramsTask.content = []
971+
m.isRefetchingChannelPrograms = false
972+
973+
if isValid(items) and items.count() > 0
974+
m.rowChannelPrograms = populateRow(m.rowChannelPrograms, translate(translationKeys.LabelUpNext), items, rowSlotSize.ROW_HEIGHT_SQUARE)
975+
else if isValid(m.rowChannelPrograms)
976+
removeRow(m.rowChannelPrograms)
977+
m.rowChannelPrograms = invalid
978+
end if
979+
end sub
980+
981+
' Refetch handler for Program "More on this Channel". Same in-place-update
982+
' pattern as onChannelProgramsRefetched — no chain to LikeThisTask.
983+
sub onProgramChannelProgramsRefetched()
984+
items = m.LoadChannelProgramsTask.content
985+
m.LoadChannelProgramsTask.unobserveField("content")
986+
m.LoadChannelProgramsTask.content = []
987+
m.isRefetchingChannelPrograms = false
988+
989+
if isValid(items) and items.count() > 0
990+
m.rowChannelPrograms = populateRow(m.rowChannelPrograms, translate(translationKeys.LabelMoreOnThisChannel), items, rowSlotSize.ROW_HEIGHT_SQUARE)
991+
else if isValid(m.rowChannelPrograms)
992+
removeRow(m.rowChannelPrograms)
993+
m.rowChannelPrograms = invalid
994+
end if
995+
end sub
996+
924997
' populateRow: Reuse an existing row ContentNode (clearing its children) or create a new one.
925998
' Creating appends the row to m.top.content at the current end — correct since the async chain
926999
' fires in display order. m.rowHeights is reset at chain start and rebuilt here on every call,

docs/components_home_HomeRows.bs.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@
4747
m.isLoadingResume = false
4848
m.isLoadingNextUp = false
4949
m.isLoadingOnNow = false
50+
51+
' Refetch the On Now row when the base class's progress tick detects that at
52+
' least one Program has ended. Without this, sitting on Home long enough would
53+
' leave a row of finished broadcasts until the user refreshes manually.
54+
m.top.observeField("programsExpired", "onProgramsExpired")
5055
end sub
5156

5257
' loadLibraries: Entry point called by Home.bs via callFunc.
@@ -530,6 +535,30 @@
530535
populateRowFromData("livetv", itemData)
531536
end sub
532537

538+
' Fires when JRRowList's progress tick detects at least one expired Program.
539+
' Re-runs LoadOnNowTask to pull fresh currently-airing data. The m.isLoadingOnNow
540+
' guard debounces repeated expiry signals while a load is already in flight, and
541+
' the "livetv" section plan check avoids wasted requests when the user has
542+
' disabled the On Now section.
543+
sub onProgramsExpired()
544+
if m.isLoadingOnNow then return
545+
if not isValidAndNotEmpty(m.sectionPlan) then return
546+
547+
hasLiveTv = false
548+
for each section in m.sectionPlan
549+
if section.type = "livetv"
550+
hasLiveTv = true
551+
exit for
552+
end if
553+
end for
554+
if not hasLiveTv then return
555+
556+
m.isLoadingOnNow = true
557+
m.LoadOnNowTask.unobserveField("content")
558+
m.LoadOnNowTask.observeField("content", "updateOnNowItems")
559+
m.LoadOnNowTask.control = "RUN"
560+
end sub
561+
533562
' updateLatestItems: Processes LoadItemsTask content for a latest-in-library row.
534563
' Uses msg parameter because each library has its own dynamically created task.
535564
'

docs/components_ui_rowitem_JRRowItem.bs.html

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,29 @@
3939
' this to colorBackgroundPrimary so the white silhouette icon picks up the theme color.
4040
m.defaultPosterBlendColor = m.poster.blendColor
4141

42+
' Reactive bridge for live Program progress bars. When this cell is bound to
43+
' a Program content node, an observer is installed on that node's
44+
' playedPercentage field so JRRowList's 60s tick can refresh the visible
45+
' progress bar without reassigning itemContent (which would re-render the cell).
46+
' See applyPlayedPercentage().
47+
m.observedProgramNode = invalid
48+
4249
m.poster.observeField("loadStatus", "onPosterLoadStatusChanged")
4350
m.itemIcon.observeField("loadStatus", "onIconLoadStatusChanged")
4451
m.top.observeField("renderTracking", "onRenderTrackingChanged")
4552
end sub
4653

4754
sub onItemContentChanged()
48-
if not isValid(m.top.itemContent) then return
55+
' Tear down any Program progress observer from a prior bind even when the
56+
' new itemContent is invalid. renderItem()'s top-of-function teardown only
57+
' covers transitions through a valid bind; transitions to invalid never
58+
' reach renderItem() and would otherwise leak the observer onto a stale
59+
' content node — bounded by the next valid rebind, or unbounded if the cell
60+
' is destroyed (Roku exposes no destroy lifecycle hook to clean up later).
61+
if not isValid(m.top.itemContent)
62+
unobserveProgramProgress()
63+
return
64+
end if
4965
if m.top.width <= 0 or m.top.height <= 0 then return
5066
setupTextureObserver()
5167
renderItem()
@@ -61,6 +77,13 @@
6177
item = m.top.itemContent
6278
if not isValid(item) then return
6379

80+
' Tear down any Program progress observer from the previous render before
81+
' branching into tile/standard/loading modes. Must run unconditionally here
82+
' (not inside applyPlayedPercentage) because the library-tile and loading
83+
' branches never reach applyPlayedPercentage — leaving a stale observer
84+
' attached would land tick writes on the wrong recycled poster.
85+
unobserveProgramProgress()
86+
6487
m.poster.callFunc("resetBadge")
6588
m.isTextureUnloaded = false
6689

@@ -220,7 +243,59 @@
220243
m.poster.unplayedCount = item.unplayedItemCount
221244
end if
222245

223-
m.poster.playedPercentage = item.playedPercentage
246+
applyPlayedPercentage(item)
247+
end sub
248+
249+
' Sets the poster's progress bar percentage based on item type.
250+
' Programs use live broadcast elapsed time (wall-clock derived); everything
251+
' else uses UserData.PlayedPercentage as flattened by the data transformer.
252+
'
253+
' For Programs, also installs a reactive bridge observer on the content node's
254+
' playedPercentage field. JRRowList's 60s tick rewrites that field, and this
255+
' observer forwards the change to m.poster.playedPercentage — updating any
256+
' currently-mounted VideoProgressBar without a cell re-render.
257+
'
258+
' Observer teardown is split across two sites to cover every itemContent
259+
' transition this cell can see:
260+
' • renderItem() top — handles transitions into a new valid bind (recycle
261+
' into a different content node) and re-renders triggered by size changes
262+
' on the same item. Required so applyPlayedPercentage doesn't double-
263+
' install observers on size-change re-renders.
264+
' • onItemContentChanged() invalid path — handles transitions to invalid,
265+
' which never reach renderItem() and would otherwise leak the observer.
266+
' This function therefore only needs to install — not clear.
267+
sub applyPlayedPercentage(item as object)
268+
if item.type = "Program"
269+
nowSeconds = CreateObject("roDateTime").AsSeconds()
270+
m.poster.playedPercentage = computeProgramBroadcastProgress(item.PlayStart, item.PlayDuration, nowSeconds)
271+
item.observeField("playedPercentage", "onProgramProgressChanged")
272+
m.observedProgramNode = item
273+
else
274+
m.poster.playedPercentage = item.playedPercentage
275+
end if
276+
end sub
277+
278+
' Tears down the bridge observer on the previously-bound Program content node.
279+
' Called from two sites to cover every itemContent transition this cell sees:
280+
' • Top of renderItem() — runs before tile/standard/loading branching so
281+
' recycled cells never leak observers onto stale content nodes regardless
282+
' of which render branch the new bind takes. Also covers size-change re-
283+
' renders on the same item (without it, applyPlayedPercentage would stack
284+
' duplicate observers on every size change).
285+
' • Invalid path of onItemContentChanged() — covers transitions to invalid,
286+
' which never reach renderItem(). Without this site, an observer installed
287+
' on a Program node would leak when the cell unbinds.
288+
sub unobserveProgramProgress()
289+
if isValid(m.observedProgramNode)
290+
m.observedProgramNode.unobserveField("playedPercentage")
291+
m.observedProgramNode = invalid
292+
end if
293+
end sub
294+
295+
' Forwarded write from the bound Program content node to the live poster.
296+
' Fires when JRRowList's tick rewrites the node's playedPercentage.
297+
sub onProgramProgressChanged(msg as object)
298+
m.poster.playedPercentage = msg.getData()
224299
end sub
225300

226301
' Updates positions and sizes of all child nodes to match the current slot dimensions.

0 commit comments

Comments
 (0)