components_home_HomeRows.bs

import "pkg:/source/utils/misc.bs"
import "pkg:/source/constants/HomeRowItemSizes.bs"

const LOADING_WAIT_TIME = 2

sub init()
    m.top.itemComponentName = "HomeItem"
    ' how many rows are visible on the screen
    m.top.numRows = 2

    m.top.rowFocusAnimationStyle = "fixedFocusWrap"
    m.top.vertFocusAnimationStyle = "fixedFocus"

    m.top.showRowLabel = [true]
    m.top.rowLabelOffset = [0, 20]
    ' Hide the row counter to prevent flicker. We'll show it once loading timer fires
    m.top.showRowCounter = [false]

    m.top.content = CreateObject("roSGNode", "ContentNode")

    m.loadingTimer = createObject("roSGNode", "Timer")
    m.loadingTimer.duration = LOADING_WAIT_TIME
    m.loadingTimer.observeField("fire", "loadingTimerComplete")

    updateSize()

    m.top.setfocus(true)

    m.top.observeField("rowItemSelected", "itemSelected")

    ' Load the Libraries from API via task
    m.LoadLibrariesTask = createObject("roSGNode", "LoadItemsTask")
    m.LoadLibrariesTask.observeField("content", "onLibrariesLoaded")

    ' set up task nodes for other rows
    m.LoadContinueWatchingTask = createObject("roSGNode", "LoadItemsTask")
    m.LoadContinueWatchingTask.itemsToLoad = "continue"

    m.LoadNextUpTask = createObject("roSGNode", "LoadItemsTask")
    m.LoadNextUpTask.itemsToLoad = "nextUp"

    m.LoadOnNowTask = createObject("roSGNode", "LoadItemsTask")
    m.LoadOnNowTask.itemsToLoad = "onNow"

    m.LoadFavoritesTask = createObject("roSGNode", "LoadItemsTask")
    m.LoadFavoritesTask.itemsToLoad = "favorites"
end sub

sub loadLibraries()
    m.LoadLibrariesTask.control = "RUN"
end sub

sub updateSize()
    uiRowLayout = m.global.session.user.settings["ui.row.layout"]

    if isValid(uiRowLayout)
        if uiRowLayout = "fullwidth"
            m.top.translation = [0, 180]
            ' rows take up full width of the screen
            m.top.itemSize = [1920, 330]
            ' align with edge of "action" safe zone
            m.top.focusXOffset = [96]
            m.top.rowLabelOffset = [96, 20]
        else
            ' original layout
            m.top.translation = [111, 180]
            m.top.itemSize = [1703, 330]
            ' reset to defaults
            m.top.focusXOffset = []
            m.top.rowLabelOffset = [0, 20]
        end if
    end if

    ' spacing between rows
    m.top.itemSpacing = [0, 105]
    ' spacing between items in a row
    m.top.rowItemSpacing = [21, 0]

    m.top.visible = true
end sub

' processUserSections: Loop through user's chosen home section settings and generate the content for each row
'
sub processUserSections()
    m.expectedRowCount = 1 ' the favorites row is hardcoded to always show atm
    m.processedRowCount = 0

    sessionUser = m.global.session.user
    userSettings = sessionUser.settings

    ' calculate expected row count by processing homesections
    for i = 0 to 6
        userSection = userSettings["homesection" + i.toStr()]
        sectionName = userSection ?? "none"
        sectionName = LCase(sectionName)

        if sectionName = "latestmedia"
            ' expect 1 row per filtered media library
            m.filteredLatest = filterNodeArray(m.libraryData, "id", sessionUser.configuration.LatestItemsExcludes)
            for each latestLibrary in m.filteredLatest
                if latestLibrary.collectionType <> "boxsets" and latestLibrary.collectionType <> "livetv" and latestLibrary.json.CollectionType <> "Program"
                    m.expectedRowCount++
                end if
            end for
        else if sectionName <> "none"
            m.expectedRowCount++
        end if
    end for

    ' Add home sections in order based on user settings
    loadedSections = 0
    for i = 0 to 6
        userSection = userSettings["homesection" + i.toStr()]
        sectionName = userSection ?? "none"
        sectionName = LCase(sectionName)

        sectionLoaded = false
        if sectionName <> "none"
            sectionLoaded = addHomeSection(sectionName)
        end if

        ' Count how many sections with data are loaded
        if sectionLoaded then loadedSections++

        ' If 2 sections with data are loaded or we're at the end of the web client section data, consider the home view loaded
        if not m.global.app_loaded
            if loadedSections = 2 or i = 6
                m.top.signalBeacon("AppLaunchComplete") ' Roku Performance monitoring
                m.global.app_loaded = true
            end if
        end if
    end for

    ' Favorites isn't an option in Web settings, so we manually add it to the end for now
    addHomeSection("favorites")

    ' Start the timer for creating the content rows before we set the cursor size
    m.loadingTimer.control = "start"
