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