Skip to content

Commit 8b014f9

Browse files
chore(docs): update code docs
1 parent 6cc75a0 commit 8b014f9

13 files changed

Lines changed: 298 additions & 158 deletions

docs/components_ItemDetails.bs.html

Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import "pkg:/source/utils/itemImageUrl.bs"
1212
import "pkg:/source/utils/mediaDisplayTitle.bs"
1313
import "pkg:/source/utils/misc.bs"
14+
import "pkg:/source/utils/placeholderImage.bs"
1415
import "pkg:/source/utils/streamSelection.bs"
1516
import "pkg:/source/utils/textureManager.bs"
1617
import "pkg:/source/utils/trackClusterFocus.bs"
@@ -2154,11 +2155,16 @@
21542155
end sub
21552156

21562157
' setItemLogo: Set the item logo image URL if available.
2157-
' Fallback chains by type:
2158-
' - Movie/Series: Logo → primaryImageTag (poster, compact size)
2159-
' - Episode/Season/Recording: primaryImageTag (item thumb/poster) → parentLogoImageTag (series logo) → seriesPrimaryImageTag → hide
2160-
' - Video/MusicVideo: primaryImageTag (poster)
2161-
' - Person: primaryImageTag (portrait photo, tall) → silhouette icon
2158+
' Fallback chains by type — every chain ends in a typed placeholder via
2159+
' getPlaceholderImagePath() so the logo slot never goes blank, matching the
2160+
' pattern Person and MusicArtist always used:
2161+
' - Movie/Series/BoxSet: Logo → primaryImageTag (poster, compact size) → placeholder
2162+
' - Episode/Season/Recording: primaryImageTag → parentLogoImageTag (series logo) → seriesPrimaryImageTag → placeholder
2163+
' - Video/MusicVideo: primaryImageTag → placeholder
2164+
' - MusicAlbum / Playlist / Photo / PhotoAlbum / TvChannel: primaryImageTag → placeholder
2165+
' - Program: primaryImageTag → channel logo → placeholder
2166+
' - Audio: primaryImageTag → album art → placeholder
2167+
' - Person / MusicArtist: primaryImageTag (portrait) → placeholder
21622168
' @param {object} item - JellyfinBaseItem node
21632169
sub setItemLogo(item as object)
21642170
if not isValid(m.itemLogo) then return
@@ -2167,12 +2173,12 @@
21672173
logoImageTag = item.logoImageTag
21682174

