1131 lines
48 KiB
HTML
1131 lines
48 KiB
HTML
<!DOCTYPE html>
|
|
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width">
|
|
<title>jellyfin-roku api docs Source: source/ShowScenes.brs</title>
|
|
|
|
<!--[if lt IE 9]>
|
|
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
<![endif]-->
|
|
<link type="text/css" rel="stylesheet" href="styles/sunlight.dark.css">
|
|
|
|
<link type="text/css" rel="stylesheet" href="styles/site.darkly.css">
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="navbar navbar-default navbar-fixed-top ">
|
|
<div class="container">
|
|
<div class="navbar-header">
|
|
<a class="navbar-brand" href="index.html">jellyfin-roku api docs</a>
|
|
<button class="navbar-toggle" type="button" data-toggle="collapse" data-target="#topNavigation">
|
|
<span class="icon-bar"></span>
|
|
<span class="icon-bar"></span>
|
|
<span class="icon-bar"></span>
|
|
</button>
|
|
</div>
|
|
<div class="navbar-collapse collapse" id="topNavigation">
|
|
<ul class="nav navbar-nav">
|
|
|
|
<li class="dropdown">
|
|
<a href="modules.list.html" class="dropdown-toggle" data-toggle="dropdown">Modules<b class="caret"></b></a>
|
|
<ul class="dropdown-menu ">
|
|
<li><a href="module-AlbumData.html">AlbumData</a></li><li><a href="module-AlbumGrid.html">AlbumGrid</a></li><li><a href="module-AlbumTrackList.html">AlbumTrackList</a></li><li><a href="module-AlbumView.html">AlbumView</a></li><li><a href="module-Alpha.html">Alpha</a></li><li><a href="module-ArtistView.html">ArtistView</a></li><li><a href="module-AudioPlayer.html">AudioPlayer</a></li><li><a href="module-AudioPlayerView.html">AudioPlayerView</a></li><li><a href="module-AudioTrackListItem.html">AudioTrackListItem</a></li><li><a href="module-ButtonGroupHoriz.html">ButtonGroupHoriz</a></li><li><a href="module-ChannelData.html">ChannelData</a></li><li><a href="module-CollectionData.html">CollectionData</a></li><li><a href="module-ConfigData.html">ConfigData</a></li><li><a href="module-ConfigItem.html">ConfigItem</a></li><li><a href="module-ConfigList.html">ConfigList</a></li><li><a href="module-ExtrasItem.html">ExtrasItem</a></li><li><a href="module-ExtrasRowList.html">ExtrasRowList</a></li><li><a href="module-FavoriteItemsTask.html">FavoriteItemsTask</a></li><li><a href="module-FolderData.html">FolderData</a></li><li><a href="module-GetFiltersTask.html">GetFiltersTask</a></li><li><a href="module-GetNextEpisodeTask.html">GetNextEpisodeTask</a></li><li><a href="module-GetPlaybackInfoTask.html">GetPlaybackInfoTask</a></li><li><a href="module-GetShuffleEpisodesTask.html">GetShuffleEpisodesTask</a></li><li><a href="module-GridItem.html">GridItem</a></li><li><a href="module-GridItemSmall.html">GridItemSmall</a></li><li><a href="module-Home.html">Home</a></li><li><a href="module-HomeData.html">HomeData</a></li><li><a href="module-HomeItem.html">HomeItem</a></li><li><a href="module-HomeRows.html">HomeRows</a></li><li><a href="module-IconButton.html">IconButton</a></li><li><a href="module-Image.html">Image</a></li><li><a href="module-ImageData.html">ImageData</a></li><li><a href="module-ItemGrid.html">ItemGrid</a></li><li><a href="module-ItemGridOptions.html">ItemGridOptions</a></li><li><a href="module-Items.html">Items</a></li><li><a href="module-JFButton.html">JFButton</a></li><li><a href="module-JFButtons.html">JFButtons</a></li><li><a href="module-JFGroup.html">JFGroup</a></li><li><a href="module-JFMessageDialog.html">JFMessageDialog</a></li><li><a href="module-JFOverhang.html">JFOverhang</a></li><li><a href="module-JFScene.html">JFScene</a></li><li><a href="module-JFScreen.html">JFScreen</a></li><li><a href="module-JFServer.html">JFServer</a></li><li><a href="module-JFVideo.html">JFVideo</a></li><li><a href="module-ListPoster.html">ListPoster</a></li><li><a href="module-LoadChannelsTask.html">LoadChannelsTask</a></li><li><a href="module-LoadItemsTask.html">LoadItemsTask</a></li><li><a href="module-LoadItemsTask2.html">LoadItemsTask2</a></li><li><a href="module-LoadPhotoTask.html">LoadPhotoTask</a></li><li><a href="module-LoadProgramDetailsTask.html">LoadProgramDetailsTask</a></li><li><a href="module-LoadScreenSaverTimeoutTask.html">LoadScreenSaverTimeoutTask</a></li><li><a href="module-LoadSheduleTask.html">LoadSheduleTask</a></li><li><a href="module-LoadVideoContentTask.html">LoadVideoContentTask</a></li><li><a href="module-LoginScene.html">LoginScene</a></li><li><a href="module-Main.html">Main</a></li><li><a href="module-MovieData.html">MovieData</a></li><li><a href="module-MovieDetails.html">MovieDetails</a></li><li><a href="module-MovieLibraryView.html">MovieLibraryView</a></li><li><a href="module-MovieOptions.html">MovieOptions</a></li><li><a href="module-MusicAlbumData.html">MusicAlbumData</a></li><li><a href="module-MusicAlbumSongListData.html">MusicAlbumSongListData</a></li><li><a href="module-MusicArtistData.html">MusicArtistData</a></li><li><a href="module-MusicArtistGridItem.html">MusicArtistGridItem</a></li><li><a href="module-MusicLibraryView.html">MusicLibraryView</a></li><li><a href="module-MusicSongData.html">MusicSongData</a></li><li><a href="module-OptionNode.html">OptionNode</a></li><li><a href="module-OptionsButton.html">OptionsButton</a></li><li><a href="module-OptionsData.html">OptionsData</a></li><li><a href="module-OptionsSlider.html">OptionsSlider</a></li><li><a href="module-OverviewDialog.html">OverviewDialog</a></li><li><a href="module-PersonData.html">PersonData</a></li><li><a href="module-PersonDetails.html">PersonDetails</a></li><li><a href="module-PhotoData.html">PhotoData</a></li><li><a href="module-PhotoDetails.html">PhotoDetails</a></li><li><a href="module-PlaybackDialog.html">PlaybackDialog</a></li><li><a href="module-PlayedCheckmark.html">PlayedCheckmark</a></li><li><a href="module-PlaylistData.html">PlaylistData</a></li><li><a href="module-PlaylistView.html">PlaylistView</a></li><li><a href="module-PlaystateTask.html">PlaystateTask</a></li><li><a href="module-ProgramDetails.html">ProgramDetails</a></li><li><a href="module-PublicUserData.html">PublicUserData</a></li><li><a href="module-QueueManager.html">QueueManager</a></li><li><a href="module-QuickConnect.html">QuickConnect</a></li><li><a href="module-QuickConnectDialog.html">QuickConnectDialog</a></li><li><a href="module-RadioDialog.html">RadioDialog</a></li><li><a href="module-RecordProgramTask.html">RecordProgramTask</a></li><li><a href="module-SceneManager.html">SceneManager</a></li><li><a href="module-ScheduleProgramData.html">ScheduleProgramData</a></li><li><a href="module-SearchBox.html">SearchBox</a></li><li><a href="module-SearchData.html">SearchData</a></li><li><a href="module-SearchResults.html">SearchResults</a></li><li><a href="module-SearchRow.html">SearchRow</a></li><li><a href="module-SearchTask.html">SearchTask</a></li><li><a href="module-SeriesData.html">SeriesData</a></li><li><a href="module-ServerDiscoveryTask.html">ServerDiscoveryTask</a></li><li><a href="module-SetServerScreen.html">SetServerScreen</a></li><li><a href="module-ShowScenes.html">ShowScenes</a></li><li><a href="module-SongItem.html">SongItem</a></li><li><a href="module-Spinner.html">Spinner</a></li><li><a href="module-StandardDialog.html">StandardDialog</a></li><li><a href="module-Subtitles.html">Subtitles</a></li><li><a href="module-TVEpisode.html">TVEpisode</a></li><li><a href="module-TVEpisodeData.html">TVEpisodeData</a></li><li><a href="module-TVEpisodeRow.html">TVEpisodeRow</a></li><li><a href="module-TVEpisodeRowWithOptions.html">TVEpisodeRowWithOptions</a></li><li><a href="module-TVEpisodes.html">TVEpisodes</a></li><li><a href="module-TVListDetails.html">TVListDetails</a></li><li><a href="module-TVListOptions.html">TVListOptions</a></li><li><a href="module-TVSeasonData.html">TVSeasonData</a></li><li><a href="module-TVSeasonRow.html">TVSeasonRow</a></li><li><a href="module-TVShowDescription.html">TVShowDescription</a></li><li><a href="module-TVShowDetails.html">TVShowDetails</a></li><li><a href="module-TextSizeTask.html">TextSizeTask</a></li><li><a href="module-UserData.html">UserData</a></li><li><a href="module-UserItem.html">UserItem</a></li><li><a href="module-UserLibrary.html">UserLibrary</a></li><li><a href="module-UserRow.html">UserRow</a></li><li><a href="module-UserSelect.html">UserSelect</a></li><li><a href="module-VideoData.html">VideoData</a></li><li><a href="module-VideoPlayer.html">VideoPlayer</a></li><li><a href="module-VideoPlayerView.html">VideoPlayerView</a></li><li><a href="module-VideoTrackListItem.html">VideoTrackListItem</a></li><li><a href="module-ViewCreator.html">ViewCreator</a></li><li><a href="module-WhatsNewDialog.html">WhatsNewDialog</a></li><li><a href="module-baserequest.html">baserequest</a></li><li><a href="module-captionTask.html">captionTask</a></li><li><a href="module-config.html">config</a></li><li><a href="module-deviceCapabilities.html">deviceCapabilities</a></li><li><a href="module-globals.html">globals</a></li><li><a href="module-migrations.html">migrations</a></li><li><a href="module-misc.html">misc</a></li><li><a href="module-schedule.html">schedule</a></li><li><a href="module-section.html">section</a></li><li><a href="module-sectionScroller.html">sectionScroller</a></li><li><a href="module-settings.html">settings</a></li><li><a href="module-userauth.html">userauth</a></li>
|
|
</ul>
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
<div class="col-sm-3 col-md-3">
|
|
<form class="navbar-form" role="search">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" placeholder="Search" name="q" id="search-input">
|
|
<div class="input-group-btn">
|
|
<button class="btn btn-default" id="search-submit"><i class="glyphicon glyphicon-search"></i></button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="container" id="toc-content">
|
|
<div class="row">
|
|
|
|
|
|
<div class="col-md-12">
|
|
|
|
<div id="main">
|
|
|
|
|
|
<h1 class="page-title">Source: source/ShowScenes.brs</h1>
|
|
|
|
<section>
|
|
<article>
|
|
<pre
|
|
class="sunlight-highlight-javascript linenums">function LoginFlow()
|
|
'Collect Jellyfin server and user information
|
|
start_login:
|
|
|
|
serverUrl = get_setting("server")
|
|
if isValid(serverUrl)
|
|
print "Previous server connection saved to registry"
|
|
startOver = not session.server.UpdateURL(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
|
|
' Show Connecting to Server spinner
|
|
dialog = createObject("roSGNode", "ProgressDialog")
|
|
dialog.title = tr("Connecting to Server")
|
|
m.scene.dialog = dialog
|
|
invalidServer = ServerInfo().Error
|
|
dialog.close = true
|
|
end if
|
|
|
|
m.serverSelection = "Saved"
|
|
if startOver or invalidServer
|
|
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()
|
|
end if
|
|
|
|
activeUser = get_setting("active_user")
|
|
if activeUser = invalid
|
|
print "No active user found in registry"
|
|
user_select:
|
|
SendPerformanceBeacon("AppDialogInitiate") ' Roku Performance monitoring - Dialog Starting
|
|
|
|
publicUsers = GetPublicUsers()
|
|
savedUsers = getSavedUsers()
|
|
|
|
numPubUsers = publicUsers.count()
|
|
numSavedUsers = savedUsers.count()
|
|
|
|
if numPubUsers > 0 or numSavedUsers > 0
|
|
publicUsersNodes = []
|
|
publicUserIds = []
|
|
' load public users
|
|
if numPubUsers > 0
|
|
for each item in publicUsers
|
|
user = CreateObject("roSGNode", "PublicUserData")
|
|
user.id = item.Id
|
|
user.name = item.Name
|
|
if isValid(item.PrimaryImageTag)
|
|
user.ImageURL = UserImageURL(user.id, { "tag": item.PrimaryImageTag })
|
|
end if
|
|
publicUsersNodes.push(user)
|
|
publicUserIds.push(user.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.session.server.id
|
|
' only show unique userids on screen.
|
|
if not arrayHasValue(publicUserIds, savedUser.Id)
|
|
user = CreateObject("roSGNode", "PublicUserData")
|
|
user.id = savedUser.Id
|
|
|
|
if isValid(savedUser.username)
|
|
user.name = savedUser.username
|
|
end if
|
|
|
|
publicUsersNodes.push(user)
|
|
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"
|
|
session.server.Delete()
|
|
unset_setting("server")
|
|
goto start_login
|
|
else
|
|
print "A public user was selected with username=" + userSelected
|
|
session.user.Update("name", userSelected)
|
|
regex = CreateObject("roRegex", "[^a-zA-Z0-9\ \-\_]", "")
|
|
session.user.Update("friendlyName", regex.ReplaceAll(userSelected, ""))
|
|
' save userid to session
|
|
for each user in publicUsersNodes
|
|
if user.name = userSelected
|
|
session.user.Update("id", user.id)
|
|
exit for
|
|
end if
|
|
end for
|
|
' try to login with token from registry
|
|
myToken = get_user_setting("token")
|
|
if myToken <> invalid
|
|
' check if token is valid
|
|
print "Auth token found in registry for selected user"
|
|
session.user.Update("authToken", myToken)
|
|
print "Attempting to use API with auth token"
|
|
currentUser = AboutMe()
|
|
if currentUser = invalid
|
|
print "Auth token is no longer valid - deleting token"
|
|
unset_user_setting("token")
|
|
unset_user_setting("username")
|
|
else
|
|
print "Success! Auth token is still valid"
|
|
session.user.Login(currentUser, true)
|
|
LoadUserAbilities()
|
|
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!"
|
|
session.user.Login(userData, true)
|
|
LoadUserAbilities()
|
|
return true
|
|
else
|
|
print "Auth failed. Password required"
|
|
end if
|
|
end if
|
|
else
|
|
userSelected = ""
|
|
end if
|
|
passwordEntry = CreateSigninGroup(userSelected)
|
|
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
|
|
if passwordEntry = "backPressed"
|
|
if numPubUsers > 0
|
|
goto user_select
|
|
else
|
|
session.server.Delete()
|
|
unset_setting("server")
|
|
goto start_login
|
|
end if
|
|
end if
|
|
else
|
|
print "Active user found in registry"
|
|
session.user.Update("id", activeUser)
|
|
|
|
myUsername = get_user_setting("username")
|
|
myAuthToken = get_user_setting("token")
|
|
if isValid(myAuthToken) and isValid(myUsername)
|
|
print "Auth token found in registry"
|
|
session.user.Update("authToken", myAuthToken)
|
|
session.user.Update("name", myUsername)
|
|
regex = CreateObject("roRegex", "[^a-zA-Z0-9\ \-\_]", "")
|
|
session.user.Update("friendlyName", regex.ReplaceAll(myUsername, ""))
|
|
print "Attempting to use API with auth token"
|
|
currentUser = AboutMe()
|
|
if currentUser = invalid
|
|
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!"
|
|
session.user.Login(userData, true)
|
|
LoadUserAbilities()
|
|
return true
|
|
else
|
|
print "Auth failed. Password required"
|
|
print "delete token and restart login flow"
|
|
unset_user_setting("token")
|
|
unset_user_setting("username")
|
|
goto start_login
|
|
end if
|
|
else
|
|
print "Success! Auth token is still valid"
|
|
session.user.Login(currentUser, true)
|
|
end if
|
|
else
|
|
print "No auth token found in registry"
|
|
end if
|
|
end if
|
|
|
|
if m.global.session.user.id = invalid or m.global.session.user.authToken = invalid
|
|
print "Login failed, restart flow"
|
|
unset_setting("active_user")
|
|
session.user.Logout()
|
|
goto start_login
|
|
end if
|
|
|
|
LoadUserAbilities()
|
|
m.global.sceneManager.callFunc("clearScenes")
|
|
|
|
return true
|
|
end function
|
|
|
|
sub SaveServerList()
|
|
'Save off this server to our list of saved servers for easier navigation between servers
|
|
server = m.global.session.server.url
|
|
saved = get_setting("saved_servers")
|
|
if isValid(server)
|
|
server = LCase(server)'Saved server data is always lowercase
|
|
end if
|
|
entryCount = 0
|
|
addNewEntry = true
|
|
savedServers = { serverList: [] }
|
|
if isValid(saved)
|
|
savedServers = ParseJson(saved)
|
|
entryCount = savedServers.serverList.Count()
|
|
if isValid(savedServers.serverList) and entryCount > 0
|
|
for each item in savedServers.serverList
|
|
if item.baseUrl = server
|
|
addNewEntry = false
|
|
exit for
|
|
end if
|
|
end for
|
|
end if
|
|
end if
|
|
|
|
if addNewEntry
|
|
if entryCount = 0
|
|
set_setting("saved_servers", FormatJson({ serverList: [{ name: m.serverSelection, baseUrl: server, iconUrl: "pkg:/images/logo-icon120.jpg", iconWidth: 120, iconHeight: 120 }] }))
|
|
else
|
|
savedServers.serverList.Push({ name: m.serverSelection, baseUrl: server, iconUrl: "pkg:/images/logo-icon120.jpg", iconWidth: 120, iconHeight: 120 })
|
|
set_setting("saved_servers", FormatJson(savedServers))
|
|
end if
|
|
end if
|
|
end sub
|
|
|
|
sub DeleteFromServerList(urlToDelete)
|
|
saved = get_setting("saved_servers")
|
|
if isValid(urlToDelete)
|
|
urlToDelete = LCase(urlToDelete)
|
|
end if
|
|
if isValid(saved)
|
|
savedServers = ParseJson(saved)
|
|
newServers = { serverList: [] }
|
|
for each item in savedServers.serverList
|
|
if item.baseUrl <> urlToDelete
|
|
newServers.serverList.Push(item)
|
|
end if
|
|
end for
|
|
set_setting("saved_servers", FormatJson(newServers))
|
|
end if
|
|
end sub
|
|
|
|
' Roku Performance monitoring
|
|
sub SendPerformanceBeacon(signalName as string)
|
|
if m.global.app_loaded = false
|
|
m.scene.signalBeacon(signalName)
|
|
end if
|
|
end sub
|
|
|
|
function CreateServerGroup()
|
|
screen = CreateObject("roSGNode", "SetServerScreen")
|
|
screen.optionsAvailable = true
|
|
m.global.sceneManager.callFunc("pushScene", screen)
|
|
port = CreateObject("roMessagePort")
|
|
m.colors = {}
|
|
|
|
if isValid(m.global.session.server.url)
|
|
screen.serverUrl = m.global.session.server.url
|
|
end if
|
|
m.viewModel = {}
|
|
button = screen.findNode("submit")
|
|
button.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"
|
|
node = msg.getNode()
|
|
if node = "submit"
|
|
' Show Connecting to Server spinner
|
|
dialog = createObject("roSGNode", "ProgressDialog")
|
|
dialog.title = tr("Connecting to Server")
|
|
m.scene.dialog = dialog
|
|
|
|
serverUrl = standardize_jellyfin_url(screen.serverUrl)
|
|
|
|
isConnected = session.server.UpdateURL(serverUrl)
|
|
serverInfoResult = invalid
|
|
if isConnected
|
|
set_setting("server", serverUrl)
|
|
serverInfoResult = ServerInfo()
|
|
end if
|
|
dialog.close = true
|
|
|
|
if isConnected = false or serverInfoResult = invalid
|
|
' 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
|
|
if isValid(serverInfoResult.Error) and serverInfoResult.Error
|
|
' If server redirected received, update the URL
|
|
if isValid(serverInfoResult.UpdatedUrl)
|
|
serverUrl = serverInfoResult.UpdatedUrl
|
|
|
|
isConnected = session.server.UpdateURL(serverUrl)
|
|
if isConnected
|
|
set_setting("server", serverUrl)
|
|
screen.visible = false
|
|
return ""
|
|
end if
|
|
end if
|
|
' Display Error Message to user
|
|
message = tr("Error: ")
|
|
if isValid(serverInfoResult.ErrorCode)
|
|
message = message + "[" + serverInfoResult.ErrorCode.toStr() + "] "
|
|
end if
|
|
screen.errorMessage = message + tr(serverInfoResult.ErrorMessage)
|
|
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 node = "delete_saved"
|
|
serverPicker = screen.findNode("serverPicker")
|
|
itemToDelete = serverPicker.content.getChild(serverPicker.itemFocused)
|
|
urlToDelete = itemToDelete.baseUrl
|
|
if isValid(urlToDelete)
|
|
DeleteFromServerList(urlToDelete)
|
|
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("alternateOptions").observeField("itemSelected", port)
|
|
group.observeField("backPressed", port)
|
|
while true
|
|
msg = wait(0, port)
|
|
if type(msg) = "roSGScreenEvent" and msg.isScreenClosed()
|
|
group.visible = false
|
|
return -1
|
|
else if isNodeEvent(msg, "backPressed")
|
|
return "backPressed"
|
|
else if type(msg) = "roSGNodeEvent" and msg.getField() = "userSelected"
|
|
return msg.GetData()
|
|
else if type(msg) = "roSGNodeEvent" and msg.getField() = "itemSelected"
|
|
if msg.getData() = 0
|
|
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(user = "")
|
|
' 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 user = "" and get_setting("username") <> invalid
|
|
username_field.value = get_setting("username")
|
|
else
|
|
username_field.value = user
|
|
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
|
|
checkbox.checkedState = [true]
|
|
quickConnect = group.findNode("quickConnect")
|
|
' Quick Connect only supported for server version 10.8+ right now...
|
|
if versionChecker(m.global.session.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
|
|
|
|
button = group.findNode("submit")
|
|
button.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)
|
|
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"
|
|
node = msg.getNode()
|
|
if node = "submit"
|
|
' Validate credentials
|
|
activeUser = get_token(username.value, password.value)
|
|
if isValid(activeUser)
|
|
print "activeUser=", activeUser
|
|
if checkbox.checkedState[0] = true
|
|
' save credentials
|
|
session.user.Login(activeUser, true)
|
|
set_user_setting("token", activeUser.token)
|
|
set_user_setting("username", username.value)
|
|
else
|
|
session.user.Login(activeUser)
|
|
end if
|
|
return "true"
|
|
end if
|
|
print "Login attempt failed..."
|
|
group.findNode("alert").text = tr("Login attempt failed.")
|
|
else if node = "quickConnect"
|
|
json = initQuickConnect()
|
|
if json = invalid
|
|
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.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
|
|
else 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 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 = []
|
|
options_buttons = [
|
|
{ "title": "Search", "id": "goto_search" },
|
|
{ "title": "Change user", "id": "change_user" },
|
|
{ "title": "Change server", "id": "change_server" },
|
|
{ "title": "Sign out", "id": "sign_out" }
|
|
]
|
|
for each opt in options_buttons
|
|
o = CreateObject("roSGNode", "OptionsButton")
|
|
o.title = tr(opt.title)
|
|
o.id = opt.id
|
|
o.observeField("optionSelected", m.port)
|
|
new_options.push(o)
|
|
end for
|
|
|
|
' Add settings option to menu
|
|
o = CreateObject("roSGNode", "OptionsButton")
|
|
o.title = "Settings"
|
|
o.id = "settings"
|
|
o.observeField("optionSelected", m.port)
|
|
new_options.push(o)
|
|
|
|
' And a profile button
|
|
user_node = CreateObject("roSGNode", "OptionsData")
|
|
user_node.id = "active_user"
|
|
user_node.title = tr("Profile")
|
|
user_node.base_title = tr("Profile")
|
|
user_options = []
|
|
for each user in AvailableUsers()
|
|
user_options.push({ display: user.username + "@" + user.server, value: user.id })
|
|
end for
|
|
user_node.choices = user_options
|
|
user_node.value = m.global.session.user.id
|
|
new_options.push(user_node)
|
|
|
|
sidepanel.options = new_options
|
|
|
|
return group
|
|
end function
|
|
|
|
function CreateMovieDetailsGroup(movie as object) as dynamic
|
|
' validate movie node
|
|
if not isValid(movie) or not isValid(movie.id) then return invalid
|
|
|
|
startLoadingSpinner()
|
|
' get movie meta data
|
|
movieMetaData = ItemMetaData(movie.id)
|
|
' validate movie meta data
|
|
if not isValid(movieMetaData)
|
|
stopLoadingSpinner()
|
|
return invalid
|
|
end if
|
|
' start building MovieDetails view
|
|
group = CreateObject("roSGNode", "MovieDetails")
|
|
group.overhangTitle = movie.title
|
|
group.optionsAvailable = false
|
|
group.trailerAvailable = false
|
|
' push scene asap (to prevent extra button presses when retriving series/movie info)
|
|
m.global.sceneManager.callFunc("pushScene", group)
|
|
group.itemContent = movieMetaData
|
|
' local trailers
|
|
trailerData = api.users.GetLocalTrailers(m.global.session.user.id, movie.id)
|
|
if isValid(trailerData)
|
|
group.trailerAvailable = trailerData.Count() > 0
|
|
end if
|
|
' watch for button presses
|
|
buttons = group.findNode("buttons")
|
|
for each b in buttons.getChildren(-1, 0)
|
|
b.observeField("buttonSelected", m.port)
|
|
end for
|
|
' setup and load movie extras
|
|
extras = group.findNode("extrasGrid")
|
|
extras.observeField("selectedItem", m.port)
|
|
extras.callFunc("loadParts", movieMetaData.json)
|
|
' done building MovieDetails view
|
|
stopLoadingSpinner()
|
|
return group
|
|
end function
|
|
|
|
function CreateSeriesDetailsGroup(seriesID as string) as dynamic
|
|
' validate series node
|
|
if not isValid(seriesID) or seriesID = "" then return invalid
|
|
|
|
startLoadingSpinner()
|
|
' get series meta data
|
|
seriesMetaData = ItemMetaData(seriesID)
|
|
' validate series meta data
|
|
if not isValid(seriesMetaData)
|
|
stopLoadingSpinner()
|
|
return invalid
|
|
end if
|
|
' Get season data early in the function so we can check number of seasons.
|
|
seasonData = TVSeasons(seriesID)
|
|
' Divert to season details if user setting goStraightToEpisodeListing is enabled and only one season exists.
|
|
if m.global.session.user.settings["ui.tvshows.goStraightToEpisodeListing"] = true and seasonData.Items.Count() = 1
|
|
stopLoadingSpinner()
|
|
return CreateSeasonDetailsGroupByID(seriesID, seasonData.Items[0].id)
|
|
end if
|
|
' start building SeriesDetails view
|
|
group = CreateObject("roSGNode", "TVShowDetails")
|
|
group.optionsAvailable = false
|
|
' push scene asap (to prevent extra button presses when retriving series/movie info)
|
|
m.global.sceneManager.callFunc("pushScene", group)
|
|
group.itemContent = seriesMetaData
|
|
group.seasonData = seasonData
|
|
' watch for button presses
|
|
group.observeField("seasonSelected", m.port)
|
|
' setup and load series extras
|
|
extras = group.findNode("extrasGrid")
|
|
extras.observeField("selectedItem", m.port)
|
|
extras.callFunc("loadParts", seriesMetaData.json)
|
|
' done building SeriesDetails view
|
|
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 (musicData = invalid or musicData.Items.Count() = 0) and (appearsOnData = invalid 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
|
|
|
|
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)
|
|
|
|
' Watch for user click on Play button on album
|
|
group.observeField("playAllSelected", m.port)
|
|
|
|
' Watch for user click on Instant Mix button on album
|
|
group.observeField("instantMixSelected", 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)
|
|
|
|
' Watch for user click on Play button
|
|
group.observeField("playAllSelected", m.port)
|
|
|
|
return group
|
|
end function
|
|
|
|
function CreateSeasonDetailsGroup(series as object, season as object) as dynamic
|
|
' validate series node
|
|
if not isValid(series) or not isValid(series.id) then return invalid
|
|
' validate season node
|
|
if not isValid(season) or not isValid(season.id) then return invalid
|
|
|
|
startLoadingSpinner()
|
|
' get season meta data
|
|
seasonMetaData = ItemMetaData(season.id)
|
|
' validate season meta data
|
|
if not isValid(seasonMetaData)
|
|
stopLoadingSpinner()
|
|
return invalid
|
|
end if
|
|
' start building SeasonDetails view
|
|
group = CreateObject("roSGNode", "TVEpisodes")
|
|
group.optionsAvailable = false
|
|
' push scene asap (to prevent extra button presses when retriving series/movie info)
|
|
m.global.sceneManager.callFunc("pushScene", group)
|
|
group.seasonData = seasonMetaData.json
|
|
group.objects = TVEpisodes(series.id, season.id)
|
|
group.episodeObjects = group.objects
|
|
group.extrasObjects = TVSeasonExtras(season.id)
|
|
|
|
' watch for button presses
|
|
group.observeField("episodeSelected", m.port)
|
|
group.observeField("quickPlayNode", m.port)
|
|
' finished building SeasonDetails view
|
|
stopLoadingSpinner()
|
|
return group
|
|
end function
|
|
|
|
function CreateSeasonDetailsGroupByID(seriesID as string, seasonID as string) as dynamic
|
|
' validate parameters
|
|
if seriesID = "" or seasonID = "" then return invalid
|
|
|
|
startLoadingSpinner()
|
|
' get season meta data
|
|
seasonMetaData = ItemMetaData(seasonID)
|
|
' validate season meta data
|
|
if not isValid(seasonMetaData)
|
|
stopLoadingSpinner()
|
|
return invalid
|
|
end if
|
|
' start building SeasonDetails view
|
|
group = CreateObject("roSGNode", "TVEpisodes")
|
|
group.optionsAvailable = false
|
|
' push scene asap (to prevent extra button presses when retriving series/movie info)
|
|
m.global.sceneManager.callFunc("pushScene", group)
|
|
group.seasonData = seasonMetaData.json
|
|
group.objects = TVEpisodes(seriesID, seasonID)
|
|
group.episodeObjects = group.objects
|
|
group.extrasObjects = TVSeasonExtras(seasonID)
|
|
|
|
' watch for button presses
|
|
group.observeField("episodeSelected", m.port)
|
|
group.observeField("quickPlayNode", m.port)
|
|
' finished building SeasonDetails view
|
|
stopLoadingSpinner()
|
|
return group
|
|
end function
|
|
|
|
function CreateItemGrid(libraryItem as object) as dynamic
|
|
' validate libraryItem
|
|
if not isValid(libraryItem) then return invalid
|
|
|
|
group = CreateObject("roSGNode", "ItemGrid")
|
|
group.parentItem = libraryItem
|
|
group.optionsAvailable = true
|
|
group.observeField("selectedItem", m.port)
|
|
return group
|
|
end function
|
|
|
|
function CreateMovieLibraryView(libraryItem as object) as dynamic
|
|
' validate libraryItem
|
|
if not isValid(libraryItem) then return invalid
|
|
|
|
group = CreateObject("roSGNode", "MovieLibraryView")
|
|
group.parentItem = libraryItem
|
|
group.optionsAvailable = true
|
|
group.observeField("selectedItem", m.port)
|
|
return group
|
|
end function
|
|
|
|
function CreateMusicLibraryView(libraryItem as object) as dynamic
|
|
' validate libraryItem
|
|
if not isValid(libraryItem) then return invalid
|
|
|
|
group = CreateObject("roSGNode", "MusicLibraryView")
|
|
group.parentItem = libraryItem
|
|
group.optionsAvailable = true
|
|
group.observeField("selectedItem", m.port)
|
|
return group
|
|
end function
|
|
|
|
function CreateSearchPage()
|
|
' Search + Results Page
|
|
group = CreateObject("roSGNode", "searchResults")
|
|
options = group.findNode("searchSelect")
|
|
options.observeField("itemSelected", m.port)
|
|
|
|
return group
|
|
end function
|
|
|
|
function CreateVideoPlayerGroup(video_id as string, mediaSourceId = invalid as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean, showIntro = true as boolean, allowResumeDialog = true as boolean)
|
|
' validate video_id
|
|
if not isValid(video_id) or video_id = "" then return invalid
|
|
|
|
startMediaLoadingSpinner()
|
|
' Video is Playing
|
|
video = VideoPlayer(video_id, mediaSourceId, audio_stream_idx, defaultSubtitleTrackFromVid(video_id), forceTranscoding, showIntro, allowResumeDialog)
|
|
|
|
if video = invalid then return invalid
|
|
|
|
video.allowCaptions = true
|
|
|
|
if video.errorMsg = "introaborted" then return video
|
|
video.observeField("selectSubtitlePressed", m.port)
|
|
video.observeField("selectPlaybackInfoPressed", m.port)
|
|
video.observeField("state", m.port)
|
|
stopLoadingSpinner()
|
|
return video
|
|
end function
|
|
|
|
function CreatePersonView(personData as object) as dynamic
|
|
' validate personData node
|
|
if not isValid(personData) or not isValid(personData.id) then return invalid
|
|
|
|
startLoadingSpinner()
|
|
' get person meta data
|
|
personMetaData = ItemMetaData(personData.id)
|
|
' validate season meta data
|
|
if not isValid(personMetaData)
|
|
stopLoadingSpinner()
|
|
return invalid
|
|
end if
|
|
' start building Person View
|
|
person = CreateObject("roSGNode", "PersonDetails")
|
|
' push scene asap (to prevent extra button presses when retriving series/movie info)
|
|
m.global.SceneManager.callFunc("pushScene", person)
|
|
person.itemContent = personMetaData
|
|
person.setFocus(true)
|
|
' watch for button presses
|
|
person.observeField("selectedItem", m.port)
|
|
person.findNode("favorite-button").observeField("buttonSelected", m.port)
|
|
' finished building Person View
|
|
stopLoadingSpinner()
|
|
return person
|
|
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, meta as object)
|
|
|
|
resumeData = [
|
|
tr("Resume playing at ") + ticksToHuman(time) + ".",
|
|
tr("Start over from the beginning.")
|
|
]
|
|
|
|
group = m.global.sceneManager.callFunc("getActiveScene")
|
|
|
|
if LCase(group.subtype()) = "home"
|
|
if LCase(meta.type) = "episode"
|
|
resumeData.push(tr("Go to series"))
|
|
resumeData.push(tr("Go to season"))
|
|
resumeData.push(tr("Go to episode"))
|
|
end if
|
|
end if
|
|
|
|
m.global.sceneManager.callFunc("optionDialog", tr("Playback Options"), [], resumeData)
|
|
end sub
|
|
</pre>
|
|
</article>
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="clearfix"></div>
|
|
|
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="modal fade" id="searchResults">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
|
<h4 class="modal-title">Search results</h4>
|
|
</div>
|
|
<div class="modal-body"></div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
|
</div>
|
|
</div><!-- /.modal-content -->
|
|
</div><!-- /.modal-dialog -->
|
|
</div>
|
|
|
|
|
|
<footer>
|
|
|
|
<span class="jsdoc-message">Source code: <a href="https://github.com/jellyfin/jellyfin-roku">https://github.com/jellyfin/jellyfin-roku</a></span><span class="jsdoc-message">Jellyfin Roku Development Forum: <a href="https://forum.jellyfin.org/f-roku-development">https://forum.jellyfin.org/f-roku-development</a></span>
|
|
|
|
|
|
<span class="jsdoc-message">
|
|
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.2</a>
|
|
|
|
on Oct 28th 2023
|
|
|
|
using the <a href="https://github.com/docstrap/docstrap">DocStrap template</a>.
|
|
</span>
|
|
</footer>
|
|
|
|
<script src="scripts/docstrap.lib.js"></script>
|
|
<script src="scripts/toc.js"></script>
|
|
|
|
<script type="text/javascript" src="scripts/fulltext-search-ui.js"></script>
|
|
|
|
|
|
<script>
|
|
$( function () {
|
|
$( "[id*='$']" ).each( function () {
|
|
var $this = $( this );
|
|
|
|
$this.attr( "id", $this.attr( "id" ).replace( "$", "__" ) );
|
|
} );
|
|
|
|
$( ".tutorial-section pre, .readme-section pre, pre.prettyprint.source" ).each( function () {
|
|
var $this = $( this );
|
|
|
|
var example = $this.find( "code" );
|
|
exampleText = example.html();
|
|
var lang = /{@lang (.*?)}/.exec( exampleText );
|
|
if ( lang && lang[1] ) {
|
|
exampleText = exampleText.replace( lang[0], "" );
|
|
example.html( exampleText );
|
|
lang = lang[1];
|
|
} else {
|
|
var langClassMatch = example.parent()[0].className.match(/lang\-(\S+)/);
|
|
lang = langClassMatch ? langClassMatch[1] : "javascript";
|
|
}
|
|
|
|
if ( lang ) {
|
|
|
|
$this
|
|
.addClass( "sunlight-highlight-" + lang )
|
|
.addClass( "linenums" )
|
|
.html( example.html() );
|
|
|
|
}
|
|
} );
|
|
|
|
Sunlight.highlightAll( {
|
|
lineNumbers : true,
|
|
showMenu : true,
|
|
enableDoclinks : true
|
|
} );
|
|
|
|
$.catchAnchorLinks( {
|
|
navbarOffset: 10
|
|
} );
|
|
$( "#toc" ).toc( {
|
|
anchorName : function ( i, heading, prefix ) {
|
|
return $( heading ).attr( "id" ) || ( prefix + i );
|
|
},
|
|
selectors : "#toc-content h1,#toc-content h2,#toc-content h3,#toc-content h4",
|
|
showAndHide : false,
|
|
smoothScrolling: true
|
|
} );
|
|
|
|
$( "#main span[id^='toc']" ).addClass( "toc-shim" );
|
|
$( '.dropdown-toggle' ).dropdown();
|
|
|
|
$( "table" ).each( function () {
|
|
var $this = $( this );
|
|
$this.addClass('table');
|
|
} );
|
|
|
|
} );
|
|
</script>
|
|
|
|
|
|
|
|
<!--Navigation and Symbol Display-->
|
|
|
|
|
|
<!--Google Analytics-->
|
|
|
|
|
|
|
|
<script type="text/javascript">
|
|
$(document).ready(function() {
|
|
SearcherDisplay.init();
|
|
});
|
|
</script>
|
|
|
|
|
|
</body>
|
|
</html>
|