|
11 | 11 | import "pkg:/source/utils/itemImageUrl.bs" |
12 | 12 | import "pkg:/source/utils/mediaDisplayTitle.bs" |
13 | 13 | import "pkg:/source/utils/misc.bs" |
| 14 | +import "pkg:/source/utils/placeholderImage.bs" |
14 | 15 | import "pkg:/source/utils/streamSelection.bs" |
15 | 16 | import "pkg:/source/utils/textureManager.bs" |
16 | 17 | import "pkg:/source/utils/trackClusterFocus.bs" |
|
2154 | 2155 | end sub |
2155 | 2156 |
|
2156 | 2157 | ' 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 |
2162 | 2168 | ' @param {object} item - JellyfinBaseItem node |
2163 | 2169 | sub setItemLogo(item as object) |
2164 | 2170 | if not isValid(m.itemLogo) then return |
|
2167 | 2173 | logoImageTag = item.logoImageTag |
2168 | 2174 |
|
2169 | 2175 | 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 |
2171 | 2177 | if isValidAndNotEmpty(item.primaryImageTag) |
2172 | 2178 | imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag } |
2173 | 2179 | m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams) |
2174 | 2180 | else |
2175 | | - m.itemLogo.uri = "pkg:/images/placeholders/person_$$RES$$.png" |
| 2181 | + m.itemLogo.uri = getPlaceholderImagePath(item.type) |
2176 | 2182 | end if |
2177 | 2183 | return |
2178 | 2184 | else if item.type = "MusicArtist" |
|
2184 | 2190 | imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag } |
2185 | 2191 | m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams) |
2186 | 2192 | else |
2187 | | - m.itemLogo.uri = "pkg:/images/placeholders/artist_$$RES$$.png" |
| 2193 | + m.itemLogo.uri = getPlaceholderImagePath(item.type) |
2188 | 2194 | end if |
2189 | 2195 | return |
2190 | 2196 | 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 |
2192 | 2198 | if isValidAndNotEmpty(item.primaryImageTag) |
2193 | 2199 | imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag } |
2194 | 2200 | m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams) |
2195 | 2201 | else |
2196 | | - m.itemLogo.visible = false |
2197 | | - m.itemLogo.uri = "" |
| 2202 | + m.itemLogo.uri = getPlaceholderImagePath(item.type) |
2198 | 2203 | end if |
2199 | 2204 | return |
2200 | 2205 | 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 |
2202 | 2207 | if isValidAndNotEmpty(item.primaryImageTag) |
2203 | 2208 | imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag } |
2204 | 2209 | m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams) |
2205 | 2210 | else |
2206 | | - m.itemLogo.visible = false |
2207 | | - m.itemLogo.uri = "" |
| 2211 | + m.itemLogo.uri = getPlaceholderImagePath(item.type) |
2208 | 2212 | end if |
2209 | 2213 | return |
2210 | 2214 | 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 |
2212 | 2216 | if isValidAndNotEmpty(item.primaryImageTag) |
2213 | 2217 | imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag } |
2214 | 2218 | m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams) |
2215 | 2219 | else |
2216 | | - m.itemLogo.visible = false |
2217 | | - m.itemLogo.uri = "" |
| 2220 | + m.itemLogo.uri = getPlaceholderImagePath(item.type) |
2218 | 2221 | end if |
2219 | 2222 | return |
2220 | 2223 | else if item.type = "TvChannel" |
2221 | | - ' Channel logo (typically square); hide if none |
| 2224 | + ' Channel logo (typically square); folder-fallback placeholder if none |
2222 | 2225 | if isValidAndNotEmpty(item.primaryImageTag) |
2223 | 2226 | imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag } |
2224 | 2227 | m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams) |
2225 | 2228 | else |
2226 | | - m.itemLogo.visible = false |
2227 | | - m.itemLogo.uri = "" |
| 2229 | + m.itemLogo.uri = getPlaceholderImagePath(item.type) |
2228 | 2230 | end if |
2229 | 2231 | return |
2230 | 2232 | else if item.type = "Program" |
2231 | | - ' Program image; fall back to channel logo |
| 2233 | + ' Program image → channel logo → typed placeholder |
2232 | 2234 | if isValidAndNotEmpty(item.primaryImageTag) |
2233 | 2235 | imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag } |
2234 | 2236 | m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams) |
2235 | 2237 | else if isValidAndNotEmpty(item.channelPrimaryImageTag) and isValidAndNotEmpty(item.channelId) |
2236 | 2238 | imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.channelPrimaryImageTag } |
2237 | 2239 | m.itemLogo.uri = ImageURL(item.channelId, "Primary", imgParams) |
2238 | 2240 | else |
2239 | | - m.itemLogo.visible = false |
2240 | | - m.itemLogo.uri = "" |
| 2241 | + m.itemLogo.uri = getPlaceholderImagePath(item.type) |
2241 | 2242 | end if |
2242 | 2243 | return |
2243 | 2244 | else if item.type = "Audio" |
2244 | | - ' Own primary image → album art fallback → hide |
| 2245 | + ' Own primary image → album art fallback → typed placeholder |
2245 | 2246 | if isValidAndNotEmpty(item.primaryImageTag) |
2246 | 2247 | imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag } |
2247 | 2248 | m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams) |
2248 | 2249 | else if isValidAndNotEmpty(item.albumId) and isValidAndNotEmpty(item.albumPrimaryImageTag) |
2249 | 2250 | imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.albumPrimaryImageTag } |
2250 | 2251 | m.itemLogo.uri = ImageURL(item.albumId, "Primary", imgParams) |
2251 | 2252 | else |
2252 | | - m.itemLogo.visible = false |
2253 | | - m.itemLogo.uri = "" |
| 2253 | + m.itemLogo.uri = getPlaceholderImagePath(item.type) |
2254 | 2254 | end if |
2255 | 2255 | return |
2256 | 2256 | else if item.type = "Episode" or item.type = "Season" or item.type = "Recording" |
|
2266 | 2266 | logoImageTag = item.parentLogoImageTag |
2267 | 2267 | end if |
2268 | 2268 | 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. |
2270 | 2271 | if isValidAndNotEmpty(item.primaryImageTag) |
2271 | 2272 | imgParams = { maxHeight: 212, maxWidth: 500, quality: 90, tag: item.primaryImageTag } |
2272 | 2273 | m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams) |
2273 | | - return |
| 2274 | + else |
| 2275 | + m.itemLogo.uri = getPlaceholderImagePath(item.type) |
2274 | 2276 | end if |
2275 | | - m.itemLogo.visible = false |
2276 | | - m.itemLogo.uri = "" |
2277 | 2277 | return |
2278 | 2278 | end if |
2279 | 2279 |
|
|
2283 | 2283 | else if item.type = "Movie" or item.type = "Series" or item.type = "BoxSet" |
2284 | 2284 | ' No logo: fall back to primary (poster) image — fetch at full quality; display height is |
2285 | 2285 | ' capped by LOGO_MAX_DISPLAY_HEIGHT in onLogoLoadStatusChanged to avoid overlapping info rows. |
| 2286 | + ' Movie placeholder if neither logo nor primary is available. |
2286 | 2287 | if isValidAndNotEmpty(item.primaryImageTag) |
2287 | 2288 | imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.primaryImageTag } |
2288 | 2289 | m.itemLogo.uri = ImageURL(item.id, "Primary", imgParams) |
2289 | 2290 | else |
2290 | | - m.itemLogo.visible = false |
2291 | | - m.itemLogo.uri = "" |
| 2291 | + m.itemLogo.uri = getPlaceholderImagePath(item.type) |
2292 | 2292 | end if |
2293 | 2293 | 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. |
2295 | 2296 | if isValidAndNotEmpty(item.seriesPrimaryImageTag) and isValidAndNotEmpty(item.seriesId) |
2296 | 2297 | imgParams = { maxHeight: 534, maxWidth: LOGO_MAX_DISPLAY_WIDTH, quality: 90, tag: item.seriesPrimaryImageTag } |
2297 | 2298 | m.itemLogo.uri = ImageURL(item.seriesId, "Primary", imgParams) |
2298 | 2299 | else |
2299 | | - m.itemLogo.visible = false |
2300 | | - m.itemLogo.uri = "" |
| 2300 | + m.itemLogo.uri = getPlaceholderImagePath(item.type) |
2301 | 2301 | end if |
2302 | 2302 | 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) |
2305 | 2306 | end if |
2306 | 2307 | end sub |
2307 | 2308 |
|
|
2389 | 2390 | if not isValid(m.itemLogo) then return |
2390 | 2391 |
|
2391 | 2392 | 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 | + |
2392 | 2404 | logoWidth = m.itemLogo.bitmapWidth |
2393 | 2405 | logoHeight = m.itemLogo.bitmapHeight |
2394 | 2406 |
|
|
0 commit comments