|
39 | 39 | ' this to colorBackgroundPrimary so the white silhouette icon picks up the theme color. |
40 | 40 | m.defaultPosterBlendColor = m.poster.blendColor |
41 | 41 |
|
| 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 | + |
42 | 49 | m.poster.observeField("loadStatus", "onPosterLoadStatusChanged") |
43 | 50 | m.itemIcon.observeField("loadStatus", "onIconLoadStatusChanged") |
44 | 51 | m.top.observeField("renderTracking", "onRenderTrackingChanged") |
45 | 52 | end sub |
46 | 53 |
|
47 | 54 | 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 |
49 | 65 | if m.top.width <= 0 or m.top.height <= 0 then return |
50 | 66 | setupTextureObserver() |
51 | 67 | renderItem() |
|
61 | 77 | item = m.top.itemContent |
62 | 78 | if not isValid(item) then return |
63 | 79 |
|
| 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 | + |
64 | 87 | m.poster.callFunc("resetBadge") |
65 | 88 | m.isTextureUnloaded = false |
66 | 89 |
|
|
220 | 243 | m.poster.unplayedCount = item.unplayedItemCount |
221 | 244 | end if |
222 | 245 |
|
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() |
224 | 299 | end sub |
225 | 300 |
|
226 | 301 | ' Updates positions and sizes of all child nodes to match the current slot dimensions. |
|
0 commit comments