components_search_SearchRow.bs
import "pkg:/source/api/ApiClient.bs"
import "pkg:/source/api/apiPool.bs"
import "pkg:/source/api/baseRequest.bs"
import "pkg:/source/api/image.bs"
import "pkg:/source/api/items.bs"
import "pkg:/source/constants/itemAspectRatio.bs"
import "pkg:/source/constants/itemTypeOrder.bs"
import "pkg:/source/translationKeys.bs"
import "pkg:/source/utils/config.bs"
import "pkg:/source/utils/deviceCapabilities.bs"
import "pkg:/source/utils/misc.bs"
import "pkg:/source/utils/textureManager.bs"
sub init()
m.top.itemComponentName = "BrowseRowItem"
' Set layout before initTextureManager so it calculates correct visible width
m.top.rowLabelOffset = [0, 21]
m.top.vertFocusAnimationStyle = "fixedFocus"
m.top.rowFocusAnimationStyle = "fixedFocus"
m.top.focusXOffset = [0]
m.top.translation = [491, 165]
' itemSize width sets the row container width; height is the tallest possible row.
' rowHeights (set in applyRowSizes) overrides height per-row.
m.top.itemSize = [1325, rowSlotSize.ROW_HEIGHT_PORTRAIT]
m.top.itemSpacing = [0, 0]
m.top.numRows = 3
m.top.content = getData()
initTextureManager(m.top.content, m.top.itemSize, m.top.focusXOffset, m.top.rowItemSpacing)
end sub
function getData()
if not isValid(m.top.itemData)
data = CreateObject("roSGNode", "ContentNode")
return data
end if
itemData = m.top.itemData
' todo - Or get the old data? I can't remember...
data = CreateObject("roSGNode", "ContentNode")
' Ordering follows itemTypeOrder.SEARCH (web-client section sort order).
' AssocArrays have no guaranteed order, so a separate array drives row creation.
typeArray = itemTypeOrder.SEARCH
contentTypes = {
"Movie": { "label": "Movies", "count": 0 },
"Series": { "label": "Shows", "count": 0 },
"Episode": { "label": "Episodes", "count": 0 },
"Person": { "label": "People", "count": 0 },
"Playlist": { "label": "Playlists", "count": 0 },
"MusicArtist": { "label": "Artists", "count": 0 },
"MusicAlbum": { "label": "Albums", "count": 0 },
"Audio": { "label": "Songs", "count": 0 },
"Video": { "label": "Videos", "count": 0 },
"MusicVideo": { "label": "Music Videos", "count": 0 },
"Program": { "label": "Programs", "count": 0 },
"TvChannel": { "label": "Channels", "count": 0 },
"Recording": { "label": "Recordings", "count": 0 },
"PhotoAlbum": { "label": "Photo Albums", "count": 0 },
"Photo": { "label": "Photos", "count": 0 },
"BoxSet": { "label": "Collections", "count": 0 }
}
for each item in itemData.Items
if isValid(contentTypes[item.type])
contentTypes[item.type].count += 1
end if
end for
for each ctype in typeArray
contentType = contentTypes[ctype]
if contentType.count > 0
addRow(data, contentType.label, ctype)
end if
end for
m.top.content = data
initTextureManager(m.top.content, m.top.itemSize, m.top.focusXOffset, m.top.rowItemSpacing)
applyRowSizes(data)
updateTextureBufferRange(m.top.content, 0, 0, m.top.numRows)
activateTextureManager(m.top.content)
return data
end function
sub addRow(data, title, typeFilter)
itemData = m.top.itemData
row = data.CreateChild("ContentNode")
row.title = title
for each item in itemData.Items
if item.type = typeFilter
row.appendChild(item)
end if
end for
end sub
' Returns the appropriate slot size for a given Jellyfin item type.
' WIDE (16:9) — Episode, Video, MusicVideo, PhotoAlbum, Photo
' SQUARE (1:1) — Music types, Playlist, Program, TvChannel, Recording
' PORTRAIT (2:3) — all others (Movie, Series, Person, BoxSet, etc.)
function getSlotSizeForType(itemType as string) as object
if itemType = "Episode" or itemType = "Video" or itemType = "MusicVideo" or itemType = "PhotoAlbum" or itemType = "Photo"
return rowSlotSize.WIDE
else if itemType = "MusicArtist" or itemType = "MusicAlbum" or itemType = "Audio" or itemType = "Playlist" or itemType = "Program" or itemType = "TvChannel" or itemType = "Recording"
return rowSlotSize.SQUARE
end if
return rowSlotSize.PORTRAIT
end function
' Applies per-row slot sizes, heights, and spacings to the RowList.
' Infers each row's slot size from its first item's type.
' Must be called after all rows are added to the content node.
sub applyRowSizes(data as object)
if not isValid(data) then return
rows = data.getChildren(-1, 0)
if rows.count() = 0 then return
newSizeArray = []
newRowHeights = []
newRowSpacings = []
for each row in rows
firstItem = row.getChild(0)
slotSize = getSlotSizeForType(isValid(firstItem) ? firstItem.type : "")
newSizeArray.Push(slotSize)
slotHeight = slotSize[1]
if slotHeight = rowSlotSize.PORTRAIT[1]
newRowHeights.Push(rowSlotSize.ROW_HEIGHT_PORTRAIT)
else if slotHeight = rowSlotSize.SQUARE[1]
newRowHeights.Push(rowSlotSize.ROW_HEIGHT_SQUARE)
else
newRowHeights.Push(rowSlotSize.ROW_HEIGHT_WIDE)
end if
newRowSpacings.Push(60)
end for
m.top.rowItemSize = newSizeArray
m.top.rowHeights = newRowHeights
m.top.rowSpacings = newRowSpacings
end sub