Playlist support & TV Shuffle (#986)
* Add TV Episode Shuffle * Reuse playback info * Get Subtitle Popup working * Get Subtitle Popup working * Get Resume/Restart popup working * Playlist poster, bug fixes * Remove commented out code * Start from beginning if playing queue * Fix Playback Info issue * Remove optional chaining to fix formatter * Fix playlist content list. Code cleanup. * Remove commented out code
This commit is contained in:
parent
948a9b550e
commit
a33ce8bd57
161
components/GetPlaybackInfoTask.brs
Normal file
161
components/GetPlaybackInfoTask.brs
Normal file
|
@ -0,0 +1,161 @@
|
|||
sub init()
|
||||
m.top.functionName = "getPlaybackInfoTask"
|
||||
end sub
|
||||
|
||||
function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioTrackIndex = -1 as integer, subtitleTrackIndex = -1 as integer, startTimeTicks = 0 as longinteger)
|
||||
body = {
|
||||
"DeviceProfile": getDeviceProfile()
|
||||
}
|
||||
params = {
|
||||
"UserId": get_setting("active_user"),
|
||||
"StartTimeTicks": startTimeTicks,
|
||||
"IsPlayback": true,
|
||||
"AutoOpenLiveStream": true,
|
||||
"MaxStreamingBitrate": "140000000",
|
||||
"MaxStaticBitrate": "140000000",
|
||||
"SubtitleStreamIndex": subtitleTrackIndex
|
||||
}
|
||||
|
||||
mediaSourceId = id
|
||||
if mediaSourceId <> "" then params.MediaSourceId = mediaSourceId
|
||||
|
||||
if audioTrackIndex > -1 then params.AudioStreamIndex = audioTrackIndex
|
||||
|
||||
req = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params)
|
||||
req.SetRequest("POST")
|
||||
return postJson(req, FormatJson(body))
|
||||
end function
|
||||
|
||||
' Returns an array of playback info to be displayed during playback.
|
||||
' In the future, with a custom playback info view, we can return an associated array.
|
||||
sub getPlaybackInfoTask()
|
||||
sessions = api_API().sessions.get()
|
||||
|
||||
m.playbackInfo = ItemPostPlaybackInfo(m.top.videoID)
|
||||
|
||||
if isValid(sessions) and sessions.Count() > 0
|
||||
m.top.data = { playbackInfo: GetTranscodingStats(sessions[0]) }
|
||||
else
|
||||
m.top.data = { playbackInfo: [tr("Unable to get playback information")] }
|
||||
end if
|
||||
end sub
|
||||
|
||||
function GetTranscodingStats(session)
|
||||
sessionStats = { data: [] }
|
||||
|
||||
if isValid(session.TranscodingInfo) and session.TranscodingInfo.Count() > 0
|
||||
transcodingReasons = session.TranscodingInfo.TranscodeReasons
|
||||
videoCodec = session.TranscodingInfo.VideoCodec
|
||||
audioCodec = session.TranscodingInfo.AudioCodec
|
||||
totalBitrate = session.TranscodingInfo.Bitrate
|
||||
audioChannels = session.TranscodingInfo.AudioChannels
|
||||
|
||||
if isValid(transcodingReasons) and transcodingReasons.Count() > 0
|
||||
sessionStats.data.push("<header>" + tr("Transcoding Information") + "</header>")
|
||||
for each item in transcodingReasons
|
||||
sessionStats.data.push("<b>• " + tr("Reason") + ":</b> " + item)
|
||||
end for
|
||||
end if
|
||||
|
||||
if isValid(videoCodec)
|
||||
data = "<b>• " + tr("Video Codec") + ":</b> " + videoCodec
|
||||
if session.TranscodingInfo.IsVideoDirect
|
||||
data = data + " (" + tr("direct") + ")"
|
||||
end if
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
|
||||
if isValid(audioCodec)
|
||||
data = "<b>• " + tr("Audio Codec") + ":</b> " + audioCodec
|
||||
if session.TranscodingInfo.IsAudioDirect
|
||||
data = data + " (" + tr("direct") + ")"
|
||||
end if
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
|
||||
if isValid(totalBitrate)
|
||||
data = "<b>• " + tr("Total Bitrate") + ":</b> " + getDisplayBitrate(totalBitrate)
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
|
||||
if isValid(audioChannels)
|
||||
data = "<b>• " + tr("Audio Channels") + ":</b> " + Str(audioChannels)
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
end if
|
||||
|
||||
if havePlaybackInfo()
|
||||
stream = m.playbackInfo.mediaSources[0].MediaStreams[0]
|
||||
sessionStats.data.push("<header>" + tr("Stream Information") + "</header>")
|
||||
if isValid(stream.Container)
|
||||
data = "<b>• " + tr("Container") + ":</b> " + stream.Container
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
if isValid(stream.Size)
|
||||
data = "<b>• " + tr("Size") + ":</b> " + stream.Size
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
if isValid(stream.BitRate)
|
||||
data = "<b>• " + tr("Bit Rate") + ":</b> " + getDisplayBitrate(stream.BitRate)
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
if isValid(stream.Codec)
|
||||
data = "<b>• " + tr("Codec") + ":</b> " + stream.Codec
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
if isValid(stream.CodecTag)
|
||||
data = "<b>• " + tr("Codec Tag") + ":</b> " + stream.CodecTag
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
if isValid(stream.VideoRangeType)
|
||||
data = "<b>• " + tr("Video range type") + ":</b> " + stream.VideoRangeType
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
if isValid(stream.PixelFormat)
|
||||
data = "<b>• " + tr("Pixel format") + ":</b> " + stream.PixelFormat
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
if isValid(stream.Width) and isValid(stream.Height)
|
||||
data = "<b>• " + tr("WxH") + ":</b> " + Str(stream.Width) + " x " + Str(stream.Height)
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
if isValid(stream.Level)
|
||||
data = "<b>• " + tr("Level") + ":</b> " + Str(stream.Level)
|
||||
sessionStats.data.push(data)
|
||||
end if
|
||||
end if
|
||||
|
||||
return sessionStats
|
||||
end function
|
||||
|
||||
function havePlaybackInfo()
|
||||
if not isValid(m.playbackInfo)
|
||||
return false
|
||||
end if
|
||||
|
||||
if not isValid(m.playbackInfo.mediaSources)
|
||||
return false
|
||||
end if
|
||||
|
||||
if m.playbackInfo.mediaSources.Count() <= 0
|
||||
return false
|
||||
end if
|
||||
|
||||
if not isValid(m.playbackInfo.mediaSources[0].MediaStreams)
|
||||
return false
|
||||
end if
|
||||
|
||||
if m.playbackInfo.mediaSources[0].MediaStreams.Count() <= 0
|
||||
return false
|
||||
end if
|
||||
|
||||
return true
|
||||
end function
|
||||
|
||||
function getDisplayBitrate(bitrate)
|
||||
if bitrate > 1000000
|
||||
return Str(Fix(bitrate / 1000000)) + " Mbps"
|
||||
else
|
||||
return Str(Fix(bitrate / 1000)) + " Kbps"
|
||||
end if
|
||||
end function
|
14
components/GetPlaybackInfoTask.xml
Normal file
14
components/GetPlaybackInfoTask.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
|
||||
<component name="GetPlaybackInfoTask" extends="Task">
|
||||
<interface>
|
||||
<field id="videoID" type="string" />
|
||||
<field id="data" type="assocarray" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="GetPlaybackInfoTask.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
|
||||
</component>
|
13
components/GetShuffleEpisodesTask.brs
Normal file
13
components/GetShuffleEpisodesTask.brs
Normal file
|
@ -0,0 +1,13 @@
|
|||
sub init()
|
||||
m.top.functionName = "getShuffleEpisodesTask"
|
||||
end sub
|
||||
|
||||
sub getShuffleEpisodesTask()
|
||||
data = api_API().shows.getepisodes(m.top.showID, {
|
||||
UserId: get_setting("active_user"),
|
||||
SortBy: "Random",
|
||||
Limit: 200
|
||||
})
|
||||
|
||||
m.top.data = data
|
||||
end sub
|
11
components/GetShuffleEpisodesTask.xml
Normal file
11
components/GetShuffleEpisodesTask.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
|
||||
<component name="GetShuffleEpisodesTask" extends="Task">
|
||||
<interface>
|
||||
<field id="showID" type="string" />
|
||||
<field id="data" type="assocarray" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="GetShuffleEpisodesTask.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
|
||||
</component>
|
|
@ -72,6 +72,10 @@ sub itemContentChanged()
|
|||
m.itemPoster.uri = itemData.PosterUrl
|
||||
m.itemIcon.uri = itemData.iconUrl
|
||||
m.itemText.text = itemData.Title
|
||||
else if itemData.type = "Playlist"
|
||||
m.itemPoster.uri = itemData.PosterUrl
|
||||
m.itemIcon.uri = itemData.iconUrl
|
||||
m.itemText.text = itemData.Title
|
||||
else if itemData.type = "Photo"
|
||||
m.itemPoster.uri = itemData.PosterUrl
|
||||
m.itemIcon.uri = itemData.iconUrl
|
||||
|
|
|
@ -135,6 +135,10 @@ sub loadItems()
|
|||
tmp = CreateObject("roSGNode", "PhotoData")
|
||||
else if item.type = "PhotoAlbum"
|
||||
tmp = CreateObject("roSGNode", "FolderData")
|
||||
else if item.type = "Playlist"
|
||||
tmp = CreateObject("roSGNode", "PlaylistData")
|
||||
tmp.type = "Playlist"
|
||||
tmp.image = PosterImage(item.id, { "maxHeight": 425, "maxWidth": 290, "quality": "90" })
|
||||
else if item.type = "Episode"
|
||||
tmp = CreateObject("roSGNode", "TVEpisode")
|
||||
else if item.Type = "Genre"
|
||||
|
@ -207,6 +211,8 @@ sub loadItems()
|
|||
tmp = CreateObject("roSGNode", "MusicArtistData")
|
||||
else if item.Type = "Audio"
|
||||
tmp = CreateObject("roSGNode", "MusicSongData")
|
||||
tmp.type = "Audio"
|
||||
tmp.image = api_API().items.getimageurl(item.id, "primary", 0, { "maxHeight": 280, "maxWidth": 280, "quality": "90" })
|
||||
else if item.Type = "MusicGenre"
|
||||
tmp = CreateObject("roSGNode", "FolderData")
|
||||
tmp.title = item.name
|
||||
|
|
1258
components/ItemGrid/LoadVideoContentTask.brs
Normal file
1258
components/ItemGrid/LoadVideoContentTask.brs
Normal file
File diff suppressed because it is too large
Load Diff
33
components/ItemGrid/LoadVideoContentTask.xml
Normal file
33
components/ItemGrid/LoadVideoContentTask.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
|
||||
<component name="LoadVideoContentTask" extends="Task">
|
||||
<interface>
|
||||
<field id="itemId" type="string" />
|
||||
<field id="startIndex" type="integer" value="0" />
|
||||
<field id="itemType" type="string" value="" />
|
||||
<field id="limit" type="integer" value="60" />
|
||||
<field id="metadata" type="assocarray" />
|
||||
<field id="sortField" type="string" value="SortName" />
|
||||
<field id="sortAscending" type="boolean" value="true" />
|
||||
<field id="nameStartsWith" type="string" value="" />
|
||||
<field id="recursive" type="boolean" value="true" />
|
||||
<field id="filter" type="string" value="All" />
|
||||
<field id="searchTerm" type="string" value="" />
|
||||
<field id="studioIds" type="string" value="" />
|
||||
<field id="genreIds" type="string" value="" />
|
||||
<field id="view" type="string" value="" />
|
||||
<!-- Total records available from server-->
|
||||
<field id="totalRecordCount" type="int" value="-1" />
|
||||
<field id="content" type="array" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="LoadVideoContentTask.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/Items.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/UserLibrary.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/userauth.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
|
||||
</component>
|
33
components/RadioDialog.brs
Normal file
33
components/RadioDialog.brs
Normal file
|
@ -0,0 +1,33 @@
|
|||
sub init()
|
||||
m.content = m.top.findNode("content")
|
||||
m.top.observeField("contentData", "onContentDataChanged")
|
||||
|
||||
m.top.observeFieldScoped("buttonSelected", "onButtonSelected")
|
||||
|
||||
m.top.id = "OKDialog"
|
||||
m.top.height = 900
|
||||
m.top.title = "What's New?"
|
||||
m.top.buttons = [tr("OK")]
|
||||
end sub
|
||||
|
||||
sub onButtonSelected()
|
||||
if m.top.buttonSelected = 0
|
||||
m.global.sceneManager.returnData = m.top.contentData.data[m.content.selectedIndex]
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub onContentDataChanged()
|
||||
i = 0
|
||||
for each item in m.top.contentData.data
|
||||
cardItem = m.content.CreateChild("StdDlgActionCardItem")
|
||||
cardItem.iconType = "radiobutton"
|
||||
|
||||
if isValid(item.selected)
|
||||
m.content.selectedIndex = i
|
||||
end if
|
||||
|
||||
textLine = cardItem.CreateChild("SimpleLabel")
|
||||
textLine.text = item.description
|
||||
i++
|
||||
end for
|
||||
end sub
|
13
components/RadioDialog.xml
Normal file
13
components/RadioDialog.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="RadioDialog" extends="StandardMessageDialog">
|
||||
<children>
|
||||
<StdDlgContentArea>
|
||||
<StdDlgItemGroup id="content" />
|
||||
</StdDlgContentArea>
|
||||
</children>
|
||||
<interface>
|
||||
<field id="contentData" type="assocarray" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="RadioDialog.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
</component>
|
36
components/StandardDialog.brs
Normal file
36
components/StandardDialog.brs
Normal file
|
@ -0,0 +1,36 @@
|
|||
sub init()
|
||||
m.content = m.top.findNode("content")
|
||||
m.top.observeField("contentData", "onContentDataChanged")
|
||||
|
||||
m.top.id = "OKDialog"
|
||||
m.top.height = 900
|
||||
m.top.title = "What's New?"
|
||||
m.top.buttons = [tr("OK")]
|
||||
|
||||
m.dialogStyles = {
|
||||
"default": {
|
||||
"fontSize": 27,
|
||||
"fontUri": "font:BoldSystemFontFile",
|
||||
"color": "#EFEFEFFF"
|
||||
},
|
||||
"b": {
|
||||
"fontSize": 27,
|
||||
"fontUri": "font:SystemFontFile",
|
||||
"color": "#999999"
|
||||
},
|
||||
"header": {
|
||||
"fontSize": 35,
|
||||
"fontUri": "font:SystemFontFile",
|
||||
"color": "#00a4dcFF"
|
||||
}
|
||||
}
|
||||
|
||||
end sub
|
||||
|
||||
sub onContentDataChanged()
|
||||
for each item in m.top.contentData.data
|
||||
textLine = m.content.CreateChild("StdDlgMultiStyleTextItem")
|
||||
textLine.drawingStyles = m.dialogStyles
|
||||
textLine.text = item
|
||||
end for
|
||||
end sub
|
10
components/StandardDialog.xml
Normal file
10
components/StandardDialog.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="StandardDialog" extends="StandardMessageDialog">
|
||||
<children>
|
||||
<StdDlgContentArea id="content" />
|
||||
</children>
|
||||
<interface>
|
||||
<field id="contentData" type="assocarray" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="StandardDialog.brs" />
|
||||
</component>
|
15
components/data/PlaylistData.brs
Normal file
15
components/data/PlaylistData.brs
Normal file
|
@ -0,0 +1,15 @@
|
|||
sub setFields()
|
||||
datum = m.top.json
|
||||
|
||||
m.top.id = datum.id
|
||||
m.top.title = datum.name
|
||||
m.top.overview = datum.overview
|
||||
end sub
|
||||
|
||||
sub setPoster()
|
||||
if m.top.image <> invalid
|
||||
m.top.posterURL = m.top.image.url
|
||||
else
|
||||
m.top.posterURL = ""
|
||||
end if
|
||||
end sub
|
10
components/data/PlaylistData.xml
Normal file
10
components/data/PlaylistData.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="PlaylistData" extends="JFContentItem">
|
||||
<interface>
|
||||
<field id="id" type="string" />
|
||||
<field id="title" type="string" />
|
||||
<field id="image" type="node" onChange="setPoster" />
|
||||
<field id="overview" type="string" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="PlaylistData.brs" />
|
||||
</component>
|
|
@ -237,7 +237,7 @@ end sub
|
|||
'
|
||||
' Display dialog to user with an OK button
|
||||
sub userMessage(title as string, message as string)
|
||||
dialog = createObject("roSGNode", "Dialog")
|
||||
dialog = createObject("roSGNode", "StandardMessageDialog")
|
||||
dialog.title = title
|
||||
dialog.message = message
|
||||
dialog.buttons = [tr("OK")]
|
||||
|
@ -245,9 +245,101 @@ sub userMessage(title as string, message as string)
|
|||
m.scene.dialog = dialog
|
||||
end sub
|
||||
|
||||
'
|
||||
' Display dialog to user with an OK button
|
||||
sub standardDialog(title, message)
|
||||
dialog = createObject("roSGNode", "StandardDialog")
|
||||
dlgPalette = createObject("roSGNode", "RSGPalette")
|
||||
dlgPalette.colors = {
|
||||
DialogBackgroundColor: "0x262828FF",
|
||||
DialogFocusColor: "0xcececeFF",
|
||||
DialogFocusItemColor: "0x202020FF",
|
||||
DialogSecondaryTextColor: "0xf8f8f8ff",
|
||||
DialogSecondaryItemColor: "#00a4dcFF",
|
||||
DialogTextColor: "0xeeeeeeFF"
|
||||
}
|
||||
dialog.palette = dlgPalette
|
||||
dialog.observeField("buttonSelected", "dismiss_dialog")
|
||||
dialog.title = title
|
||||
dialog.contentData = message
|
||||
dialog.buttons = [tr("OK")]
|
||||
|
||||
m.scene.dialog = dialog
|
||||
end sub
|
||||
|
||||
'
|
||||
' Display dialog to user with an OK button
|
||||
sub radioDialog(title, message)
|
||||
dialog = createObject("roSGNode", "RadioDialog")
|
||||
dlgPalette = createObject("roSGNode", "RSGPalette")
|
||||
dlgPalette.colors = {
|
||||
DialogBackgroundColor: "0x262828FF",
|
||||
DialogFocusColor: "0xcececeFF",
|
||||
DialogFocusItemColor: "0x202020FF",
|
||||
DialogSecondaryTextColor: "0xf8f8f8ff",
|
||||
DialogSecondaryItemColor: "#00a4dcFF",
|
||||
DialogTextColor: "0xeeeeeeFF"
|
||||
}
|
||||
dialog.palette = dlgPalette
|
||||
dialog.observeField("buttonSelected", "dismiss_dialog")
|
||||
dialog.title = title
|
||||
dialog.contentData = message
|
||||
dialog.buttons = [tr("OK")]
|
||||
|
||||
m.scene.dialog = dialog
|
||||
end sub
|
||||
|
||||
'
|
||||
' Display dialog to user with an OK button
|
||||
sub optionDialog(title, message, buttons)
|
||||
m.top.returnData = invalid
|
||||
m.userselection = false
|
||||
|
||||
dialog = createObject("roSGNode", "StandardMessageDialog")
|
||||
dlgPalette = createObject("roSGNode", "RSGPalette")
|
||||
dlgPalette.colors = {
|
||||
DialogBackgroundColor: "0x262828FF",
|
||||
DialogFocusColor: "0xcececeFF",
|
||||
DialogFocusItemColor: "0x202020FF",
|
||||
DialogSecondaryTextColor: "0xf8f8f8ff",
|
||||
DialogSecondaryItemColor: "#00a4dcFF",
|
||||
DialogTextColor: "0xeeeeeeFF"
|
||||
}
|
||||
dialog.palette = dlgPalette
|
||||
dialog.observeField("buttonSelected", "optionSelected")
|
||||
dialog.observeField("wasClosed", "optionClosed")
|
||||
dialog.title = title
|
||||
dialog.message = message
|
||||
dialog.buttons = buttons
|
||||
|
||||
m.scene.dialog = dialog
|
||||
end sub
|
||||
|
||||
'
|
||||
' Return button the user selected
|
||||
sub optionClosed()
|
||||
if m.userselection then return
|
||||
|
||||
m.top.returnData = {
|
||||
indexSelected: -1,
|
||||
buttonSelected: ""
|
||||
}
|
||||
end sub
|
||||
|
||||
'
|
||||
' Return button the user selected
|
||||
sub optionSelected()
|
||||
m.userselection = true
|
||||
m.top.returnData = {
|
||||
indexSelected: m.scene.dialog.buttonSelected,
|
||||
buttonSelected: m.scene.dialog.buttons[m.scene.dialog.buttonSelected]
|
||||
}
|
||||
|
||||
dismiss_dialog()
|
||||
end sub
|
||||
|
||||
'
|
||||
' Close currently displayed dialog
|
||||
sub dismiss_dialog()
|
||||
print "Button Pressed"
|
||||
m.scene.dialog.close = true
|
||||
end sub
|
||||
|
|
|
@ -9,7 +9,11 @@
|
|||
<function name="clearPreviousScene" />
|
||||
<function name="resetTime" />
|
||||
<function name="userMessage" />
|
||||
<function name="standardDialog" />
|
||||
<function name="radioDialog" />
|
||||
<function name="optionDialog" />
|
||||
<field id="currentUser" type="string" onChange="updateUser" />
|
||||
<field id="returnData" type="assocarray" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="SceneManager.brs" />
|
||||
</component>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
sub init()
|
||||
m.queue = []
|
||||
m.queueTypes = []
|
||||
m.position = 0
|
||||
end sub
|
||||
|
||||
|
@ -7,6 +8,7 @@ end sub
|
|||
' Clear all content from play queue
|
||||
sub clear()
|
||||
m.queue = []
|
||||
m.queueTypes = []
|
||||
setPosition(0)
|
||||
end sub
|
||||
|
||||
|
@ -15,6 +17,7 @@ end sub
|
|||
' Delete item from play queue at passed index
|
||||
sub deleteAtIndex(index)
|
||||
m.queue.Delete(index)
|
||||
m.queueTypes.Delete(index)
|
||||
end sub
|
||||
|
||||
|
||||
|
@ -67,6 +70,28 @@ function getQueue()
|
|||
end function
|
||||
|
||||
|
||||
'
|
||||
' Return the types of items in current play queue
|
||||
function getQueueTypes()
|
||||
return m.queueTypes
|
||||
end function
|
||||
|
||||
|
||||
'
|
||||
' Return the unique types of items in current play queue
|
||||
function getQueueUniqueTypes()
|
||||
itemTypes = []
|
||||
|
||||
for each item in getQueueTypes()
|
||||
if not inArray(itemTypes, item)
|
||||
itemTypes.push(item)
|
||||
end if
|
||||
end for
|
||||
|
||||
return itemTypes
|
||||
end function
|
||||
|
||||
|
||||
'
|
||||
' Return item at end of play queue without removing
|
||||
function peek()
|
||||
|
@ -77,19 +102,17 @@ end function
|
|||
'
|
||||
' Play items in queue
|
||||
sub playQueue()
|
||||
nextItem = top()
|
||||
nextItemMediaType = invalid
|
||||
|
||||
if isValid(nextItem?.json?.mediatype) and nextItem.json.mediatype <> ""
|
||||
nextItemMediaType = LCase(nextItem.json.mediatype)
|
||||
else if isValid(nextItem?.type) and nextItem.type <> ""
|
||||
nextItemMediaType = LCase(nextItem.type)
|
||||
end if
|
||||
nextItem = getCurrentItem()
|
||||
nextItemMediaType = getItemType(nextItem)
|
||||
|
||||
if not isValid(nextItemMediaType) then return
|
||||
|
||||
if nextItemMediaType = "audio"
|
||||
CreateAudioPlayerView()
|
||||
else if nextItemMediaType = "video"
|
||||
CreateVideoPlayerView()
|
||||
else if nextItemMediaType = "episode"
|
||||
CreateVideoPlayerView()
|
||||
end if
|
||||
end sub
|
||||
|
||||
|
@ -98,6 +121,7 @@ end sub
|
|||
' Remove item at end of play queue
|
||||
sub pop()
|
||||
m.queue.pop()
|
||||
m.queueTypes.pop()
|
||||
end sub
|
||||
|
||||
|
||||
|
@ -105,6 +129,7 @@ end sub
|
|||
' Push new items to the play queue
|
||||
sub push(newItem)
|
||||
m.queue.push(newItem)
|
||||
m.queueTypes.push(getItemType(newItem))
|
||||
end sub
|
||||
|
||||
'
|
||||
|
@ -126,4 +151,18 @@ end function
|
|||
sub set(items)
|
||||
setPosition(0)
|
||||
m.queue = items
|
||||
for each item in items
|
||||
m.queueTypes.push(getItemType(item))
|
||||
end for
|
||||
end sub
|
||||
|
||||
function getItemType(item) as string
|
||||
|
||||
if isValid(item?.json?.mediatype) and item.json.mediatype <> ""
|
||||
return LCase(item.json.mediatype)
|
||||
else if isValid(item?.type) and item.type <> ""
|
||||
return LCase(item.type)
|
||||
end if
|
||||
|
||||
return invalid
|
||||
end function
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
<function name="getItemByIndex" />
|
||||
<function name="getPosition" />
|
||||
<function name="getQueue" />
|
||||
<function name="getQueueTypes" />
|
||||
<function name="getQueueUniqueTypes" />
|
||||
<function name="moveBack" />
|
||||
<function name="moveForward" />
|
||||
<function name="peek" />
|
||||
|
|
|
@ -1,6 +1,118 @@
|
|||
'
|
||||
' View Creators
|
||||
' ----------------
|
||||
|
||||
' Play Audio
|
||||
sub CreateAudioPlayerView()
|
||||
view = CreateObject("roSGNode", "AudioPlayerView")
|
||||
view.observeField("state", m.port)
|
||||
m.global.sceneManager.callFunc("pushScene", view)
|
||||
m.view = CreateObject("roSGNode", "AudioPlayerView")
|
||||
m.view.observeField("state", "onStateChange")
|
||||
m.global.sceneManager.callFunc("pushScene", m.view)
|
||||
end sub
|
||||
|
||||
' Play Video
|
||||
sub CreateVideoPlayerView()
|
||||
m.playbackData = {}
|
||||
m.selectedSubtitle = {}
|
||||
|
||||
m.view = CreateObject("roSGNode", "VideoPlayerView")
|
||||
m.view.observeField("state", "onStateChange")
|
||||
m.view.observeField("selectPlaybackInfoPressed", "onSelectPlaybackInfoPressed")
|
||||
m.view.observeField("selectSubtitlePressed", "onSelectSubtitlePressed")
|
||||
|
||||
m.getPlaybackInfoTask = createObject("roSGNode", "GetPlaybackInfoTask")
|
||||
m.getPlaybackInfoTask.videoID = m.global.queueManager.callFunc("getCurrentItem").id
|
||||
m.getPlaybackInfoTask.observeField("data", "onPlaybackInfoLoaded")
|
||||
|
||||
m.global.sceneManager.callFunc("pushScene", m.view)
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Event Handlers
|
||||
' -----------------
|
||||
|
||||
' User requested subtitle selection popup
|
||||
sub onSelectSubtitlePressed()
|
||||
|
||||
' None is always first in the subtitle list
|
||||
subtitleData = {
|
||||
data: [{ "description": "None", "type": "subtitleselection" }]
|
||||
}
|
||||
|
||||
for each item in m.view.content.subtitletracks
|
||||
item.type = "subtitleselection"
|
||||
|
||||
if item.description = m.selectedSubtitle.description
|
||||
item.selected = true
|
||||
end if
|
||||
|
||||
subtitleData.data.push(item)
|
||||
end for
|
||||
|
||||
m.global.sceneManager.callFunc("radioDialog", tr("Select Subtitles"), subtitleData)
|
||||
m.global.sceneManager.observeField("returnData", "onSelectionMade")
|
||||
end sub
|
||||
|
||||
' User has selected something from the radioDialog popup
|
||||
sub onSelectionMade()
|
||||
m.global.sceneManager.unobserveField("returnData")
|
||||
|
||||
if not isValid(m.global.sceneManager.returnData) then return
|
||||
if not isValid(m.global.sceneManager.returnData.type) then return
|
||||
|
||||
if LCase(m.global.sceneManager.returnData.type) = "subtitleselection"
|
||||
processSubtitleSelection()
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub processSubtitleSelection()
|
||||
m.selectedSubtitle = m.global.sceneManager.returnData
|
||||
|
||||
if LCase(m.selectedSubtitle.description) = "none"
|
||||
m.view.globalCaptionMode = "Off"
|
||||
m.view.subtitleTrack = ""
|
||||
return
|
||||
end if
|
||||
|
||||
m.view.globalCaptionMode = "On"
|
||||
m.view.subtitleTrack = m.selectedSubtitle.TrackName
|
||||
end sub
|
||||
|
||||
' User requested playback info
|
||||
sub onSelectPlaybackInfoPressed()
|
||||
|
||||
' Check if we already have playback info and show it in a popup
|
||||
if isValid(m.playbackData?.playbackinfo)
|
||||
m.global.sceneManager.callFunc("standardDialog", tr("Playback Info"), m.playbackData.playbackinfo)
|
||||
return
|
||||
end if
|
||||
|
||||
m.getPlaybackInfoTask.control = "RUN"
|
||||
end sub
|
||||
|
||||
' The playback info task has returned data
|
||||
sub onPlaybackInfoLoaded()
|
||||
m.playbackData = m.getPlaybackInfoTask.data
|
||||
|
||||
' Check if we have playback info and show it in a popup
|
||||
if isValid(m.playbackData?.playbackinfo)
|
||||
m.global.sceneManager.callFunc("standardDialog", tr("Playback Info"), m.playbackData.playbackinfo)
|
||||
end if
|
||||
end sub
|
||||
|
||||
|
||||
' Playback state change event handlers
|
||||
sub onStateChange()
|
||||
if LCase(m.view.state) = "finished"
|
||||
' If there is something next in the queue, play it
|
||||
if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
|
||||
m.global.sceneManager.callFunc("clearPreviousScene")
|
||||
m.global.queueManager.callFunc("moveForward")
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
return
|
||||
end if
|
||||
|
||||
' Playback completed, return user to previous screen
|
||||
m.global.sceneManager.callFunc("popScene")
|
||||
end if
|
||||
end sub
|
||||
|
|
|
@ -8,6 +8,8 @@ sub init()
|
|||
setupDataTasks()
|
||||
setupScreenSaver()
|
||||
|
||||
m.playlistTypeCount = m.global.queueManager.callFunc("getQueueUniqueTypes").count()
|
||||
|
||||
m.shuffleEnabled = false
|
||||
m.loopMode = ""
|
||||
m.buttonCount = m.buttons.getChildCount()
|
||||
|
@ -226,8 +228,7 @@ sub audioStateChanged()
|
|||
end if
|
||||
|
||||
if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
|
||||
' We are not at the end of the song queue, advance to next song
|
||||
LoadNextSong()
|
||||
m.top.state = "finished"
|
||||
else
|
||||
' We are at the end of the song queue
|
||||
|
||||
|
@ -266,6 +267,8 @@ function playAction() as boolean
|
|||
end function
|
||||
|
||||
function previousClicked() as boolean
|
||||
if m.playlistTypeCount > 1 then return false
|
||||
|
||||
if m.top.audio.state = "playing"
|
||||
m.top.audio.control = "stop"
|
||||
end if
|
||||
|
@ -296,6 +299,8 @@ function loopClicked() as boolean
|
|||
end function
|
||||
|
||||
function nextClicked() as boolean
|
||||
if m.playlistTypeCount > 1 then return false
|
||||
|
||||
if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
|
||||
LoadNextSong()
|
||||
end if
|
||||
|
@ -415,6 +420,9 @@ end sub
|
|||
|
||||
' If we have more and 1 song to play, fade in the next and previous controls
|
||||
sub loadButtons()
|
||||
' Don't show audio buttons if we have a mixed playlist
|
||||
if m.playlistTypeCount > 1 then return
|
||||
|
||||
if m.global.queueManager.callFunc("getCount") > 1
|
||||
m.shuffleIndicator.opacity = ".4"
|
||||
m.loopIndicator.opacity = ".4"
|
||||
|
@ -504,7 +512,11 @@ sub setOnScreenTextValues(json)
|
|||
if m.shuffleEnabled
|
||||
currentSongIndex = findCurrentSongIndex(m.originalSongList)
|
||||
end if
|
||||
setFieldTextValue("numberofsongs", "Track " + stri(currentSongIndex + 1) + "/" + stri(m.global.queueManager.callFunc("getCount")))
|
||||
|
||||
if m.playlistTypeCount = 1
|
||||
setFieldTextValue("numberofsongs", "Track " + stri(currentSongIndex + 1) + "/" + stri(m.global.queueManager.callFunc("getCount")))
|
||||
end if
|
||||
|
||||
setFieldTextValue("artist", json.Artists[0])
|
||||
setFieldTextValue("song", json.name)
|
||||
end if
|
||||
|
|
166
components/music/PlaylistView.brs
Normal file
166
components/music/PlaylistView.brs
Normal file
|
@ -0,0 +1,166 @@
|
|||
sub init()
|
||||
m.top.optionsAvailable = false
|
||||
setupMainNode()
|
||||
|
||||
m.playAll = m.top.findNode("playAll")
|
||||
m.albumCover = m.top.findNode("albumCover")
|
||||
m.songList = m.top.findNode("songList")
|
||||
m.infoGroup = m.top.FindNode("infoGroup")
|
||||
m.songListRect = m.top.FindNode("songListRect")
|
||||
|
||||
m.songList.observeField("doneLoading", "onDoneLoading")
|
||||
m.spinner = m.top.findNode("spinner")
|
||||
m.spinner.visible = true
|
||||
|
||||
m.dscr = m.top.findNode("overview")
|
||||
createDialogPallete()
|
||||
end sub
|
||||
|
||||
sub setupMainNode()
|
||||
main = m.top.findNode("toplevel")
|
||||
main.translation = [96, 175]
|
||||
end sub
|
||||
|
||||
' Set values for displayed values on screen
|
||||
sub pageContentChanged()
|
||||
item = m.top.pageContent
|
||||
|
||||
setPosterImage(item.posterURL)
|
||||
setScreenTitle(item.json)
|
||||
setOnScreenTextValues(item.json)
|
||||
|
||||
' Only 1 song shown, so hide Play Album button
|
||||
if item.json.ChildCount = 1
|
||||
m.playAll.visible = false
|
||||
end if
|
||||
end sub
|
||||
|
||||
' Set poster image on screen
|
||||
sub setPosterImage(posterURL)
|
||||
if isValid(posterURL)
|
||||
m.albumCover.uri = posterURL
|
||||
end if
|
||||
end sub
|
||||
|
||||
' Set screen's title text
|
||||
sub setScreenTitle(json)
|
||||
newTitle = ""
|
||||
if isValid(json)
|
||||
if isValid(json.AlbumArtist)
|
||||
newTitle = json.AlbumArtist
|
||||
end if
|
||||
if isValid(json.AlbumArtist) and isValid(json.name)
|
||||
newTitle = newTitle + " / "
|
||||
end if
|
||||
if isValid(json.name)
|
||||
newTitle = newTitle + json.name
|
||||
end if
|
||||
end if
|
||||
m.top.overhangTitle = newTitle
|
||||
end sub
|
||||
|
||||
' Adjust scene by removing overview node and showing more songs
|
||||
sub adjustScreenForNoOverview()
|
||||
m.infoGroup.removeChild(m.dscr)
|
||||
m.songListRect.height = 800
|
||||
m.songList.numRows = 12
|
||||
end sub
|
||||
|
||||
' Populate on screen text variables
|
||||
sub setOnScreenTextValues(json)
|
||||
if isValid(json)
|
||||
if isValid(json.overview) and json.overview <> ""
|
||||
' We have overview text
|
||||
setFieldTextValue("overview", json.overview)
|
||||
else
|
||||
' We don't have overview text
|
||||
adjustScreenForNoOverview()
|
||||
end if
|
||||
|
||||
setFieldTextValue("numberofsongs", stri(json.ChildCount) + " Tracks")
|
||||
|
||||
if type(json.ProductionYear) = "roInt"
|
||||
setFieldTextValue("released", "Released " + stri(json.ProductionYear))
|
||||
end if
|
||||
|
||||
if json.genres.count() > 0
|
||||
setFieldTextValue("genres", json.genres.join(", "))
|
||||
end if
|
||||
|
||||
if type(json.RunTimeTicks) = "LongInteger"
|
||||
setFieldTextValue("runtime", stri(getMinutes(json.RunTimeTicks)) + " mins")
|
||||
end if
|
||||
end if
|
||||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
if not press then return false
|
||||
|
||||
if m.spinner.visible then return false
|
||||
|
||||
if key = "options"
|
||||
if m.dscr.isTextEllipsized
|
||||
createFullDscrDlg()
|
||||
return true
|
||||
end if
|
||||
return false
|
||||
end if
|
||||
|
||||
if key = "right"
|
||||
if m.playAll.hasFocus()
|
||||
m.songList.setFocus(true)
|
||||
return true
|
||||
end if
|
||||
else if key = "left" and m.songList.hasFocus()
|
||||
if m.playAll.visible
|
||||
m.playAll.setFocus(true)
|
||||
else
|
||||
return false
|
||||
end if
|
||||
return true
|
||||
end if
|
||||
|
||||
return false
|
||||
end function
|
||||
|
||||
sub createFullDscrDlg()
|
||||
dlg = CreateObject("roSGNode", "OverviewDialog")
|
||||
dlg.Title = tr("Press 'Back' to Close")
|
||||
dlg.width = 1290
|
||||
dlg.palette = m.dlgPalette
|
||||
dlg.overview = [m.dscr.text]
|
||||
m.fullDscrDlg = dlg
|
||||
m.top.getScene().dialog = dlg
|
||||
border = createObject("roSGNode", "Poster")
|
||||
border.uri = "pkg:/images/hd_focul_9.png"
|
||||
border.blendColor = "#c9c9c9ff"
|
||||
border.width = dlg.width + 6
|
||||
border.height = dlg.height + 6
|
||||
border.translation = [dlg.translation[0] - 3, dlg.translation[1] - 3]
|
||||
border.visible = true
|
||||
end sub
|
||||
|
||||
sub createDialogPallete()
|
||||
m.dlgPalette = createObject("roSGNode", "RSGPalette")
|
||||
m.dlgPalette.colors = {
|
||||
DialogBackgroundColor: "0x262828FF",
|
||||
DialogItemColor: "0x00EF00FF",
|
||||
DialogTextColor: "0xb0b0b0FF",
|
||||
DialogFocusColor: "0xcececeFF",
|
||||
DialogFocusItemColor: "0x202020FF",
|
||||
DialogSecondaryTextColor: "0xf8f8f8ff",
|
||||
DialogSecondaryItemColor: "0xcc7ecc4D",
|
||||
DialogInputFieldColor: "0x80FF8080",
|
||||
DialogKeyboardColor: "0x80FF804D",
|
||||
DialogFootprintColor: "0x80FF804D"
|
||||
}
|
||||
end sub
|
||||
|
||||
sub onDoneLoading()
|
||||
m.songList.unobservefield("doneLoading")
|
||||
m.spinner.visible = false
|
||||
end sub
|
||||
|
||||
sub OnScreenHidden()
|
||||
m.spinner.visible = false
|
||||
end sub
|
32
components/music/PlaylistView.xml
Normal file
32
components/music/PlaylistView.xml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="PlaylistView" extends="JFScreen">
|
||||
<children>
|
||||
<LayoutGroup id="toplevel" layoutDirection="vert" itemSpacings="[-10]">
|
||||
<LayoutGroup id="main_group" layoutDirection="horiz" itemSpacings="[15]">
|
||||
<LayoutGroup layoutDirection="vert" itemSpacings="[15]">
|
||||
<Poster id="albumCover" width="450" height="450" />
|
||||
<Label id="numberofsongs" width="450" height="25" />
|
||||
<Label id="genres" width="450" height="25" />
|
||||
<Label id="runtime" width="450" height="25" />
|
||||
<Label id="released" width="450" height="25" />
|
||||
<JFButton id="playAll" minChars="8" text="Play All"></JFButton>
|
||||
</LayoutGroup>
|
||||
<LayoutGroup id="infoGroup" layoutDirection="vert" itemSpacings="[15]">
|
||||
<Label id="overview" wrap="true" height="310" width="1250" ellipsisText=" ... (Press * to read more)" />
|
||||
<Rectangle id='songListRect' translation="[-30, 0]" width="1260" height="510" color="0x202020ff">
|
||||
<AlbumTrackList itemComponentName="SongItem" id="songList" translation="[45, 25]" itemSize="[1170,60]" numRows="7" />
|
||||
</Rectangle>
|
||||
</LayoutGroup>
|
||||
</LayoutGroup>
|
||||
</LayoutGroup>
|
||||
<Spinner id="spinner" translation="[920, 540]" visible="false" />
|
||||
</children>
|
||||
<interface>
|
||||
<field id="pageContent" type="node" onChange="pageContentChanged" />
|
||||
<field id="albumData" type="assocarray" alias="songList.MusicArtistAlbumData" />
|
||||
<field id="playItem" alias="songList.itemSelected" />
|
||||
<field id="playAllSelected" alias="playAll.buttonSelected" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="PlaylistView.brs" />
|
||||
</component>
|
|
@ -3,6 +3,7 @@ sub init()
|
|||
|
||||
m.rows = m.top.findNode("picker")
|
||||
m.poster = m.top.findNode("seasonPoster")
|
||||
m.Shuffle = m.top.findNode("Shuffle")
|
||||
m.Random = m.top.findNode("Random")
|
||||
m.tvEpisodeRow = m.top.findNode("tvEpisodeRow")
|
||||
|
||||
|
@ -29,17 +30,28 @@ sub updateSeason()
|
|||
imgParams = { "maxHeight": 450, "maxWidth": 300 }
|
||||
m.poster.uri = ImageURL(m.top.seasonData.Id, "Primary", imgParams)
|
||||
m.Random.visible = true
|
||||
m.Shuffle.visible = true
|
||||
m.top.overhangTitle = m.top.seasonData.SeriesName + " - " + m.top.seasonData.name
|
||||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
handled = false
|
||||
|
||||
if key = "left" and not m.Random.hasFocus()
|
||||
if key = "left" and not m.Shuffle.hasFocus()
|
||||
m.Shuffle.setFocus(true)
|
||||
return true
|
||||
end if
|
||||
|
||||
if key = "down" and m.Shuffle.hasFocus()
|
||||
m.Random.setFocus(true)
|
||||
return true
|
||||
end if
|
||||
|
||||
if key = "up" and m.Random.hasFocus()
|
||||
m.Shuffle.setFocus(true)
|
||||
return true
|
||||
end if
|
||||
|
||||
if key = "right" and not m.tvEpisodeRow.hasFocus()
|
||||
m.tvEpisodeRow.setFocus(true)
|
||||
return true
|
||||
|
@ -52,6 +64,21 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
m.top.quickPlayNode = m.rows.getChild(0).objects.items[randomEpisode]
|
||||
return true
|
||||
end if
|
||||
|
||||
if m.Shuffle.hasFocus()
|
||||
episodeList = m.rows.getChild(0).objects.items
|
||||
|
||||
for i = 0 to episodeList.count() - 1
|
||||
j = Rnd(episodeList.count() - 1)
|
||||
temp = episodeList[i]
|
||||
episodeList[i] = episodeList[j]
|
||||
episodeList[j] = temp
|
||||
end for
|
||||
|
||||
m.global.queueManager.callFunc("set", episodeList)
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
return true
|
||||
end if
|
||||
end if
|
||||
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:SmallestBoldSystemFont" horizAlign="center" vertAlign="center" />
|
||||
</Rectangle>
|
||||
</Poster>
|
||||
<JFButton id="Random" text="Play Random" translation="[90, 640]" visible="false"></JFButton>
|
||||
<JFButton id="Shuffle" minChars="10" text="Shuffle" translation="[90, 640]" visible="false"></JFButton>
|
||||
<JFButton id="Random" minChars="12" text="Play Random" translation="[90, 740]" visible="false"></JFButton>
|
||||
<TVEpisodeRowWithOptions id="picker" visible="true" />
|
||||
</children>
|
||||
<interface>
|
||||
|
|
|
@ -5,7 +5,8 @@ sub init()
|
|||
m.extrasSlider = m.top.findNode("tvSeasonExtras")
|
||||
m.unplayedCount = m.top.findNode("unplayedCount")
|
||||
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
|
||||
'm.extrasSlider.translation = [30,1014]
|
||||
m.getShuffleEpisodesTask = createObject("roSGNode", "getShuffleEpisodesTask")
|
||||
m.Shuffle = m.top.findNode("Shuffle")
|
||||
m.extrasSlider.visible = true
|
||||
end sub
|
||||
|
||||
|
@ -55,6 +56,7 @@ sub itemContentChanged()
|
|||
|
||||
setFieldText("overview", itemData.overview)
|
||||
|
||||
m.Shuffle.visible = true
|
||||
|
||||
if type(itemData.RunTimeTicks) = "LongInteger"
|
||||
setFieldText("runtime", stri(getRuntime()) + " mins")
|
||||
|
@ -170,7 +172,22 @@ function round(f as float) as integer
|
|||
end if
|
||||
end function
|
||||
|
||||
sub onShuffleEpisodeDataLoaded()
|
||||
m.getShuffleEpisodesTask.unobserveField("data")
|
||||
m.global.queueManager.callFunc("set", m.getShuffleEpisodesTask.data.items)
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
if key = "OK" or key = "play"
|
||||
if m.Shuffle.hasFocus()
|
||||
m.getShuffleEpisodesTask.showID = m.top.itemContent.id
|
||||
m.getShuffleEpisodesTask.observeField("data", "onShuffleEpisodeDataLoaded")
|
||||
m.getShuffleEpisodesTask.control = "RUN"
|
||||
return true
|
||||
end if
|
||||
end if
|
||||
|
||||
if not press then return false
|
||||
|
||||
overview = m.top.findNode("overview")
|
||||
|
@ -178,6 +195,9 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
bottomGrp = m.top.findNode("extrasGrid")
|
||||
|
||||
if key = "down" and overview.hasFocus()
|
||||
m.Shuffle.setFocus(true)
|
||||
return true
|
||||
else if key = "down" and m.Shuffle.hasFocus()
|
||||
topGrp.setFocus(true)
|
||||
return true
|
||||
else if key = "down" and topGrp.hasFocus()
|
||||
|
@ -195,6 +215,9 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
|||
return true
|
||||
end if
|
||||
else if key = "up" and topGrp.hasFocus()
|
||||
m.Shuffle.setFocus(true)
|
||||
return true
|
||||
else if key = "up" and m.Shuffle.hasFocus()
|
||||
overview.setFocus(true)
|
||||
return true
|
||||
end if
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
<Label id="tagline" />
|
||||
<Label id="overview" wrap="true" width="1400" maxLines="4" />
|
||||
<Label id="history" />
|
||||
<JFButton id="Shuffle" minChars="15" text="Shuffle" translation="[90, 640]" visible="false"></JFButton>
|
||||
</LayoutGroup>
|
||||
</LayoutGroup>
|
||||
<TVSeasonRow id="seasons" />
|
||||
|
|
308
components/video/VideoPlayerView.brs
Normal file
308
components/video/VideoPlayerView.brs
Normal file
|
@ -0,0 +1,308 @@
|
|||
sub init()
|
||||
currentItem = m.global.queueManager.callFunc("getCurrentItem")
|
||||
|
||||
m.top.id = currentItem.id
|
||||
|
||||
' Load meta data
|
||||
m.LoadMetaDataTask = CreateObject("roSGNode", "LoadVideoContentTask")
|
||||
m.LoadMetaDataTask.itemId = currentItem.id
|
||||
m.LoadMetaDataTask.observeField("content", "onVideoContentLoaded")
|
||||
m.LoadMetaDataTask.control = "RUN"
|
||||
|
||||
m.playbackTimer = m.top.findNode("playbackTimer")
|
||||
m.bufferCheckTimer = m.top.findNode("bufferCheckTimer")
|
||||
m.top.observeField("state", "onState")
|
||||
m.top.observeField("content", "onContentChange")
|
||||
|
||||
m.playbackTimer.observeField("fire", "ReportPlayback")
|
||||
m.bufferPercentage = 0 ' Track whether content is being loaded
|
||||
m.playReported = false
|
||||
m.top.transcodeReasons = []
|
||||
m.bufferCheckTimer.duration = 30
|
||||
|
||||
if get_user_setting("ui.design.hideclock") = "true"
|
||||
clockNode = findNodeBySubtype(m.top, "clock")
|
||||
if clockNode[0] <> invalid then clockNode[0].parent.removeChild(clockNode[0].node)
|
||||
end if
|
||||
|
||||
'Play Next Episode button
|
||||
m.nextEpisodeButton = m.top.findNode("nextEpisode")
|
||||
m.nextEpisodeButton.text = tr("Next Episode")
|
||||
m.nextEpisodeButton.setFocus(false)
|
||||
|
||||
m.showNextEpisodeButtonAnimation = m.top.findNode("showNextEpisodeButton")
|
||||
m.hideNextEpisodeButtonAnimation = m.top.findNode("hideNextEpisodeButton")
|
||||
|
||||
m.checkedForNextEpisode = false
|
||||
m.getNextEpisodeTask = createObject("roSGNode", "GetNextEpisodeTask")
|
||||
m.getNextEpisodeTask.observeField("nextEpisodeData", "onNextEpisodeDataLoaded")
|
||||
|
||||
m.top.retrievingBar.filledBarBlendColor = m.global.constants.colors.blue
|
||||
m.top.bufferingBar.filledBarBlendColor = m.global.constants.colors.blue
|
||||
m.top.trickPlayBar.filledBarBlendColor = m.global.constants.colors.blue
|
||||
end sub
|
||||
|
||||
sub onVideoContentLoaded()
|
||||
m.LoadMetaDataTask.unobserveField("content")
|
||||
|
||||
' If we have nothing to play, return to previous screen
|
||||
if not isValid(m.LoadMetaDataTask.content)
|
||||
m.global.sceneManager.callFunc("popScene")
|
||||
return
|
||||
end if
|
||||
|
||||
if not isValid(m.LoadMetaDataTask.content[0])
|
||||
m.global.sceneManager.callFunc("popScene")
|
||||
return
|
||||
end if
|
||||
|
||||
if m.LoadMetaDataTask.content.count() = 0
|
||||
m.global.sceneManager.callFunc("popScene")
|
||||
return
|
||||
end if
|
||||
|
||||
m.top.content = m.LoadMetaDataTask.content[0].content
|
||||
m.top.PlaySessionId = m.LoadMetaDataTask.content[0].PlaySessionId
|
||||
m.top.videoId = m.LoadMetaDataTask.content[0].id
|
||||
m.top.container = m.LoadMetaDataTask.content[0].container
|
||||
m.top.mediaSourceId = m.LoadMetaDataTask.content[0].mediaSourceId
|
||||
m.top.audioIndex = m.LoadMetaDataTask.content[0].audio_stream_idx
|
||||
|
||||
m.top.setFocus(true)
|
||||
m.top.control = "play"
|
||||
m.top.getScene().findNode("overhang").visible = false
|
||||
end sub
|
||||
|
||||
' Event handler for when video content field changes
|
||||
sub onContentChange()
|
||||
if not isValid(m.top.content) then return
|
||||
|
||||
m.top.observeField("position", "onPositionChanged")
|
||||
|
||||
' If video content type is not episode, remove position observer
|
||||
if m.top.content.contenttype <> 4
|
||||
m.top.unobserveField("position")
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub onNextEpisodeDataLoaded()
|
||||
m.checkedForNextEpisode = true
|
||||
|
||||
m.top.observeField("position", "onPositionChanged")
|
||||
|
||||
if m.getNextEpisodeTask.nextEpisodeData.Items.count() <> 2
|
||||
m.top.unobserveField("position")
|
||||
end if
|
||||
end sub
|
||||
|
||||
'
|
||||
' Runs Next Episode button animation and sets focus to button
|
||||
sub showNextEpisodeButton()
|
||||
if not m.nextEpisodeButton.visible
|
||||
m.showNextEpisodeButtonAnimation.control = "start"
|
||||
m.nextEpisodeButton.setFocus(true)
|
||||
m.nextEpisodeButton.visible = true
|
||||
end if
|
||||
end sub
|
||||
|
||||
'
|
||||
'Update count down text
|
||||
sub updateCount()
|
||||
m.nextEpisodeButton.text = tr("Next Episode") + " " + Int(m.top.duration - m.top.position).toStr()
|
||||
end sub
|
||||
|
||||
'
|
||||
' Runs hide Next Episode button animation and sets focus back to video
|
||||
sub hideNextEpisodeButton()
|
||||
m.hideNextEpisodeButtonAnimation.control = "start"
|
||||
m.nextEpisodeButton.setFocus(false)
|
||||
m.top.setFocus(true)
|
||||
end sub
|
||||
|
||||
' Checks if we need to display the Next Episode button
|
||||
sub checkTimeToDisplayNextEpisode()
|
||||
if int(m.top.position) >= (m.top.duration - 30)
|
||||
showNextEpisodeButton()
|
||||
updateCount()
|
||||
return
|
||||
end if
|
||||
|
||||
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
|
||||
m.nextEpisodeButton.visible = false
|
||||
m.nextEpisodeButton.setFocus(false)
|
||||
end if
|
||||
end sub
|
||||
|
||||
' When Video Player state changes
|
||||
sub onPositionChanged()
|
||||
' Check if dialog is open
|
||||
m.dialog = m.top.getScene().findNode("dialogBackground")
|
||||
if not isValid(m.dialog)
|
||||
checkTimeToDisplayNextEpisode()
|
||||
end if
|
||||
end sub
|
||||
|
||||
'
|
||||
' When Video Player state changes
|
||||
sub onState(msg)
|
||||
' When buffering, start timer to monitor buffering process
|
||||
if m.top.state = "buffering" and m.bufferCheckTimer <> invalid
|
||||
|
||||
' start timer
|
||||
m.bufferCheckTimer.control = "start"
|
||||
m.bufferCheckTimer.ObserveField("fire", "bufferCheck")
|
||||
else if m.top.state = "error"
|
||||
if not m.playReported and m.top.transcodeAvailable
|
||||
m.top.retryWithTranscoding = true ' If playback was not reported, retry with transcoding
|
||||
else
|
||||
' If an error was encountered, Display dialog
|
||||
dialog = createObject("roSGNode", "Dialog")
|
||||
dialog.title = tr("Error During Playback")
|
||||
dialog.buttons = [tr("OK")]
|
||||
dialog.message = tr("An error was encountered while playing this item.")
|
||||
dialog.observeField("buttonSelected", "dialogClosed")
|
||||
m.top.getScene().dialog = dialog
|
||||
end if
|
||||
|
||||
' Stop playback and exit player
|
||||
m.top.control = "stop"
|
||||
m.top.backPressed = true
|
||||
else if m.top.state = "playing"
|
||||
|
||||
' Check if next episde is available
|
||||
if isValid(m.top.showID)
|
||||
if m.top.showID <> "" and not m.checkedForNextEpisode and m.top.content.contenttype = 4
|
||||
m.getNextEpisodeTask.showID = m.top.showID
|
||||
m.getNextEpisodeTask.videoID = m.top.id
|
||||
m.getNextEpisodeTask.control = "RUN"
|
||||
end if
|
||||
end if
|
||||
|
||||
if m.playReported = false
|
||||
ReportPlayback("start")
|
||||
m.playReported = true
|
||||
else
|
||||
ReportPlayback()
|
||||
end if
|
||||
m.playbackTimer.control = "start"
|
||||
else if m.top.state = "paused"
|
||||
m.playbackTimer.control = "stop"
|
||||
ReportPlayback()
|
||||
else if m.top.state = "stopped"
|
||||
m.playbackTimer.control = "stop"
|
||||
ReportPlayback("stop")
|
||||
m.playReported = false
|
||||
end if
|
||||
|
||||
end sub
|
||||
|
||||
'
|
||||
' Report playback to server
|
||||
sub ReportPlayback(state = "update" as string)
|
||||
|
||||
if m.top.position = invalid then return
|
||||
|
||||
params = {
|
||||
"ItemId": m.top.id,
|
||||
"PlaySessionId": m.top.PlaySessionId,
|
||||
"PositionTicks": int(m.top.position) * 10000000&, 'Ensure a LongInteger is used
|
||||
"IsPaused": (m.top.state = "paused")
|
||||
}
|
||||
if m.top.content.live
|
||||
params.append({
|
||||
"MediaSourceId": m.top.transcodeParams.MediaSourceId,
|
||||
"LiveStreamId": m.top.transcodeParams.LiveStreamId
|
||||
})
|
||||
m.bufferCheckTimer.duration = 30
|
||||
end if
|
||||
|
||||
' Report playstate via worker task
|
||||
playstateTask = m.global.playstateTask
|
||||
playstateTask.setFields({ status: state, params: params })
|
||||
playstateTask.control = "RUN"
|
||||
end sub
|
||||
|
||||
'
|
||||
' Check the the buffering has not hung
|
||||
sub bufferCheck(msg)
|
||||
|
||||
if m.top.state <> "buffering"
|
||||
' If video is not buffering, stop timer
|
||||
m.bufferCheckTimer.control = "stop"
|
||||
m.bufferCheckTimer.unobserveField("fire")
|
||||
return
|
||||
end if
|
||||
if m.top.bufferingStatus <> invalid
|
||||
|
||||
' Check that the buffering percentage is increasing
|
||||
if m.top.bufferingStatus["percentage"] > m.bufferPercentage
|
||||
m.bufferPercentage = m.top.bufferingStatus["percentage"]
|
||||
else if m.top.content.live = true
|
||||
m.top.callFunc("refresh")
|
||||
else
|
||||
' If buffering has stopped Display dialog
|
||||
dialog = createObject("roSGNode", "Dialog")
|
||||
dialog.title = tr("Error Retrieving Content")
|
||||
dialog.buttons = [tr("OK")]
|
||||
dialog.message = tr("There was an error retrieving the data for this item from the server.")
|
||||
dialog.observeField("buttonSelected", "dialogClosed")
|
||||
m.top.getScene().dialog = dialog
|
||||
|
||||
' Stop playback and exit player
|
||||
m.top.control = "stop"
|
||||
m.top.backPressed = true
|
||||
end if
|
||||
end if
|
||||
|
||||
end sub
|
||||
|
||||
'
|
||||
' Clean up on Dialog Closed
|
||||
sub dialogClosed(msg)
|
||||
sourceNode = msg.getRoSGNode()
|
||||
sourceNode.unobserveField("buttonSelected")
|
||||
sourceNode.close = true
|
||||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
|
||||
if key = "OK" and m.nextEpisodeButton.hasfocus() and not m.top.trickPlayBar.visible
|
||||
m.top.control = "stop"
|
||||
m.top.state = "finished"
|
||||
hideNextEpisodeButton()
|
||||
return true
|
||||
else
|
||||
'Hide Next Episode Button
|
||||
if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus()
|
||||
m.nextEpisodeButton.visible = false
|
||||
m.nextEpisodeButton.setFocus(false)
|
||||
m.top.setFocus(true)
|
||||
end if
|
||||
end if
|
||||
|
||||
if not press then return false
|
||||
|
||||
if key = "down"
|
||||
m.top.selectSubtitlePressed = true
|
||||
return true
|
||||
else if key = "up"
|
||||
m.top.selectPlaybackInfoPressed = true
|
||||
return true
|
||||
else if key = "OK"
|
||||
' OK will play/pause depending on current state
|
||||
' return false to allow selection during seeking
|
||||
if m.top.state = "paused"
|
||||
m.top.control = "resume"
|
||||
return false
|
||||
else if m.top.state = "playing"
|
||||
m.top.control = "pause"
|
||||
return false
|
||||
end if
|
||||
end if
|
||||
|
||||
if key = "back"
|
||||
m.top.control = "stop"
|
||||
end if
|
||||
|
||||
return false
|
||||
end function
|
46
components/video/VideoPlayerView.xml
Normal file
46
components/video/VideoPlayerView.xml
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="VideoPlayerView" extends="Video">
|
||||
<interface>
|
||||
<field id="backPressed" type="boolean" alwaysNotify="true" />
|
||||
<field id="selectSubtitlePressed" type="boolean" alwaysNotify="true" />
|
||||
<field id="selectPlaybackInfoPressed" type="boolean" alwaysNotify="true" />
|
||||
<field id="PlaySessionId" type="string" />
|
||||
<field id="Subtitles" type="array" />
|
||||
<field id="SelectedSubtitle" type="integer" />
|
||||
<field id="captionMode" type="string" />
|
||||
<field id="container" type="string" />
|
||||
<field id="directPlaySupported" type="boolean" />
|
||||
<field id="systemOverlay" type="boolean" value="false" />
|
||||
<field id="showID" type="string" />
|
||||
<field id="lastFocus" type="node" />
|
||||
|
||||
<field id="transcodeParams" type="assocarray" />
|
||||
<field id="transcodeAvailable" type="boolean" value="false" />
|
||||
<field id="retryWithTranscoding" type="boolean" value="false" />
|
||||
<field id="isTranscoded" type="boolean" />
|
||||
<field id="transcodeReasons" type="array" />
|
||||
|
||||
<field id="videoId" type="string" />
|
||||
<field id="mediaSourceId" type="string" />
|
||||
<field id="audioIndex" type="integer" />
|
||||
<field id="runTime" type="integer" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="VideoPlayerView.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
|
||||
|
||||
<children>
|
||||
<timer id="playbackTimer" repeat="true" duration="30" />
|
||||
<timer id="bufferCheckTimer" repeat="true" />
|
||||
|
||||
<JFButton id="nextEpisode" opacity="0" textColor="#f0f0f0" focusedTextColor="#202020" focusFootprintBitmapUri="pkg:/images/option-menu-bg.9.png" focusBitmapUri="pkg:/images/white.9.png" translation="[1500, 900]" />
|
||||
<!--animation for the play next episode button-->
|
||||
<Animation id="showNextEpisodeButton" duration="1.0" repeat="false" easeFunction="inQuad">
|
||||
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0.0, .9]" fieldToInterp="nextEpisode.opacity" />
|
||||
</Animation>
|
||||
<Animation id="hideNextEpisodeButton" duration=".2" repeat="false" easeFunction="inQuad">
|
||||
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[.9, 0]" fieldToInterp="nextEpisode.opacity" />
|
||||
</Animation>
|
||||
</children>
|
||||
</component>
|
|
@ -233,6 +233,8 @@ sub Main (args as dynamic) as void
|
|||
end if
|
||||
else if selectedItem.type = "MusicAlbum"
|
||||
group = CreateAlbumView(selectedItem.json)
|
||||
else if selectedItem.type = "Playlist"
|
||||
group = CreatePlaylistView(selectedItem.json)
|
||||
else if selectedItem.type = "Audio"
|
||||
m.global.queueManager.callFunc("clear")
|
||||
m.global.queueManager.callFunc("push", selectedItem.json)
|
||||
|
@ -273,6 +275,14 @@ sub Main (args as dynamic) as void
|
|||
selectedIndex = msg.getData()
|
||||
screenContent = msg.getRoSGNode()
|
||||
|
||||
m.global.queueManager.callFunc("clear")
|
||||
m.global.queueManager.callFunc("push", screenContent.albumData.items[selectedIndex])
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
else if isNodeEvent(msg, "playItem")
|
||||
' User has selected audio they want us to play
|
||||
selectedIndex = msg.getData()
|
||||
screenContent = msg.getRoSGNode()
|
||||
|
||||
m.global.queueManager.callFunc("clear")
|
||||
m.global.queueManager.callFunc("push", screenContent.albumData.items[selectedIndex])
|
||||
m.global.queueManager.callFunc("playQueue")
|
||||
|
|
|
@ -448,6 +448,23 @@ function CreateAlbumView(album)
|
|||
return group
|
||||
end function
|
||||
|
||||
' Shows details on selected playlist. Description text, image, and list of available items
|
||||
function CreatePlaylistView(album)
|
||||
group = CreateObject("roSGNode", "PlaylistView")
|
||||
m.global.sceneManager.callFunc("pushScene", group)
|
||||
|
||||
group.pageContent = ItemMetaData(album.id)
|
||||
group.albumData = PlaylistItemList(album.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, season)
|
||||
startLoadingSpinner()
|
||||
group = CreateObject("roSGNode", "TVEpisodes")
|
||||
|
|
|
@ -76,6 +76,7 @@ function ItemMetaData(id as string)
|
|||
resp = APIRequest(url)
|
||||
data = getJson(resp)
|
||||
if data = invalid then return invalid
|
||||
|
||||
imgParams = {}
|
||||
if data.type <> "Audio"
|
||||
if data?.UserData?.PlayedPercentage <> invalid
|
||||
|
@ -250,6 +251,30 @@ function GetSongsByArtist(id as string)
|
|||
return data
|
||||
end function
|
||||
|
||||
' Get Items that are under the provided item
|
||||
function PlaylistItemList(id as string)
|
||||
url = Substitute("Playlists/{0}/Items", id)
|
||||
resp = APIRequest(url, {
|
||||
"UserId": get_setting("active_user")
|
||||
})
|
||||
|
||||
results = []
|
||||
data = getJson(resp)
|
||||
|
||||
if data = invalid then return invalid
|
||||
if data.Items = invalid then return invalid
|
||||
if data.Items.Count() = 0 then return invalid
|
||||
|
||||
for each item in data.Items
|
||||
tmp = CreateObject("roSGNode", "PlaylistData")
|
||||
tmp.image = PosterImage(item.id)
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end for
|
||||
data.Items = results
|
||||
return data
|
||||
end function
|
||||
|
||||
' Get Songs that are on an Album
|
||||
function MusicSongList(id as string)
|
||||
url = Substitute("Users/{0}/Items", get_setting("active_user"), id)
|
||||
|
@ -402,3 +427,23 @@ function TVEpisodes(show_id as string, season_id as string)
|
|||
data.Items = results
|
||||
return data
|
||||
end function
|
||||
|
||||
function TVEpisodeShuffleList(show_id as string)
|
||||
url = Substitute("Shows/{0}/Episodes", show_id)
|
||||
resp = APIRequest(url, {
|
||||
"UserId": get_setting("active_user"),
|
||||
"Limit": 200,
|
||||
"sortBy": "Random"
|
||||
})
|
||||
|
||||
data = getJson(resp)
|
||||
results = []
|
||||
for each item in data.Items
|
||||
tmp = CreateObject("roSGNode", "TVEpisodeData")
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end for
|
||||
data.Items = results
|
||||
|
||||
return data
|
||||
end function
|
||||
|
|
|
@ -9,7 +9,8 @@ sub setConstants()
|
|||
poster_bg_pallet: ["#00455c", "#44bae1", "#00a4db", "#1c4c5c", "#007ea8"],
|
||||
|
||||
colors: {
|
||||
button: "#006fab"
|
||||
button: "#006fab",
|
||||
blue: "#00a4dcFF"
|
||||
},
|
||||
|
||||
icons: {
|
||||
|
|
|
@ -282,6 +282,14 @@ function findNodeBySubtype(node, subtype)
|
|||
return foundNodes
|
||||
end function
|
||||
|
||||
' Search string array for search value. Return if it's found
|
||||
function inArray(array, searchValue) as boolean
|
||||
for each item in array
|
||||
if lcase(item) = lcase(searchValue) then return true
|
||||
end for
|
||||
return false
|
||||
end function
|
||||
|
||||
sub startLoadingSpinner()
|
||||
m.spinner = createObject("roSGNode", "Spinner")
|
||||
m.spinner.translation = "[900, 450]"
|
||||
|
@ -289,7 +297,6 @@ sub startLoadingSpinner()
|
|||
m.scene.appendChild(m.spinner)
|
||||
end sub
|
||||
|
||||
|
||||
sub startMediaLoadingSpinner()
|
||||
dialog = createObject("roSGNode", "ProgressDialog")
|
||||
dialog.id = "invisibiledialog"
|
||||
|
|
Loading…
Reference in New Issue
Block a user