21692175
if item.type = "Person"
2170-
' Person: display primary (portrait) photo where the logo sits; fall back to silhouette icon
2176+
' Person: display primary (portrait) photo where the logo sits; fall back to silhouette glyph
21712177
if isValidAndNotEmpty(item.primaryImageTag)
21722178
imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag }
21732179
m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams)
21742180
else
2175-
m.itemLogo.uri = "pkg:/images/placeholders/person_$$RES$$.png"
2181+
m.itemLogo.uri = getPlaceholderImagePath(item.type)
21762182
end if
21772183
return
21782184
else if item.type = "MusicArtist"
@@ -2184,73 +2190,67 @@
21842190
imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag }
21852191
m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams)
21862192
else
2187-
m.itemLogo.uri = "pkg:/images/placeholders/artist_$$RES$$.png"
2193+
m.itemLogo.uri = getPlaceholderImagePath(item.type)
21882194
end if
21892195
return
21902196
else if item.type = "MusicAlbum"
2191-
' Square album art as the primary image; hide if none
2197+
' Square album art as the primary image; typed placeholder if none
21922198
if isValidAndNotEmpty(item.primaryImageTag)
21932199
imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag }
21942200
m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams)
21952201
else
2196-
m.itemLogo.visible = false
2197-
m.itemLogo.uri = ""
2202+
m.itemLogo.uri = getPlaceholderImagePath(item.type)
21982203
end if
21992204
return
22002205
else if item.type = "Playlist"
2201-
' Square playlist cover as primary image; hide if none
2206+
' Square playlist cover as primary image; typed placeholder if none
22022207
if isValidAndNotEmpty(item.primaryImageTag)
22032208
imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag }
22042209
m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams)
22052210
else
2206-
m.itemLogo.visible = false
2207-
m.itemLogo.uri = ""
2211+
m.itemLogo.uri = getPlaceholderImagePath(item.type)
22082212
end if
22092213
return
22102214
else if item.type = "Photo" or item.type = "PhotoAlbum"
2211-
' Photo thumbnail or album cover as primary image; hide if none
2215+
' Photo thumbnail or album cover as primary image; typed placeholder if none
22122216
if isValidAndNotEmpty(item.primaryImageTag)
22132217
imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag }
22142218
m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams)
22152219
else
2216-
m.itemLogo.visible = false
2217-
m.itemLogo.uri = ""
2220+
m.itemLogo.uri = getPlaceholderImagePath(item.type)
22182221
end if
22192222
return
22202223
else if item.type = "TvChannel"
2221-
' Channel logo (typically square); hide if none
2224+
' Channel logo (typically square); folder-fallback placeholder if none
22222225
if isValidAndNotEmpty(item.primaryImageTag)
22232226
imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag }
22242227
m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams)
22252228
else
2226-
m.itemLogo.visible = false
2227-
m.itemLogo.uri = ""
2229+
m.itemLogo.uri = getPlaceholderImagePath(item.type)
22282230
end if
22292231
return
22302232
else if item.type = "Program"
2231-
' Program image; fall back to channel logo
2233+
' Program imagechannel logo → typed placeholder
22322234
if isValidAndNotEmpty(item.primaryImageTag)
22332235
imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag }
22342236
m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams)
22352237
else if isValidAndNotEmpty(item.channelPrimaryImageTag) and isValidAndNotEmpty(item.channelId)
22362238
imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.channelPrimaryImageTag }
22372239
m.itemLogo.uri = ImageURL(item.channelId, "Primary", imgParams)
22382240
else
2239-
m.itemLogo.visible = false
2240-
m.itemLogo.uri = ""
2241+
m.itemLogo.uri = getPlaceholderImagePath(item.type)
22412242
end if
22422243
return
22432244
else if item.type = "Audio"
2244-
' Own primary image → album art fallback → hide
2245+
' Own primary image → album art fallback → typed placeholder
22452246
if isValidAndNotEmpty(item.primaryImageTag)
22462247
imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag }
22472248
m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams)
22482249
else if isValidAndNotEmpty(item.albumId) and isValidAndNotEmpty(item.albumPrimaryImageTag)
22492250
imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.albumPrimaryImageTag }
22502251
m.itemLogo.uri = ImageURL(item.albumId, "Primary", imgParams)
22512252
else
2252-
m.itemLogo.visible = false
2253-
m.itemLogo.uri = ""
2253+
m.itemLogo.uri = getPlaceholderImagePath(item.type)
22542254
end if
22552255
return
22562256
else if item.type = "Episode" or item.type = "Season" or item.type = "Recording"
@@ -2266,14 +2266,14 @@
22662266
logoImageTag = item.parentLogoImageTag
22672267
end if
22682268
else if item.type = "Video" or item.type = "MusicVideo"
2269-
' Videos and MusicVideos don't typically have Logo images — use Primary (poster) image instead
2269+
' Videos and MusicVideos don't typically have Logo images — use Primary (poster)
2270+
' image instead; playCircle placeholder if none.
22702271
if isValidAndNotEmpty(item.primaryImageTag)
22712272
imgParams = { maxHeight: 212, maxWidth: 500, quality: 90, tag: item.primaryImageTag }
22722273
m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams)
2273-
return
2274+
else
2275+
m.itemLogo.uri = getPlaceholderImagePath(item.type)
22742276
end if
2275-
m.itemLogo.visible = false
2276-
m.itemLogo.uri = ""
22772277
return
22782278
end if
22792279

