2023-11-11 13:41:20 +00:00
|
|
|
<!DOCTYPE html><html lang="en" style="font-size:16px"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Source: components/home/HomeRows.bs</title><!--[if lt IE 9]>
|
|
|
|
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
2023-12-05 16:56:00 +00:00
|
|
|
<![endif]--><script src="scripts/third-party/hljs.js" defer="defer"></script><script src="scripts/third-party/hljs-line-num.js" defer="defer"></script><script src="scripts/third-party/popper.js" defer="defer"></script><script src="scripts/third-party/tippy.js" defer="defer"></script><script src="scripts/third-party/tocbot.min.js"></script><script>var baseURL="/",locationPathname="";baseURL=(baseURL=(baseURL="https://jellyfin.github.io/jellyfin-roku/").replace(/https?:\/\//i,"")).substr(baseURL.indexOf("/"))</script><link rel="stylesheet" href="styles/clean-jsdoc-theme.min.css"><svg aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none"><defs><symbol id="copy-icon" viewbox="0 0 488.3 488.3"><g><path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/><path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z"/></g></symbol><symbol id="search-icon" viewBox="0 0 512 512"><g><g><path d="M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z"/></g></g><g><g><path d="M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z"/></g></g></symbol><symbol id="font-size-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol id="add-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></symbol><symbol id="minus-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11h14v2H5z"/></symbol><symbol id="dark-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol><symbol id="light-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol><symbol id="reset-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"/></symbol><symbol id="down-icon" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z"></path></symbol><symbol id="codepen-icon" viewBox="0 0 24 24"><path fill="none" d=
|
|
|
|
import "pkg:/source/constants/HomeRowItemSizes.bs"
|
|
|
|
|
|
|
|
const LOADING_WAIT_TIME = 2
|
2023-10-06 03:18:36 +00:00
|
|
|
|
|
|
|
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]
|
2023-12-05 16:56:00 +00:00
|
|
|
' 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")
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
m.loadingTimer = createObject("roSGNode", "Timer")
|
|
|
|
m.loadingTimer.duration = LOADING_WAIT_TIME
|
|
|
|
m.loadingTimer.observeField("fire", "loadingTimerComplete")
|
2023-10-27 02:20:25 +00:00
|
|
|
|
2023-10-06 03:18:36 +00:00
|
|
|
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")
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' set up task nodes for other rows
|
2023-10-27 02:20:25 +00:00
|
|
|
m.LoadContinueWatchingTask = createObject("roSGNode", "LoadItemsTask")
|
|
|
|
m.LoadContinueWatchingTask.itemsToLoad = "continue"
|
2023-10-06 03:18:36 +00:00
|
|
|
|
|
|
|
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()
|
|
|
|
m.top.translation = [111, 180]
|
|
|
|
itemHeight = 330
|
|
|
|
|
|
|
|
'Set width of Rows to cut off at edge of Safe Zone
|
|
|
|
m.top.itemSize = [1703, itemHeight]
|
|
|
|
|
|
|
|
' spacing between rows
|
|
|
|
m.top.itemSpacing = [0, 105]
|
|
|
|
|
|
|
|
' spacing between items in a row
|
|
|
|
m.top.rowItemSpacing = [20, 0]
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' Default size to wide poster, the most used size
|
|
|
|
m.top.rowItemSize = homeRowItemSizes.WIDE_POSTER
|
|
|
|
|
2023-10-06 03:18:36 +00:00
|
|
|
m.top.visible = true
|
|
|
|
end sub
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' 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
|
2023-10-27 02:20:25 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' calculate expected row count by processing homesections
|
|
|
|
for i = 0 to 6
|
|
|
|
sectionName = LCase(m.global.session.user.settings["homesection" + i.toStr()])
|
|
|
|
if sectionName = "latestmedia"
|
|
|
|
' expect 1 row per filtered media library
|
|
|
|
m.filteredLatest = filterNodeArray(m.libraryData, "id", m.global.session.user.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
|
2023-10-27 02:20:25 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' Add home sections in order based on user settings
|
|
|
|
loadedSections = 0
|
2023-10-27 02:20:25 +00:00
|
|
|
for i = 0 to 6
|
|
|
|
sectionName = LCase(m.global.session.user.settings["homesection" + i.toStr()])
|
2023-12-05 16:56:00 +00:00
|
|
|
sectionLoaded = false
|
|
|
|
if sectionName <> "none"
|
|
|
|
sectionLoaded = addHomeSection(sectionName)
|
|
|
|
end if
|
2023-10-27 02:20:25 +00:00
|
|
|
|
|
|
|
' 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
|
2023-12-05 16:56:00 +00:00
|
|
|
if not m.global.app_loaded
|
|
|
|
if loadedSections = 2 or i = 6
|
2023-10-27 02:20:25 +00:00
|
|
|
m.top.signalBeacon("AppLaunchComplete") ' Roku Performance monitoring
|
|
|
|
m.global.app_loaded = true
|
|
|
|
end if
|
|
|
|
end if
|
|
|
|
end for
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' 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 = []
|
2023-10-27 02:20:25 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
processUserSections()
|
2023-10-27 02:20:25 +00:00
|
|
|
end sub
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' 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(" ", "")
|
2023-10-27 02:20:25 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
sectionIndex = 0
|
|
|
|
indexLatestMediaSection = 0
|
2023-10-27 02:20:25 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
for i = 0 to 6
|
|
|
|
settingSectionName = LCase(m.global.session.user.settings["homesection" + i.toStr()])
|
|
|
|
if settingSectionName = "latestmedia"
|
|
|
|
indexLatestMediaSection = i
|
|
|
|
end if
|
2023-10-27 02:20:25 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
if settingSectionName = searchSectionName
|
|
|
|
sectionIndex = i
|
2023-10-27 02:20:25 +00:00
|
|
|
end if
|
|
|
|
end for
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' 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)
|
2023-10-27 02:20:25 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
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
|
2023-10-27 02:20:25 +00:00
|
|
|
end sub
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' 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
|
2023-10-27 02:20:25 +00:00
|
|
|
' Poster size library items
|
2023-12-05 16:56:00 +00:00
|
|
|
if sectionType = "livetv"
|
|
|
|
createLiveTVRow()
|
2023-10-27 02:20:25 +00:00
|
|
|
return true
|
|
|
|
end if
|
|
|
|
|
|
|
|
' Poster size library items
|
2023-12-05 16:56:00 +00:00
|
|
|
if sectionType = "smalllibrarytiles"
|
|
|
|
createLibraryRow()
|
2023-10-27 02:20:25 +00:00
|
|
|
return true
|
|
|
|
end if
|
|
|
|
|
|
|
|
' Continue Watching items
|
2023-12-05 16:56:00 +00:00
|
|
|
if sectionType = "resume"
|
|
|
|
createContinueWatchingRow()
|
2023-10-27 02:20:25 +00:00
|
|
|
return true
|
|
|
|
end if
|
|
|
|
|
|
|
|
' Next Up items
|
2023-12-05 16:56:00 +00:00
|
|
|
if sectionType = "nextup"
|
|
|
|
createNextUpRow()
|
2023-10-27 02:20:25 +00:00
|
|
|
return true
|
|
|
|
end if
|
|
|
|
|
|
|
|
' Latest items in each library
|
2023-12-05 16:56:00 +00:00
|
|
|
if sectionType = "latestmedia"
|
|
|
|
createLatestInRows()
|
2023-10-27 02:20:25 +00:00
|
|
|
return true
|
|
|
|
end if
|
|
|
|
|
|
|
|
' Favorite Items
|
2023-12-05 16:56:00 +00:00
|
|
|
if sectionType = "favorites"
|
|
|
|
createFavoritesRow()
|
2023-10-27 02:20:25 +00:00
|
|
|
return true
|
|
|
|
end if
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' This section type isn't supported.
|
|
|
|
' Count it as processed since we aren't going to do anything else with it
|
|
|
|
m.processedRowCount++
|
2023-10-27 02:20:25 +00:00
|
|
|
return false
|
|
|
|
end function
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' createLibraryRow: Creates a row displaying the user's libraries
|
|
|
|
'
|
|
|
|
sub createLibraryRow()
|
|
|
|
m.processedRowCount++
|
2023-10-27 02:20:25 +00:00
|
|
|
' Ensure we have data
|
|
|
|
if not isValidAndNotEmpty(m.libraryData) then return
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
sectionName = tr("My Media")
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
row = CreateObject("roSGNode", "HomeRow")
|
|
|
|
row.title = sectionName
|
|
|
|
row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
|
|
|
|
row.cursorSize = homeRowItemSizes.WIDE_POSTER
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-10-27 02:20:25 +00:00
|
|
|
filteredMedia = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.MyMediaExcludes)
|
|
|
|
for each item in filteredMedia
|
2023-12-05 16:56:00 +00:00
|
|
|
row.appendChild(item)
|
2023-10-27 02:20:25 +00:00
|
|
|
end for
|
2023-12-05 16:56:00 +00:00
|
|
|
|
|
|
|
' 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("smalllibrarytiles"))
|
|
|
|
setRowItemSize()
|
2023-10-27 02:20:25 +00:00
|
|
|
end sub
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' createLatestInRows: Creates a row displaying latest items in each of the user's libraries
|
|
|
|
'
|
|
|
|
sub createLatestInRows()
|
2023-10-27 02:20:25 +00:00
|
|
|
' Ensure we have data
|
|
|
|
if not isValidAndNotEmpty(m.libraryData) then return
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-10-27 02:20:25 +00:00
|
|
|
' create a "Latest In" row for each library
|
2023-12-05 16:56:00 +00:00
|
|
|
for each lib in m.filteredLatest
|
2023-10-27 02:20:25 +00:00
|
|
|
if lib.collectionType <> "boxsets" and lib.collectionType <> "livetv" and lib.json.CollectionType <> "Program"
|
|
|
|
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
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' 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
|
2023-10-27 02:20:25 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
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()
|
2023-10-27 02:20:25 +00:00
|
|
|
m.LoadOnNowTask.observeField("content", "updateOnNowItems")
|
|
|
|
m.LoadOnNowTask.control = "RUN"
|
|
|
|
end sub
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' createContinueWatchingRow: Creates a row displaying items the user can continue watching
|
|
|
|
'
|
|
|
|
sub createContinueWatchingRow()
|
2023-10-06 03:18:36 +00:00
|
|
|
' Load the Continue Watching Data
|
2023-10-27 02:20:25 +00:00
|
|
|
m.LoadContinueWatchingTask.observeField("content", "updateContinueWatchingItems")
|
|
|
|
m.LoadContinueWatchingTask.control = "RUN"
|
|
|
|
end sub
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' 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
|
2023-10-27 02:20:25 +00:00
|
|
|
|
|
|
|
' Load the Next Up Data
|
|
|
|
m.LoadNextUpTask.observeField("content", "updateNextUpItems")
|
|
|
|
m.LoadNextUpTask.control = "RUN"
|
|
|
|
end sub
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' createFavoritesRow: Creates a row displaying items from the user's favorites list
|
|
|
|
'
|
|
|
|
sub createFavoritesRow()
|
2023-10-06 03:18:36 +00:00
|
|
|
' Load the Favorites Data
|
|
|
|
m.LoadFavoritesTask.observeField("content", "updateFavoritesItems")
|
|
|
|
m.LoadFavoritesTask.control = "RUN"
|
|
|
|
end sub
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' updateHomeRows: Update function exposed to outside components
|
|
|
|
'
|
2023-10-06 03:18:36 +00:00
|
|
|
sub updateHomeRows()
|
2023-12-05 16:56:00 +00:00
|
|
|
' Hide the row counter to prevent flicker. We'll show it once loading timer fires
|
|
|
|
m.top.showRowCounter = [false]
|
|
|
|
processUserSections()
|
2023-10-06 03:18:36 +00:00
|
|
|
end sub
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' updateFavoritesItems: Processes LoadFavoritesTask content. Removes, Creates, or Updates favorites row as needed
|
|
|
|
'
|
2023-10-06 03:18:36 +00:00
|
|
|
sub updateFavoritesItems()
|
2023-12-05 16:56:00 +00:00
|
|
|
m.processedRowCount++
|
2023-10-06 03:18:36 +00:00
|
|
|
itemData = m.LoadFavoritesTask.content
|
|
|
|
m.LoadFavoritesTask.unobserveField("content")
|
|
|
|
m.LoadFavoritesTask.content = []
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
sectionName = tr("Favorites")
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
if not isValidAndNotEmpty(itemData)
|
|
|
|
removeHomeSection(sectionName)
|
2023-10-27 02:20:25 +00:00
|
|
|
return
|
2023-12-05 16:56:00 +00:00
|
|
|
end if
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' remake row using the new data
|
|
|
|
row = CreateObject("roSGNode", "HomeRow")
|
|
|
|
row.title = sectionName
|
|
|
|
row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
|
|
|
|
row.cursorSize = homeRowItemSizes.WIDE_POSTER
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
for each item in itemData
|
|
|
|
usePoster = true
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
if lcase(item.type) = "episode" or lcase(item.type) = "audio" or lcase(item.type) = "musicartist"
|
|
|
|
usePoster = false
|
|
|
|
end if
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
item.usePoster = usePoster
|
|
|
|
item.imageWidth = row.imageWidth
|
|
|
|
row.appendChild(item)
|
|
|
|
end for
|
2023-10-27 02:20:25 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
if sectionExists(sectionName)
|
|
|
|
m.top.content.replaceChild(row, getSectionIndex(sectionName))
|
|
|
|
setRowItemSize()
|
|
|
|
return
|
2023-10-06 03:18:36 +00:00
|
|
|
end if
|
2023-12-05 16:56:00 +00:00
|
|
|
|
|
|
|
m.top.content.insertChild(row, getSectionIndex(sectionName))
|
|
|
|
setRowItemSize()
|
2023-10-06 03:18:36 +00:00
|
|
|
end sub
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' updateContinueWatchingItems: Processes LoadContinueWatchingTask content. Removes, Creates, or Updates continue watching row as needed
|
|
|
|
'
|
2023-10-27 02:20:25 +00:00
|
|
|
sub updateContinueWatchingItems()
|
2023-12-05 16:56:00 +00:00
|
|
|
m.processedRowCount++
|
2023-10-27 02:20:25 +00:00
|
|
|
itemData = m.LoadContinueWatchingTask.content
|
|
|
|
m.LoadContinueWatchingTask.unobserveField("content")
|
|
|
|
m.LoadContinueWatchingTask.content = []
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
sectionName = tr("Continue Watching")
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
if not isValidAndNotEmpty(itemData)
|
|
|
|
removeHomeSection(sectionName)
|
2023-10-27 02:20:25 +00:00
|
|
|
return
|
|
|
|
end if
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
sectionName = tr("Continue Watching")
|
|
|
|
|
2023-10-27 02:20:25 +00:00
|
|
|
' remake row using the new data
|
|
|
|
row = CreateObject("roSGNode", "HomeRow")
|
2023-12-05 16:56:00 +00:00
|
|
|
row.title = sectionName
|
|
|
|
row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
|
|
|
|
row.cursorSize = homeRowItemSizes.WIDE_POSTER
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-10-27 02:20:25 +00:00
|
|
|
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
|
2023-10-06 03:18:36 +00:00
|
|
|
end if
|
|
|
|
|
2023-10-27 02:20:25 +00:00
|
|
|
item.usePoster = row.usePoster
|
|
|
|
item.imageWidth = row.imageWidth
|
|
|
|
row.appendChild(item)
|
|
|
|
end for
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' 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()
|
2023-10-06 03:18:36 +00:00
|
|
|
end sub
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' updateNextUpItems: Processes LoadNextUpTask content. Removes, Creates, or Updates next up row as needed
|
|
|
|
'
|
2023-10-06 03:18:36 +00:00
|
|
|
sub updateNextUpItems()
|
2023-12-05 16:56:00 +00:00
|
|
|
m.processedRowCount++
|
2023-10-06 03:18:36 +00:00
|
|
|
itemData = m.LoadNextUpTask.content
|
|
|
|
m.LoadNextUpTask.unobserveField("content")
|
|
|
|
m.LoadNextUpTask.content = []
|
2023-12-05 16:56:00 +00:00
|
|
|
m.LoadNextUpTask.control = "STOP"
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
sectionName = tr("Next Up") + " >"
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
if not isValidAndNotEmpty(itemData)
|
|
|
|
removeHomeSection(sectionName)
|
2023-10-27 02:20:25 +00:00
|
|
|
return
|
2023-10-06 03:18:36 +00:00
|
|
|
end if
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' 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
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
for each item in itemData
|
|
|
|
item.usePoster = row.usePoster
|
|
|
|
item.imageWidth = row.imageWidth
|
|
|
|
row.appendChild(item)
|
|
|
|
end for
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' Row already exists, replace it with new content
|
|
|
|
if sectionExists(sectionName)
|
|
|
|
m.top.content.replaceChild(row, getSectionIndex(sectionName))
|
|
|
|
setRowItemSize()
|
|
|
|
return
|
|
|
|
end if
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' Row does not exist, insert it into the home view
|
|
|
|
m.top.content.insertChild(row, getSectionIndex(sectionName))
|
|
|
|
setRowItemSize()
|
2023-10-06 03:18:36 +00:00
|
|
|
end sub
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' updateLatestItems: Processes LoadItemsTask content. Removes, Creates, or Updates latest in {library} row as needed
|
|
|
|
'
|
|
|
|
' @param {dynamic} msg - LoadItemsTask
|
2023-10-06 03:18:36 +00:00
|
|
|
sub updateLatestItems(msg)
|
2023-12-05 16:56:00 +00:00
|
|
|
m.processedRowCount++
|
2023-10-06 03:18:36 +00:00
|
|
|
itemData = msg.GetData()
|
|
|
|
|
|
|
|
node = msg.getRoSGNode()
|
|
|
|
node.unobserveField("content")
|
|
|
|
node.content = []
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
sectionName = tr("Latest in") + " " + node.metadata.title + " >"
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
if not isValidAndNotEmpty(itemData)
|
2023-10-27 02:20:25 +00:00
|
|
|
removeHomeSection(sectionName)
|
|
|
|
return
|
2023-12-05 16:56:00 +00:00
|
|
|
end if
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
imagesize = homeRowItemSizes.WIDE_POSTER
|
2023-11-11 13:47:32 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
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
|
2023-11-11 13:47:32 +00:00
|
|
|
end if
|
2023-12-05 16:56:00 +00:00
|
|
|
end if
|
2023-11-11 13:47:32 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' remake row using new data
|
|
|
|
row = CreateObject("roSGNode", "HomeRow")
|
|
|
|
row.title = sectionName
|
|
|
|
row.imageWidth = imagesize[0]
|
|
|
|
row.cursorSize = imagesize
|
|
|
|
row.usePoster = true
|
2023-11-11 13:47:32 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
for each item in itemData
|
|
|
|
item.usePoster = row.usePoster
|
|
|
|
item.imageWidth = row.imageWidth
|
|
|
|
row.appendChild(item)
|
|
|
|
end for
|
2023-11-11 13:47:32 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
if sectionExists(sectionName)
|
|
|
|
' Row already exists, replace it with new content
|
|
|
|
m.top.content.replaceChild(row, getSectionIndex(sectionName))
|
|
|
|
setRowItemSize()
|
2023-11-11 13:47:32 +00:00
|
|
|
return
|
2023-10-06 03:18:36 +00:00
|
|
|
end if
|
2023-12-05 16:56:00 +00:00
|
|
|
|
|
|
|
m.top.content.insertChild(row, getOriginalSectionIndex("latestmedia"))
|
|
|
|
setRowItemSize()
|
2023-10-06 03:18:36 +00:00
|
|
|
end sub
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' updateOnNowItems: Processes LoadOnNowTask content. Removes, Creates, or Updates latest in on now row as needed
|
|
|
|
'
|
2023-10-06 03:18:36 +00:00
|
|
|
sub updateOnNowItems()
|
2023-12-05 16:56:00 +00:00
|
|
|
m.processedRowCount++
|
2023-10-06 03:18:36 +00:00
|
|
|
itemData = m.LoadOnNowTask.content
|
|
|
|
m.LoadOnNowTask.unobserveField("content")
|
|
|
|
m.LoadOnNowTask.content = []
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
sectionName = tr("On Now")
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
if not isValidAndNotEmpty(itemData)
|
|
|
|
removeHomeSection(sectionName)
|
2023-10-27 02:20:25 +00:00
|
|
|
return
|
2023-10-06 03:18:36 +00:00
|
|
|
end if
|
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
' 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
|
2023-10-06 03:18:36 +00:00
|
|
|
|
2023-12-05 16:56:00 +00:00
|
|
|
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
|
2023-10-06 03:18:36 +00:00
|
|
|
end if
|
2023-12-05 16:56:00 +00:00
|
|
|
|
|
|
|
item.usePoster = row.usePoster
|
|
|
|
item.imageWidth = row.imageWidth
|
|
|
|
row.appendChild(item)
|
2023-10-06 03:18:36 +00:00
|
|
|
end for
|
2023-12-05 16:56:00 +00:00
|
|
|
|
|
|
|
' 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()
|
2023-10-06 03:18:36 +00:00
|
|
|
end sub
|
|
|
|
|
|
|
|
sub itemSelected()
|
2023-12-05 16:56:00 +00:00
|
|
|
m.selectedRowItem = m.top.rowItemSelected
|
|
|
|
|
2023-10-06 03:18:36 +00:00
|
|
|
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"
|
2023-10-28 21:26:12 +00:00
|
|
|
print "play was pressed from homerow"
|
2023-10-06 03:18:36 +00:00
|
|
|
itemToPlay = m.top.content.getChild(m.top.rowItemFocused[0]).getChild(m.top.rowItemFocused[1])
|
2023-10-28 21:26:12 +00:00
|
|
|
if isValid(itemToPlay)
|
2023-10-06 03:18:36 +00:00
|
|
|
m.top.quickPlayNode = itemToPlay
|
|
|
|
end if
|
2023-10-28 21:26:12 +00:00
|
|
|
return true
|
|
|
|
else if key = "replay"
|
2023-10-06 03:18:36 +00:00
|
|
|
m.top.jumpToRowItem = [m.top.rowItemFocused[0], 0]
|
2023-10-28 21:26:12 +00:00
|
|
|
return true
|
2023-10-06 03:18:36 +00:00
|
|
|
end if
|
|
|
|
end if
|
2023-10-28 21:26:12 +00:00
|
|
|
return false
|
2023-10-06 03:18:36 +00:00
|
|
|
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
|
2023-12-05 16:56:00 +00:00
|
|
|
</code></pre></article></section><footer class="footer" id="PeOAagUepe"><div class="wrapper"><span class="jsdoc-message">Automatically generated using <a href="https://github.com/jsdoc/jsdoc" target="_blank">JSDoc</a> and the <a href="https://github.com/ankitskvmdam/clean-jsdoc-theme" target="_blank">clean-jsdoc-theme</a>.</span></div></footer></div></div></div><div class="search-container" id="PkfLWpAbet" style="display:none"><div class="wrapper" id="iCxFxjkHbP"><button class="icon-button search-close-button" id="VjLlGakifb" aria-label="close search"><svg><use xlink:href="#close-icon"></use></svg></button><div class="search-box-c"><svg><use xlink:href="#search-icon"></use></svg> <input type="text" id="vpcKVYIppa" class="search-input" placeholder="Search..." autofocus></div><div class="search-result-c" id="fWwVHRuDuN"><span class="search-result-c-text">Type anything to view search result</span></div></div></div><div class="mobile-menu-icon-container"><button class="icon-button" id="mobile-menu" data-isopen="false" aria-label="menu"><svg><use xlink:href="#menu-icon"></use></svg></button></div><div id="mobile-sidebar" class="mobile-sidebar-container"><div class="mobile-sidebar-wrapper"><a href="/" class="sidebar-title sidebar-title-anchor">jellyfin-roku Code Documentation</a><div class="mobile-nav-links"><div class="external-link navbar-item"><a id="jellyfin-link-mobile" href="https://jellyfin.org/" target="_blank">Jellyfin</a></div><div class="external-link navbar-item"><a id="github-link-mobile" href="https://github.com/jellyfin/jellyfin-roku" target="_blank">GitHub</a></div><div class="external-link navbar-item"><a id="forum-link-mobile" href="https://forum.jellyfin.org/f-roku-development" target="_blank">Forum</a></div><div class="external-link navbar-item"><a id="matrix-link-mobile" href="https://matrix.to/#/#jellyfin-dev-roku:matrix.org" target="_blank">Matrix</a></div></div><div class="mobile-sidebar-items-c"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-modules"><div>Modules</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="module-AlbumData.html">AlbumData</a></div><div class="sidebar-section-children"><a href="module-AlbumGrid.html">AlbumGrid</a></div><div class="sidebar-section-children"><a href="module-AlbumTrackList.html">AlbumTrackList</a></div><div class="sidebar-section-children"><a href="module-AlbumView.html">AlbumView</a></div><div class="sidebar-section-children"><a href="module-Alpha.html">Alpha</a></div><div class="sidebar-section-children"><a href="module-ArtistView.html">ArtistView</a></div><div class="sidebar-section-children"><a href="module-AudioPlayer.html">AudioPlayer</a></div><div class="sidebar-section-children"><a href="module-AudioPlayerView.html">AudioPlayerView</a></div><div class="sidebar-section-children"><a href="module-AudioTrackListItem.html">AudioTrackListItem</a></div><div class="sidebar-section-children"><a href="module-ButtonGroupHoriz.html">ButtonGroupHoriz</a></div><div class="sidebar-section-children"><a href="module-ButtonGroupVert.html">ButtonGroupVert</a></div><div class="sidebar-section-children"><a href="module-ChannelData.html">ChannelData</a></div><div class="sidebar-section-children"><a href="module-Clock.html">Clock</a></div><div class="sidebar-section-children"><a href="module-CollectionData.html">CollectionData</a></div><div class="sidebar-section-children"><a href="module-ConfigData.html">ConfigData</a></div><div class="sidebar-section-children"><a href="module-ConfigItem.html">ConfigItem</a></div><div class="sidebar-section-children"><a href="module-ConfigList.html">ConfigList</a></div><div class="sidebar-section-children"><a href="module-ExtrasItem.html">ExtrasItem</a></div><div class="sidebar-section-children"><a href="module-ExtrasRowList.html">ExtrasRowList</a></div><div class="sidebar-section-children"><a href="module-FavoriteItemsTask.html">FavoriteItemsTask</a></div><div class="sidebar-section-children"><a href="module-Folder
|