end sub

' onLibrariesLoaded: Handler when LoadLibrariesTask returns data
'
sub onLibrariesLoaded()
    ' save data for other functions
    m.libraryData = m.LoadLibrariesTask.content
    m.LoadLibrariesTask.unobserveField("content")
    m.LoadLibrariesTask.content = []

    processUserSections()
end sub

' getOriginalSectionIndex: Gets the index of a section from user settings and adds count of currently known latest media sections
'
' @param {string} sectionName - Name of section we're looking up
'
' @return {integer} indicating index of section taking latest media sections into account
function getOriginalSectionIndex(sectionName as string) as integer
    searchSectionName = LCase(sectionName).Replace(" ", "")

    sectionIndex = 0
    indexLatestMediaSection = 0

    userSettings = m.global.session.user.settings

    for i = 0 to 6
        userSection = userSettings["homesection" + i.toStr()]
        settingSectionName = userSection ?? "none"
        settingSectionName = LCase(settingSectionName)

        if settingSectionName = "latestmedia"
            indexLatestMediaSection = i
        end if

        if settingSectionName = searchSectionName
            sectionIndex = i
        end if
    end for

    ' If the latest media section is before the section we're searching for, then we need to account for how many latest media rows there are
    addLatestMediaSectionCount = (indexLatestMediaSection < sectionIndex)

    if addLatestMediaSectionCount
        for i = sectionIndex to m.top.content.getChildCount() - 1
            sectionToTest = m.top.content.getChild(i)
            if LCase(Left(sectionToTest.title, 6)) = "latest"
                sectionIndex++
            end if
        end for
    end if

    return sectionIndex
end function

' removeHomeSection: Removes a home section from the home rows
'
' @param {string} sectionToRemove - Title property of section we're removing
sub removeHomeSection(sectionTitleToRemove as string)
    if not isValid(sectionTitleToRemove) then return

    sectionTitle = LCase(sectionTitleToRemove).Replace(" ", "")
    if not sectionExists(sectionTitle) then return

    sectionIndexToRemove = getSectionIndex(sectionTitle)

    m.top.content.removeChildIndex(sectionIndexToRemove)
    setRowItemSize()
end sub

' setRowItemSize: Loops through all home sections and sets the correct item sizes per row
'
sub setRowItemSize()
    if not isValid(m.top.content) then return

    homeSections = m.top.content.getChildren(-1, 0)
    newSizeArray = CreateObject("roArray", homeSections.count(), false)

    for i = 0 to homeSections.count() - 1
        newSizeArray[i] = isValid(homeSections[i].cursorSize) ? homeSections[i].cursorSize : homeRowItemSizes.WIDE_POSTER
    end for
    m.top.rowItemSize = newSizeArray

    ' If we have processed the expected number of content rows, stop the loading timer and run the complete function
    if m.expectedRowCount = m.processedRowCount
        m.loadingTimer.control = "stop"
        loadingTimerComplete()
    end if
end sub

' loadingTimerComplete: Event handler for when loading wait time has expired
'
sub loadingTimerComplete()
    if not m.top.showRowCounter[0]
        ' Show the row counter to prevent flicker
        m.top.showRowCounter = [true]
    end if
end sub

' addHomeSection: Adds a new home section to the home rows.
'
' @param {string} sectionType - Type of section to add
' @return {boolean} indicating if the section was handled
function addHomeSection(sectionType as string) as boolean
    ' Poster size library items
    if sectionType = "livetv"
        createLiveTVRow()
        return true
    end if

    ' Poster size library items
    if sectionType = "smalllibrarytiles"
        createLibraryRow()
        return true
    end if

    ' Continue Watching items
    if sectionType = "resume"
        createContinueWatchingRow()
        return true
    end if

    ' Next Up items
    if sectionType = "nextup"
        createNextUpRow()
        return true
    end if

    ' Latest items in each library
    if sectionType = "latestmedia"
        createLatestInRows()
        return true
    end if

    ' Favorite Items
    if sectionType = "favorites"
        createFavoritesRow()
        return true
    end if

    ' This section type isn't supported.
    ' Count it as processed since we aren't going to do anything else with it
    m.processedRowCount++
    return false
end function

