import "pkg:/source/api/ApiClient.bs"
function LoginFlow()
'Collect Jellyfin server and user information
start_login:
serverUrl = get_setting("server")
if isValid(serverUrl)
print "Previous server connection saved to registry"
' Pass originalUrl to preserve user's input for re-discovery on each connection
startOver = not server.UpdateURL(serverUrl, serverUrl)
if startOver
print "Could not connect to previously saved server."
end if
else
startOver = true
print "No previous server connection saved to registry"
end if
invalidServer = true
if not startOver
m.scene.isLoading = true
invalidServer = ServerInfo().Error
m.scene.isLoading = false
end if
m.serverSelection = "Saved"
' Always ensure server select is on the stack before user select (for back button)
if startOver or invalidServer
' Need to show server select interactively
print "Get server details"
SendPerformanceBeacon("AppDialogInitiate") ' Roku Performance monitoring - Dialog Starting
m.serverSelection = CreateServerGroup()
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
if m.serverSelection = "backPressed"
print "backPressed"
m.global.sceneManager.callFunc("clearScenes")
return false
end if
SaveServerList()
else
' Server is valid - push placeholder to maintain consistent stack depth
' This ensures the scene stack has the same depth regardless of whether
' server selection UI was shown, preventing stack corruption on cleanup
' Using Group because it has a visible field (ContentNode does not)
print "Server valid, pushing placeholder to stack"
placeholderNode = CreateObject("roSGNode", "Group")
placeholderNode.visible = false
m.global.sceneManager.callFunc("pushScene", placeholderNode)
end if
localUser = m.global.user
activeUser = get_setting("active_user")
if not isValid(activeUser)
print "No active user found in registry"
user_select:
SendPerformanceBeacon("AppDialogInitiate") ' Roku Performance monitoring - Dialog Starting
publicUsers = GetPublicUsers()
numPubUsers = 0
if isValid(publicUsers) then numPubUsers = publicUsers.count()
savedUsers = getSavedUsers()
numSavedUsers = savedUsers.count()
if numPubUsers > 0 or numSavedUsers > 0
publicUsersNodes = []
publicUserIds = []
' load public users
if numPubUsers > 0
for each item in publicUsers
userData = CreateObject("roSGNode", "PublicUserData")
userData.id = item.Id
userData.name = item.Name
if isValidAndNotEmpty(item.PrimaryImageTag)
userData.ImageURL = UserImageURL(userData.id, { "tag": item.PrimaryImageTag })
end if
publicUsersNodes.push(userData)
publicUserIds.push(userData.id)
end for
end if
' load saved users for this server id
if numSavedUsers > 0
for each savedUser in savedUsers
if isValid(savedUser.serverId) and savedUser.serverId = m.global.server.id
' only show unique userids on screen.
if not arrayHasValue(publicUserIds, savedUser.Id)
userData = CreateObject("roSGNode", "PublicUserData")
userData.id = savedUser.Id
if isValid(savedUser.username)
userData.name = savedUser.username
end if
if isValidAndNotEmpty(savedUser.primaryImageTag)
userData.ImageURL = UserImageURL(userData.id, { "tag": savedUser.primaryImageTag })
end if
publicUsersNodes.push(userData)
end if
end if
end for
end if
' push all users to the user select view
userSelected = CreateUserSelectGroup(publicUsersNodes)
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
if userSelected = "backPressed"
' User wants to change server - clear all scenes and restart
server.Delete()
unset_setting("server")
m.global.sceneManager.callFunc("clearScenes")
goto start_login
else if userSelected <> ""
startLoadingSpinner()
print "A public user was selected with username=" + userSelected
localUser.name = userSelected
' save userid to session
for each userData in publicUsersNodes
if userData.name = userSelected
localUser.id = userData.id
exit for
end if
end for
' try to login with token from registry
myToken = get_user_setting("authToken")
if isValid(myToken)
' check if token is valid
print "Auth token found in registry for selected user"
localUser.authToken = myToken
print "Attempting to use API with auth token"
currentUser = AboutMe()
if not isValid(currentUser)
print "Auth token is no longer valid - deleting token"
unset_user_setting("authToken")
unset_user_setting("username")
unset_user_setting("primaryImageTag")
else
print "Success! Auth token is still valid"
user.Login(currentUser, true)
return true
end if
else
print "No auth token found in registry for selected user"
end if
'Try to login without password. If the token is valid, we're done
print "Attempting to login with no password"
userData = get_token(userSelected, "")
if isValid(userData)
print "login success!"
user.Login(userData, true)
return true
else
print "Auth failed. Password required"
end if
end if
else
userSelected = ""
end if
stopLoadingSpinner()
passwordEntry = CreateSigninGroup(userSelected)
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
if passwordEntry = "backPressed"
if numPubUsers > 0
goto user_select
else
server.Delete()
unset_setting("server")
goto start_login
end if
end if
else
print "Active user found in registry"
localUser.id = activeUser
myUsername = get_user_setting("username")
myAuthToken = get_user_setting("authToken")
myPrimaryImageTag = get_user_setting("primaryImageTag")
if isValid(myAuthToken) and isValid(myUsername)
print "Auth token found in registry"
localUser.authToken = myAuthToken
localUser.name = myUsername
if isValidAndNotEmpty(myPrimaryImageTag)
localUser.primaryImageTag = myPrimaryImageTag
end if
print "Attempting to use API with auth token"
currentUser = AboutMe()
if not isValid(currentUser)
print "Auth token is no longer valid"
'Try to login without password. If the token is valid, we're done
print "Attempting to login with no password"
userData = get_token(myUsername, "")
if isValid(userData)
print "login success!"
user.Login(userData, true)
return true
else
print "Auth failed. Password required"
print "delete token and restart login flow"
unset_user_setting("authToken")
unset_user_setting("username")
if isValid(myPrimaryImageTag)
unset_user_setting("primaryImageTag")
end if
goto start_login
end if
else
print "Success! Auth token is still valid"
user.Login(currentUser, true)
end if
else
print "No auth token found in registry"
end if
end if
if not isValid(localUser.id) or not isValid(localUser.authToken)
print "Login failed, restart flow"
unset_setting("active_user")
user.Logout()
goto start_login
end if
m.global.sceneManager.callFunc("clearScenes")
return true
end function
sub SaveServerList()
' Save this server to the list of previously-used servers shown in the server picker.
' baseUrl: canonical URL (lowercase) — used for deduplication against SSDP-discovered servers.
' originalUrl: user-entered URL from registry — shown in the picker and pre-filled on re-selection,
' so inferServerUrl() can re-discover the correct protocol on each connection.
' id: Jellyfin server ID — primary deduplication key, robust to URL changes (e.g. HTTP→HTTPS).
globalServer = m.global.server
serverUrl = globalServer.serverUrl
serverId = globalServer.id
serverName = globalServer.name
originalUrl = get_setting("server") ' set correctly by server.UpdateURL() before this is called
if isValid(serverUrl)
serverUrl = LCase(serverUrl) ' canonical URL always lowercase for comparison
end if
if not isValidAndNotEmpty(originalUrl)
originalUrl = serverUrl ' fallback: use canonical if original is somehow missing
end if
savedServers = { serverList: [] }
saved = get_setting("saved_servers")
if isValid(saved)
parsed = ParseJson(saved)
if isValid(parsed) and isValid(parsed.serverList)
savedServers = parsed
end if
end if
' Check for an existing entry (ID-based first; URL fallback for old entries without id)
for i = 0 to savedServers.serverList.Count() - 1
item = savedServers.serverList[i]
isMatch = false
if isValidAndNotEmpty(serverId) and isValidAndNotEmpty(item.id)
isMatch = (item.id = serverId)
else if LCase(item.baseUrl) = serverUrl
isMatch = true
end if
if isMatch
' Update in-place: refresh mutable server identity fields (name, id, baseUrl, originalUrl).
' iconUrl/iconWidth/iconHeight are static app branding defaults and are not changed.
savedServers.serverList[i].name = serverName
savedServers.serverList[i].id = serverId
savedServers.serverList[i].baseUrl = serverUrl ' keep canonical URL current (e.g. HTTP→HTTPS)
savedServers.serverList[i].originalUrl = originalUrl
set_setting("saved_servers", FormatJson(savedServers))
return
end if
end for
' No existing entry found — append a new one
savedServers.serverList.Push({
name: serverName,
id: serverId,
baseUrl: serverUrl,
originalUrl: originalUrl,
iconUrl: "pkg:/images/branding/logo-icon120.jpg",
iconWidth: 120,
iconHeight: 120
})
set_setting("saved_servers", FormatJson(savedServers))
end sub
sub DeleteFromServerList(idOrUrl as string)
' idOrUrl should be the server's id when available (passed from itemToDelete.id).
' Falls back to a canonical baseUrl for legacy entries that predate the id field.
' ID match is tried first so deletion is correct even when the saved entry's baseUrl
' differs from the picker item's baseUrl (e.g. a saved HTTPS entry matched via SSDP
' on HTTP — the picker item carries the SSDP baseUrl, not the saved one).
saved = get_setting("saved_servers")
if not isValid(saved) then return
savedServers = ParseJson(saved)
newServers = { serverList: [] }
normalizedInput = LCase(idOrUrl) ' for URL fallback comparison (baseUrls are always lowercase)
for each item in savedServers.serverList
keepEntry = true
if isValidAndNotEmpty(item.id) and item.id = idOrUrl
keepEntry = false ' ID match — remove this entry
else if item.baseUrl = normalizedInput
keepEntry = false ' URL fallback — remove this entry
end if
if keepEntry
newServers.serverList.Push(item)
end if
end for
set_setting("saved_servers", FormatJson(newServers))
end sub
' Roku Performance monitoring
sub SendPerformanceBeacon(signalName as string)
if m.global.appLoaded = false
m.scene.signalBeacon(signalName)
end if
end sub
function CreateServerGroup()
globalServer = m.global.server
' Capture the URL before any updates to detect changes during discovery/redirects
previousServerUrl = globalServer.serverUrl
screen = CreateObject("roSGNode", "SetServerScreen")
screen.optionsAvailable = true
m.global.sceneManager.callFunc("pushScene", screen)
port = CreateObject("roMessagePort")
if isValid(globalServer.serverUrl)
screen.serverUrl = globalServer.serverUrl
end if
buttons = screen.findNode("buttons")
buttons.observeField("buttonSelected", port)
'create delete saved server option
new_options = []
sidepanel = screen.findNode("options")
opt = CreateObject("roSGNode", "OptionsButton")
opt.title = tr("Delete Saved")
opt.id = "delete_saved"
opt.observeField("optionSelected", port)
new_options.push(opt)
sidepanel.options = new_options
sidepanel.observeField("closeSidePanel", port)
screen.observeField("backPressed", port)
while true
msg = wait(0, port)
print type(msg), msg
if type(msg) = "roSGScreenEvent" and msg.isScreenClosed()
return "false"
else if isNodeEvent(msg, "backPressed")
return "backPressed"
else if isNodeEvent(msg, "closeSidePanel")
screen.setFocus(true)
serverPicker = screen.findNode("serverPicker")
serverPicker.setFocus(true)
else if type(msg) = "roSGNodeEvent"
nodeName = msg.getNode()
' print "roSGNodeEvent: msg.getNode() =", msg.getNode()
' print "roSGNodeEvent: msg.getData() =", msg.getData()
' print "roSGNodeEvent: msg.getField() =", msg.getField()
' print "roSGNodeEvent: msg.getRoSGNode() =", msg.getRoSGNode()
' print "roSGNodeEvent: msg.getInfo() =", msg.getInfo()
if nodeName = "buttons"
buttonIndex = msg.getData()
buttonGroup = msg.getRoSGNode()
buttonSelected = buttonGroup.getChild(buttonIndex)
if buttonSelected.id = "submit"
m.scene.isLoading = true
originalUrl = screen.serverUrl
serverUrl = inferServerUrl(originalUrl)
' Pass originalUrl so it gets persisted (not the canonical redirect URL)
' This allows re-discovery on each connection for multi-network access
isConnected = server.UpdateURL(serverUrl, originalUrl)
serverInfoResult = invalid
if isConnected
serverInfoResult = ServerInfo()
'If this is a different server from what we know, reset username/password setting
' Compare using canonical URL from global state against pre-connection URL
canonicalUrl = m.global.server.serverUrl
if previousServerUrl <> canonicalUrl
set_setting("username", "")
set_setting("password", "")
end if
end if
m.scene.isLoading = false
if isConnected = false or not isValid(serverInfoResult) or (isValid(serverInfoResult.Error) and serverInfoResult.Error)
' Maybe don't unset setting, but offer as a prompt
' Server not found, is it online? New values / Retry
print "Server not found, is it online? New values / Retry"
screen.errorMessage = tr("Server not found, is it online?")
SignOut(false)
else
screen.visible = false
if isValid(serverInfoResult.serverName)
return serverInfoResult.ServerName + " (Saved)"
else
return "Saved"
end if
end if
end if
else if nodeName = "delete_saved"
serverPicker = screen.findNode("serverPicker")
itemToDelete = serverPicker.content.getChild(serverPicker.itemFocused)
' Prefer id for deletion — robust when the picker item's baseUrl differs from
' the saved entry's baseUrl (e.g. SSDP HTTP item merged from an HTTPS saved entry)
idOrUrl = itemToDelete.id
if not isValidAndNotEmpty(idOrUrl)
idOrUrl = itemToDelete.baseUrl
end if
if isValidAndNotEmpty(idOrUrl)
DeleteFromServerList(idOrUrl)
serverPicker.content.removeChild(itemToDelete)
sidepanel.visible = false
serverPicker.setFocus(true)
end if
end if
end if
end while
' Just hide it when done, in case we need to come back
screen.visible = false
return ""
end function
function CreateUserSelectGroup(users = [])
if users.count() = 0
return ""
end if
group = CreateObject("roSGNode", "UserSelect")
m.global.sceneManager.callFunc("pushScene", group)
port = CreateObject("roMessagePort")
group.itemContent = users
group.findNode("userRow").observeField("userSelected", port)
group.findNode("buttons").observeField("buttonSelected", port)
group.observeField("backPressed", port)
while true
msg = wait(0, port)
' print "roSGNodeEvent: msg.getNode() =", msg.getNode()
' print "roSGNodeEvent: msg.getData() =", msg.getData()
' print "roSGNodeEvent: msg.getField() =", msg.getField()
' print "roSGNodeEvent: msg.getRoSGNode() =", msg.getRoSGNode()
' print "roSGNodeEvent: msg.getInfo() =", msg.getInfo()
if type(msg) = "roSGScreenEvent" and msg.isScreenClosed()
group.visible = false
return -1
else if isNodeEvent(msg, "backPressed")
group.visible = false
return "backPressed"
else if type(msg) = "roSGNodeEvent" and msg.getField() = "userSelected"
return msg.GetData()
else if type(msg) = "roSGNodeEvent" and msg.getField() = "buttonSelected"
' Manual Login button pressed
if msg.getData() = 0 ' button index
return ""
end if
end if
end while
' Just hide it when done, in case we need to come back
group.visible = false
return ""
end function
function CreateSigninGroup(userName = "")
' Get and Save Jellyfin user login credentials
group = CreateObject("roSGNode", "LoginScene")
m.global.sceneManager.callFunc("pushScene", group)
port = CreateObject("roMessagePort")
group.findNode("prompt").text = tr("Sign In")
config = group.findNode("configOptions")
username_field = CreateObject("roSGNode", "ConfigData")
username_field.label = tr("Username")
username_field.field = "username"
username_field.type = "string"
if userName = "" and isValid(get_setting("username"))
username_field.value = get_setting("username")
else
username_field.value = userName
end if
password_field = CreateObject("roSGNode", "ConfigData")
password_field.label = tr("Password")
password_field.field = "password"
password_field.type = "password"
registryPassword = get_setting("password")
if isValid(registryPassword)
password_field.value = registryPassword
end if
' Add checkbox for saving credentials
checkbox = group.findNode("onOff")
items = CreateObject("roSGNode", "ContentNode")
items.role = "content"
saveCheckBox = CreateObject("roSGNode", "ContentNode")
saveCheckBox.title = tr("Save Credentials?")
items.appendChild(saveCheckBox)
checkbox.content = items
quickConnect = group.findNode("quickConnect")
' Quick Connect only supported for server version 10.8+ right now...
if versionChecker(m.global.server.version, "10.8.0")
' Add option for Quick Connect
quickConnect.text = tr("Quick Connect")
quickConnect.observeField("buttonSelected", port)
else
quickConnect.visible = false
end if
items = [username_field, password_field]
config.configItems = items
buttons = group.findNode("buttons")
buttons.observeField("buttonSelected", port)
config = group.findNode("configOptions")
userName = config.content.getChild(0)
password = config.content.getChild(1)
group.observeField("backPressed", port)
while true
msg = wait(0, port)
' print "roSGNodeEvent: msg.getNode() =", msg.getNode()
' print "roSGNodeEvent: msg.getData() =", msg.getData()
' print "roSGNodeEvent: msg.getField() =", msg.getField()
' print "roSGNodeEvent: msg.getRoSGNode() =", msg.getRoSGNode()
' print "roSGNodeEvent: msg.getInfo() =", msg.getInfo()
if type(msg) = "roSGScreenEvent" and msg.isScreenClosed()
group.visible = false
return "false"
else if isNodeEvent(msg, "backPressed")
group.unobserveField("backPressed")
group.backPressed = false
return "backPressed"
else if type(msg) = "roSGNodeEvent"
nodeName = msg.getNode()
if nodeName = "buttons"
buttonIndex = msg.getData()
buttonGroup = msg.getRoSGNode()
buttonSelected = buttonGroup.getChild(buttonIndex)
if buttonSelected.id = "submit"
startLoadingSpinner()
' Validate credentials
activeUser = get_token(userName.value, password.value)
if isValid(activeUser)
print "activeUser=", activeUser
if checkbox.checkedState[0] = true
' save credentials
user.Login(activeUser, true)
set_user_setting("authToken", activeUser.token)
set_user_setting("username", userName.value)
if isValidAndNotEmpty(activeUser.json.PrimaryImageTag)
set_user_setting("primaryImageTag", activeUser.json.PrimaryImageTag)
end if
else
user.Login(activeUser)
end if
return "true"
end if
stopLoadingSpinner()
print "Login attempt failed..."
group.findNode("alert").text = tr("Login attempt failed.")
else if buttonSelected.id = "quickConnect"
json = initQuickConnect()
if not isValid(json)
group.findNode("alert").text = tr("Quick Connect not available.")
else
' Server user is talking to is at least 10.8 and has quick connect enabled...
m.quickConnectDialog = createObject("roSGNode", "QuickConnectDialog")
m.quickConnectDialog.saveCredentials = checkbox.checkedState[0]
m.quickConnectDialog.quickConnectJson = json
m.quickConnectDialog.title = tr("Quick Connect")
m.quickConnectDialog.message = [tr("Here is your Quick Connect code: ") + json.Code, tr("(Dialog will close automatically)")]
m.quickConnectDialog.buttons = [tr("Cancel")]
m.quickConnectDialog.observeField("authenticated", port)
m.scene.dialog = m.quickConnectDialog
end if
end if
if msg.getField() = "authenticated"
authenticated = msg.getData()
if authenticated = true
' Quick connect authentication was successful...
return "true"
else
dialog = createObject("roSGNode", "Dialog")
dialog.id = "QuickConnectError"
dialog.title = tr("Quick Connect")
dialog.buttons = [tr("OK")]
dialog.message = tr("There was an error authenticating via Quick Connect.")
m.scene.dialog = dialog
m.scene.dialog.observeField("buttonSelected", port)
end if
else
' If there are no other button matches, check if this is a simple "OK" Dialog & Close if so
dialog = msg.getRoSGNode()
if dialog.id = "QuickConnectError"
dialog.unobserveField("buttonSelected")
dialog.close = true
end if
end if
end if
end if
end while
' Just hide it when done, in case we need to come back
group.visible = false
return ""
end function
function CreateHomeGroup()
' Main screen after logging in. Shows the user's libraries
group = CreateObject("roSGNode", "Home")
group.overhangTitle = tr("Home")
group.optionsAvailable = true
group.observeField("selectedItem", m.port)
group.observeField("quickPlayNode", m.port)
sidepanel = group.findNode("options")
sidepanel.observeField("closeSidePanel", m.port)
new_options = []
o = CreateObject("roSGNode", "OptionsButton")
o.title = tr("Search")
o.id = "goto_search"
o.observeField("optionSelected", m.port)
new_options.push(o)
o = invalid
' Add settings option to menu
o = CreateObject("roSGNode", "OptionsButton")
o.title = tr("Settings")
o.id = "settings"
o.observeField("optionSelected", m.port)
new_options.push(o)
o = invalid
o = CreateObject("roSGNode", "OptionsButton")
o.title = tr("Change user")
o.id = "change_user"
o.observeField("optionSelected", m.port)
new_options.push(o)
o = invalid
o = CreateObject("roSGNode", "OptionsButton")
o.title = tr("Change server")
o.id = "change_server"
o.observeField("optionSelected", m.port)
new_options.push(o)
o = invalid
o = CreateObject("roSGNode", "OptionsButton")
o.title = tr("Sign out")
o.id = "sign_out"
o.observeField("optionSelected", m.port)
new_options.push(o)
sidepanel.options = new_options
return group
end function
function CreateItemDetailsGroup(item as object) as dynamic
if not isValid(item) or not isValid(item.id) then return invalid
startLoadingSpinner()
itemData = ItemDetailsMetaData(item.id)
if not isValid(itemData)
stopLoadingSpinner()
return invalid
end if
group = CreateObject("roSGNode", "ItemDetails")
group.observeField("quickPlayNode", m.port)
group.observeField("refreshItemDetailsData", m.port)
group.overhangTitle = ""
group.optionsAvailable = false
group.trailerAvailable = false
' Push scene asap to prevent extra button presses while fetching data
m.global.sceneManager.callFunc("pushScene", group)
group.itemContent = itemData
' Type-specific post-load setup
if item.type = "Movie" or item.type = "Video" or item.type = "MusicVideo"
trailerData = GetApi().GetLocalTrailers(item.id)
if isValid(trailerData)
group.trailerAvailable = trailerData.Count() > 0
end if
end if
' Watch for button presses
buttons = group.findNode("buttons")
buttons.observeField("buttonSelected", m.port)
' Set up and load item extras
extras = group.findNode("extrasGrid")
extras.observeField("selectedItem", m.port)
' type and loadParts are called once by itemContentChanged() in ItemDetails.bs on first content load,
' and again on explicit refresh via the refreshExtrasData field toggle.
stopLoadingSpinner()
return group
end function
' Shows details on selected artist. Bio, image, and list of available albums
function CreateArtistView(artist as object) as dynamic
' validate artist node
if not isValid(artist) or not isValid(artist.id) then return invalid
musicData = MusicAlbumList(artist.id)
appearsOnData = AppearsOnList(artist.id)
if (not isValid(musicData) or musicData.Items.Count() = 0) and (not isValid(appearsOnData) or appearsOnData.Items.Count() = 0)
' Just songs under artists...
group = CreateObject("roSGNode", "AlbumView")
group.pageContent = ItemMetaData(artist.id)
' Lookup songs based on artist id
songList = GetSongsByArtist(artist.id)
if not isValid(songList)
' Lookup songs based on folder parent / child relationship
songList = MusicSongList(artist.id)
end if
if not isValid(songList)
return invalid
end if
group.albumData = songList
group.observeField("playSong", m.port)
group.observeField("playAllSelected", m.port)
group.observeField("instantMixSelected", m.port)
else
' User has albums under artists
group = CreateObject("roSGNode", "ArtistView")
group.pageContent = ItemMetaData(artist.id)
group.musicArtistAlbumData = musicData
group.musicArtistAppearsOnData = appearsOnData
group.artistOverview = ArtistOverview(artist.name)
group.observeField("musicAlbumSelected", m.port)
group.observeField("playArtistSelected", m.port)
group.observeField("instantMixSelected", m.port)
group.observeField("appearsOnSelected", m.port)
end if
group.observeField("quickPlayNode", m.port)
m.global.sceneManager.callFunc("pushScene", group)
return group
end function
' Shows details on selected album. Description text, image, and list of available songs
function CreateAlbumView(album as object) as dynamic
' validate album node
if not isValid(album) or not isValid(album.id) then return invalid
group = CreateObject("roSGNode", "AlbumView")
m.global.sceneManager.callFunc("pushScene", group)
group.pageContent = ItemMetaData(album.id)
group.albumData = MusicSongList(album.id)
' Watch for user clicking on a song
group.observeField("playSong", m.port)
buttons = group.findNode("buttons")
buttons.observeField("buttonSelected", m.port)
return group
end function
' Shows details on selected playlist. Description text, image, and list of available items
function CreatePlaylistView(playlist as object) as dynamic
' validate playlist node
if not isValid(playlist) or not isValid(playlist.id) then return invalid
group = CreateObject("roSGNode", "PlaylistView")
m.global.sceneManager.callFunc("pushScene", group)
group.pageContent = ItemMetaData(playlist.id)
group.albumData = PlaylistItemList(playlist.id)
' Watch for user clicking on an item
group.observeField("playItem", m.port)
buttons = group.findNode("buttons")
buttons.observeField("buttonSelected", m.port)
return group
end function
function CreateSearchPage()
' Search + Results Page
group = CreateObject("roSGNode", "searchResults")
group.observeField("quickPlayNode", m.port)
options = group.findNode("searchSelect")
options.observeField("itemSelected", m.port)
return group
end function
'Opens dialog asking user if they want to resume video or start playback over only on the home screen
sub playbackOptionDialog(time as longinteger)
resumeData = [
tr("Resume playing at ") + ticksToHuman(time) + ".",
tr("Start over from the beginning.")
]
stopLoadingSpinner()
m.global.sceneManager.callFunc("optionDialog", tr("Playback Options"), [], resumeData)
end sub