@@ -2283,25 +2283,26 @@
22832283
else if item.type = "Movie" or item.type = "Series" or item.type = "BoxSet"
22842284
' No logo: fall back to primary (poster) image — fetch at full quality; display height is
22852285
' capped by LOGO_MAX_DISPLAY_HEIGHT in onLogoLoadStatusChanged to avoid overlapping info rows.
2286+
' Movie placeholder if neither logo nor primary is available.
22862287
if isValidAndNotEmpty(item.primaryImageTag)
22872288
imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag }
22882289
m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams)
22892290
else
2290-
m.itemLogo.visible = false
2291-
m.itemLogo.uri = ""
2291+
m.itemLogo.uri = getPlaceholderImagePath(item.type)
22922292
end if
22932293
else if item.type = "Episode" or item.type = "Season" or item.type = "Recording"
2294-
' No item primary (already tried above), no series logo: fall back to series primary poster
2294+
' No item primary (already tried above), no series logo: fall back to series primary poster,
2295+
' then a playCircle placeholder if the series has no primary image either.
22952296
if isValidAndNotEmpty(item.seriesPrimaryImageTag) and isValidAndNotEmpty(item.seriesId)
22962297
imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.seriesPrimaryImageTag }
22972298
m.itemLogo.uri = ImageURL(item.seriesId, "Primary", imgParams)
22982299
else
2299-
m.itemLogo.visible = false
2300-
m.itemLogo.uri = ""
2300+
m.itemLogo.uri = getPlaceholderImagePath(item.type)
23012301
end if
23022302
else
2303-
m.itemLogo.visible = false
2304-
m.itemLogo.uri = ""
2303+
' Catch-all: any other type without a known fallback chain gets the type-driven
2304+
' placeholder (falls through to the folder glyph for genuinely unknown types).
2305+
m.itemLogo.uri = getPlaceholderImagePath(item.type)
23052306
end if
23062307
end sub
23072308

@@ -2389,6 +2390,17 @@
23892390
if not isValid(m.itemLogo) then return
23902391

23912392
if m.itemLogo.loadStatus = "ready"
2393+
' Tint placeholder PNGs (white glyph on transparent BG) with a near-page-BG
2394+
' color so they recess like a watermark instead of competing with the title.
2395+
' Detection by URI prefix: every placeholder asset lives under
2396+
' pkg:/images/placeholders/. Real (server-loaded) images need the default
2397+
' white blendColor so their native colors render correctly.
2398+
if Left(m.itemLogo.uri, 24) = "pkg:/images/placeholders"
2399+
m.itemLogo.blendColor = m.global.constants.colorBackgroundSecondary
2400+
else
2401+
m.itemLogo.blendColor = "0xFFFFFFFF"
2402+
end if
2403+
23922404
logoWidth = m.itemLogo.bitmapWidth
23932405
logoHeight = m.itemLogo.bitmapHeight
23942406

docs/components_ItemGrid_GridItem.bs.html

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
import "pkg:/source/roku_modules/log/LogMixin.brs"
55
import "pkg:/source/utils/itemImageUrl.bs"
66
import "pkg:/source/utils/misc.bs"
7+
import "pkg:/source/utils/placeholderImage.bs"
78
import "pkg:/source/utils/textureManager.bs"
89

910
sub init()
1011
m.log = new log.Logger("GridItem")
1112
m.itemPoster = m.top.findNode("itemPoster")
1213
m.itemIcon = m.top.findNode("itemIcon")
13-
m.posterText = m.top.findNode("posterText")
1414
m.itemText = m.top.findNode("itemText")
15-
m.backdrop = m.top.findNode("backdrop")
15+
m.placeholder = m.top.findNode("placeholder")
1616

1717
m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
1818

@@ -131,25 +131,29 @@
131131
m.log.warn("Unhandled Grid Item Type", itemData.type)
132132
end if
133133

134-
' Cache URI for texture management and conditionally load
134+
' Cache URI for texture management
135135
m.cachedPosterUri = posterUri
136136
m.cachedLoadDisplayMode = m.itemPoster.loadDisplayMode
137-
if posterUri = "" or shouldLoadGridTexture()
137+
138+
if posterUri = ""
139+
' Known-missing image — eagerly surface the typed glyph on the backdrop.
140+
' Load-status never fires "failed" for an empty URI, so we must set the
141+
' placeholder state here rather than waiting for onPosterLoadStatusChanged.
142+
m.itemPoster.uri = ""
143+
showPlaceholder(resolveItemPlaceholderType(m.top.itemContent))
144+
else if shouldLoadGridTexture()
145+
' Image expected and the texture manager wants this cell loaded — start
146+
' the placeholder in loading state; the load-status observer takes over.
147+
resetPlaceholderToLoading()
138148
m.itemPoster.uri = posterUri
139149
else
150+
' Image expected but the texture manager is keeping this cell off-screen
151+
' to save memory. Loading state until the cell scrolls into range and
152+
' reloadGridTexture() restores the URI.
140153
m.isTextureUnloaded = true
154+
resetPlaceholderToLoading()
141155
m.itemPoster.uri = ""
142-
m.backdrop.visible = true
143-
end if
144-
145-
'If Poster not loaded, ensure "blue box" is shown until loaded
146-
if m.itemPoster.loadStatus <> "ready"
147-
m.backdrop.visible = true
148-
m.posterText.visible = true
149156
end if
150-
151-
m.posterText.text = m.itemText.text
152-
153157
end sub
154158