' createLibraryRow: Creates a row displaying the user's libraries
'
sub createLibraryRow()
    m.processedRowCount++
    ' Ensure we have data
    if not isValidAndNotEmpty(m.libraryData) then return

    sectionName = tr("My Media")

    ' We don't refresh library data, so if section already exists, exit
    if sectionExists(sectionName)
        return
    end if

    row = CreateObject("roSGNode", "HomeRow")
    row.title = sectionName
    row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
    row.cursorSize = homeRowItemSizes.WIDE_POSTER

    filteredMedia = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.MyMediaExcludes)
    for each item in filteredMedia
        row.appendChild(item)
    end for

    ' Row does not exist, insert it into the home view
    m.top.content.insertChild(row, getOriginalSectionIndex("smalllibrarytiles"))
    setRowItemSize()
end sub

' createLatestInRows: Creates a row displaying latest items in each of the user's libraries
'
sub createLatestInRows()
    ' Ensure we have data
    if not isValidAndNotEmpty(m.libraryData) then return

    ' create a "Latest In" row for each library
    for each lib in m.filteredLatest
        if lib.collectionType <> "boxsets" and lib.collectionType <> "livetv" and lib.json.CollectionType <> "Program"
            sectionName = `${tr("Latest in")} ${lib.name} >`

            imagesize = homeRowItemSizes.WIDE_POSTER

            if isValid(lib.json.CollectionType)
                if LCase(lib.json.CollectionType) = "movies"
                    imagesize = homeRowItemSizes.MOVIE_POSTER
                else if LCase(lib.json.CollectionType) = "music"
                    imagesize = homeRowItemSizes.MUSIC_ALBUM
                end if
            end if

            if not sectionExists(sectionName)
                nextUpRow = m.top.content.CreateChild("HomeRow")
                nextUpRow.title = sectionName
                nextUpRow.imageWidth = imagesize[0]
                nextUpRow.cursorSize = imagesize
            end if

            loadLatest = createObject("roSGNode", "LoadItemsTask")
            loadLatest.itemsToLoad = "latest"
            loadLatest.itemId = lib.id

            metadata = { "title": lib.name }
            metadata.Append({ "contentType": lib.json.CollectionType })
            loadLatest.metadata = metadata

            loadLatest.observeField("content", "updateLatestItems")
            loadLatest.control = "RUN"
        end if
    end for
end sub

' sectionExists: Checks if passed section exists in home row content
'
' @param {string} sectionTitle - Title of section we're checking for
'
' @return {boolean} indicating if the section currently exists in the home row content
function sectionExists(sectionTitle as string) as boolean
    if not isValid(sectionTitle) then return false
    if not isValid(m.top.content) then return false

    searchSectionTitle = LCase(sectionTitle).Replace(" ", "")

    homeSections = m.top.content.getChildren(-1, 0)

    for each section in homeSections
        if LCase(section.title).Replace(" ", "") = searchSectionTitle
            return true
        end if
    end for

    return false
end function

' getSectionIndex: Returns index of requested section in home row content
'
' @param {string} sectionTitle - Title of section we're checking for
'
' @return {integer} indicating index of request section
function getSectionIndex(sectionTitle as string) as integer
    if not isValid(sectionTitle) then return false
    if not isValid(m.top.content) then return false

    searchSectionTitle = LCase(sectionTitle).Replace(" ", "")

    homeSections = m.top.content.getChildren(-1, 0)

    sectionIndex = homeSections.count()
    i = 0

    for each section in homeSections
        if LCase(section.title).Replace(" ", "") = searchSectionTitle
            sectionIndex = i
            exit for
        end if
        i++
    end for

    return sectionIndex
end function

' createLiveTVRow: Creates a row displaying the live tv now on section
'
sub createLiveTVRow()
    sectionName = tr("On Now")

    if not sectionExists(sectionName)
        nextUpRow = m.top.content.CreateChild("HomeRow")
        nextUpRow.title = sectionName
        nextUpRow.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
        nextUpRow.cursorSize = homeRowItemSizes.WIDE_POSTER
    end if

    m.LoadOnNowTask.observeField("content", "updateOnNowItems")
    m.LoadOnNowTask.control = "RUN"
end sub

' createContinueWatchingRow: Creates a row displaying items the user can continue watching
'
sub createContinueWatchingRow()
    ' Load the Continue Watching Data
    m.LoadContinueWatchingTask.observeField("content", "updateContinueWatchingItems")
    m.LoadContinueWatchingTask.control = "RUN"
end sub

