components_search_SearchRow.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/utils/config.bs"
import "pkg:/source/utils/deviceCapabilities.bs"
import "pkg:/source/utils/misc.bs"

sub init()
  m.top.itemComponentName = "BrowseRowItem"
  m.top.content = getData()

  ' override defaults
  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
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")
  ' Do this to keep the ordering, AssociateArrays have no order
  type_array = ["Movie", "Series", "TvChannel", "Episode", "MusicArtist", "MusicAlbum", "Audio", "Person", "Playlist"]
  content_types = {
    "TvChannel": { "label": "Channels", "count": 0 },
    "Movie": { "label": "Movies", "count": 0 },
    "Series": { "label": "Shows", "count": 0 },
    "Episode": { "label": "Episodes", "count": 0 },
    "MusicArtist": { "label": "Artists", "count": 0 },
    "MusicAlbum": { "label": "Albums", "count": 0 },
    "Audio": { "label": "Songs", "count": 0 },
    "Person": { "label": "People", "count": 0 },
    "Playlist": { "label": "Playlist", "count": 0 }
  }

  for each item in itemData.Items
    if isValid(content_types[item.type])
      content_types[item.type].count += 1
    end if
  end for

  for each ctype in type_array
    content_type = content_types[ctype]
    if content_type.count > 0
      addRow(data, content_type.label, ctype)
    end if
  end for

  m.top.content = data
  applyRowSizes(data)
  return data
end function

sub addRow(data, title, type_filter)
  itemData = m.top.itemData
  row = data.CreateChild("ContentNode")
  row.title = title
  for each item in itemData.Items
    if item.type = type_filter
      row.appendChild(item)
    end if
  end for
end sub

' Returns the appropriate slot size for a given Jellyfin item type.
' Episode → WIDE (16:9 screenshots)
' Music and Playlist → SQUARE (1:1 album art)
' All others (Movie, Series, TvChannel, Person, etc.) → PORTRAIT (2:3 poster)
function getSlotSizeForType(itemType as string) as object
  if itemType = "Episode"
    return rowSlotSize.WIDE
  else if itemType = "MusicArtist" or itemType = "MusicAlbum" or itemType = "Audio" or itemType = "Playlist"
    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