155159
' Apply square 323x323 layout for music items (artists, albums, genres)
@@ -162,11 +166,8 @@
162166
m.itemText.translation = [0, m.itemPoster.translation[1] + m.itemPoster.height + 18]
163167
m.itemText.maxWidth = 323
164168

165-
m.backdrop.height = 323
166-
m.backdrop.width = 323
167-
168-
m.posterText.height = 313
169-
m.posterText.width = 313
169+
m.placeholder.width = 323
170+
m.placeholder.height = 323
170171
end sub
171172

172173
' Enable title scrolling based on item focus
@@ -181,17 +182,44 @@
181182
end if
182183
end sub
183184

184-
'Hide backdrop and text when poster loaded
185+
' Toggle the JRPlaceholder fallback based on the real poster's load state.
186+
' Mirrors the canonical state machine from JRRowItem.bs onPosterLoadStatusChanged.
185187
sub onPosterLoadStatusChanged()
186188
' Ignore status changes from intentional texture unloads (uri cleared to free memory)
187189
if m.isTextureUnloaded then return
188190

189-
if m.itemPoster.loadStatus = "ready"
190-
m.backdrop.visible = false
191-
m.posterText.visible = false
191+
if m.itemPoster.loadStatus = "ready" and m.itemPoster.uri <> ""
192+
' Real image loaded — placeholder is no longer needed
193+
m.placeholder.visible = false
194+
else if m.itemPoster.loadStatus = "failed" and m.itemPoster.uri <> ""
195+
' Real image failed — surface a type-appropriate glyph on the backdrop
196+
showPlaceholder(resolveItemPlaceholderType(m.top.itemContent))
197+
else
198+
' Loading or no URI — keep the placeholder visible in its current state
199+
m.placeholder.visible = true
192200
end if
193201
end sub
194202

203+
' Flip the placeholder into failure state with a type-appropriate glyph, then
204+
' clear the poster URI so the glyph isn't covered by a stale broken image.
205+
' itemType="" puts JRPlaceholder in loading state (backdrop only, no glyph);
206+
' resolveItemPlaceholderType returns "" only for invalid/typeless items, so
207+
' valid items always end up here with a typed glyph or the Folder fallback.
208+
sub showPlaceholder(itemType as string)
209+
m.placeholder.itemType = itemType
210+
m.placeholder.visible = true
211+
m.itemPoster.uri = ""
212+
end sub
213+
214+
' Reset the JRPlaceholder to loading state (themed backdrop visible, no glyph).
215+
' Used by the initial-load branches in onItemContentChanged and by the texture
216+
' unload paths so off-screen / unloaded cells don't carry a stale failure-state
217+
' glyph back into view when they reload.
218+
sub resetPlaceholderToLoading()
219+
m.placeholder.itemType = ""
220+
m.placeholder.visible = true
221+
end sub
222+
195223
' ============================================================================
196224
' Texture Management
197225
' ============================================================================
@@ -352,15 +380,15 @@
352380

353381
m.isTextureUnloaded = true
354382
m.itemPoster.uri = ""
355-
m.backdrop.visible = true
383+
resetPlaceholderToLoading()
356384
end sub
357385

358386
' Force-unloads textures unconditionally — used only during onDestroy().
359387
' Ignores m.isTextureUnloaded and m.cachedPosterUri guards so every cell releases memory.
360388
sub forceUnloadGridTexture()
361389
m.isTextureUnloaded = true
362390
m.itemPoster.uri = ""
363-
m.backdrop.visible = true
391+
resetPlaceholderToLoading()
364392
end sub
365393

366394
' Restores poster URI from cache when the cell scrolls back on screen.

docs/components_ItemGrid_GridItemSmall.bs.html

Lines changed: 48 additions & 18 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)