' createNextUpRow: Creates a row displaying next episodes up to watch
'
sub createNextUpRow()
    sectionName = tr("Next Up") + ">"

    if not sectionExists(sectionName)
        nextUpRow = m.top.content.CreateChild("HomeRow")
        nextUpRow.title = sectionName
        nextUpRow.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
        nextUpRow.cursorSize = homeRowItemSizes.WIDE_POSTER
    end if

    ' Load the Next Up Data
    m.LoadNextUpTask.observeField("content", "updateNextUpItems")
    m.LoadNextUpTask.control = "RUN"
end sub

' createFavoritesRow: Creates a row displaying items from the user's favorites list
'
sub createFavoritesRow()
    ' Load the Favorites Data
    m.LoadFavoritesTask.observeField("content", "updateFavoritesItems")
    m.LoadFavoritesTask.control = "RUN"
end sub

' updateHomeRows: Update function exposed to outside components
'
sub updateHomeRows()
    ' Hide the row counter to prevent flicker. We'll show it once loading timer fires
    m.top.showRowCounter = [false]
    m.top.visible = false
    updateSize()
    processUserSections()
end sub

' updateFavoritesItems: Processes LoadFavoritesTask content. Removes, Creates, or Updates favorites row as needed
'
sub updateFavoritesItems()
    m.processedRowCount++
    itemData = m.LoadFavoritesTask.content
    m.LoadFavoritesTask.unobserveField("content")
    m.LoadFavoritesTask.content = []

    sectionName = tr("Favorites")

    if not isValidAndNotEmpty(itemData)
        removeHomeSection(sectionName)
        return
    end if

    ' remake row using the new data
    row = CreateObject("roSGNode", "HomeRow")
    row.title = sectionName
    row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
    row.cursorSize = homeRowItemSizes.WIDE_POSTER

    for each item in itemData
        usePoster = true

        if lcase(item.type) = "episode" or lcase(item.type) = "audio" or lcase(item.type) = "musicartist"
            usePoster = false
        end if

        item.usePoster = usePoster
        item.imageWidth = row.imageWidth
        row.appendChild(item)
    end for

    if sectionExists(sectionName)
        m.top.content.replaceChild(row, getSectionIndex(sectionName))
        setRowItemSize()
        return
    end if

    m.top.content.insertChild(row, getSectionIndex(sectionName))
    setRowItemSize()
end sub

' updateContinueWatchingItems: Processes LoadContinueWatchingTask content. Removes, Creates, or Updates continue watching row as needed
'
sub updateContinueWatchingItems()
    m.processedRowCount++
    itemData = m.LoadContinueWatchingTask.content
    m.LoadContinueWatchingTask.unobserveField("content")
    m.LoadContinueWatchingTask.content = []

    sectionName = tr("Continue Watching")

    if not isValidAndNotEmpty(itemData)
        removeHomeSection(sectionName)
        return
    end if

    ' remake row using the new data
    row = CreateObject("roSGNode", "HomeRow")
    row.title = sectionName
    row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
    row.cursorSize = homeRowItemSizes.WIDE_POSTER

    for each item in itemData
        if isValid(item.json) and isValid(item.json.UserData) and isValid(item.json.UserData.PlayedPercentage)
            item.PlayedPercentage = item.json.UserData.PlayedPercentage
        end if

        item.usePoster = row.usePoster
        item.imageWidth = row.imageWidth
        row.appendChild(item)
    end for

    ' Row already exists, replace it with new content
    if sectionExists(sectionName)
        m.top.content.replaceChild(row, getSectionIndex(sectionName))
        setRowItemSize()
        return
    end if

    ' Row does not exist, insert it into the home view
    m.top.content.insertChild(row, getOriginalSectionIndex("resume"))
    setRowItemSize()
end sub

' updateNextUpItems: Processes LoadNextUpTask content. Removes, Creates, or Updates next up row as needed
'
sub updateNextUpItems()
    m.processedRowCount++
    itemData = m.LoadNextUpTask.content
    m.LoadNextUpTask.unobserveField("content")
    m.LoadNextUpTask.content = []
    m.LoadNextUpTask.control = "STOP"

    sectionName = tr("Next Up") + " >"

    if not isValidAndNotEmpty(itemData)
        removeHomeSection(sectionName)
        return
    end if

    ' remake row using the new data
    row = CreateObject("roSGNode", "HomeRow")
    row.title = tr("Next Up") + " >"
    row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
    row.cursorSize = homeRowItemSizes.WIDE_POSTER

    for each item in itemData
        item.usePoster = row.usePoster
        item.imageWidth = row.imageWidth
        row.appendChild(item)
    end for

    ' Row already exists, replace it with new content
    if sectionExists(sectionName)
        m.top.content.replaceChild(row, getSectionIndex(sectionName))
        setRowItemSize()
        return
    end if

    ' Row does not exist, insert it into the home view
    m.top.content.insertChild(row, getSectionIndex(sectionName))
    setRowItemSize()
end sub

' updateLatestItems: Processes LoadItemsTask content. Removes, Creates, or Updates latest in {library} row as needed
'
' @param {dynamic} msg - LoadItemsTask
sub updateLatestItems(msg)
    m.processedRowCount++
    itemData = msg.GetData()

    node = msg.getRoSGNode()
    node.unobserveField("content")
    node.content = []

    sectionName = tr("Latest in") + " " + node.metadata.title + " >"

    if not isValidAndNotEmpty(itemData)
        removeHomeSection(sectionName)
        return
    end if

    imagesize = homeRowItemSizes.WIDE_POSTER

    if isValid(node.metadata.contentType)
        if LCase(node.metadata.contentType) = "movies"
            imagesize = homeRowItemSizes.MOVIE_POSTER
        else if LCase(node.metadata.contentType) = "music"
            imagesize = homeRowItemSizes.MUSIC_ALBUM
        end if
    end if

    ' remake row using new data
    row = CreateObject("roSGNode", "HomeRow")
    row.title = sectionName
    row.imageWidth = imagesize[0]
    row.cursorSize = imagesize
    row.usePoster = true

    for each item in itemData
        item.usePoster = row.usePoster
        item.imageWidth = row.imageWidth
        row.appendChild(item)
    end for

    if sectionExists(sectionName)
        ' Row already exists, replace it with new content
        m.top.content.replaceChild(row, getSectionIndex(sectionName))
        setRowItemSize()
        return
    end if

    m.top.content.insertChild(row, getOriginalSectionIndex("latestmedia"))
    setRowItemSize()
end sub

' updateOnNowItems: Processes LoadOnNowTask content. Removes, Creates, or Updates latest in on now row as needed
'
sub updateOnNowItems()
    m.processedRowCount++
    itemData = m.LoadOnNowTask.content
    m.LoadOnNowTask.unobserveField("content")
    m.LoadOnNowTask.content = []

    sectionName = tr("On Now")

    if not isValidAndNotEmpty(itemData)
        removeHomeSection(sectionName)
        return
    end if

    ' remake row using the new data
    row = CreateObject("roSGNode", "HomeRow")
    row.title = tr("On Now")
    row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
    row.cursorSize = homeRowItemSizes.WIDE_POSTER

    for each item in itemData
        row.usePoster = false

        if (not isValid(item.thumbnailURL) or item.thumbnailURL = "") and isValid(item.json) and isValid(item.json.imageURL)
            item.thumbnailURL = item.json.imageURL
            row.usePoster = true
            row.imageWidth = homeRowItemSizes.MOVIE_POSTER[0]
            row.cursorSize = homeRowItemSizes.MOVIE_POSTER
        end if

        item.usePoster = row.usePoster
        item.imageWidth = row.imageWidth
        row.appendChild(item)
    end for

    ' Row already exists, replace it with new content
    if sectionExists(sectionName)
        m.top.content.replaceChild(row, getSectionIndex(sectionName))
        setRowItemSize()
        return
    end if

    ' Row does not exist, insert it into the home view
    m.top.content.insertChild(row, getOriginalSectionIndex("livetv"))
    setRowItemSize()
end sub

sub itemSelected()
    m.selectedRowItem = m.top.rowItemSelected

    m.global.launchSource = "home"
    m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1])

    'Prevent the selected item event from double firing
    m.top.selectedItem = invalid
end sub

function onKeyEvent(key as string, press as boolean) as boolean
    if press
        if key = "play"
            print "play was pressed from homerow"
            itemToPlay = m.top.content.getChild(m.top.rowItemFocused[0]).getChild(m.top.rowItemFocused[1])
            if isValid(itemToPlay)
                m.top.quickPlayNode = itemToPlay
            end if
            return true
        else if key = "replay"
            m.top.jumpToRowItem = [m.top.rowItemFocused[0], 0]
            return true
        end if
    end if
    return false
end function

function filterNodeArray(nodeArray as object, nodeKey as string, excludeArray as object) as object
    if excludeArray.IsEmpty() then return nodeArray

    newNodeArray = []
    for each node in nodeArray
        excludeThisNode = false
        for each exclude in excludeArray
            if node[nodeKey] = exclude
                excludeThisNode = true
            end if
        end for
        if excludeThisNode = false
            newNodeArray.Push(node)
        end if
    end for
    return newNodeArray
end function