Merge pull request #311 from neilsb/livetv-guide

Add Live TV guide
This commit is contained in:
Anthony Lavado 2020-11-29 23:49:08 -05:00 committed by GitHub
commit 6bab4b719e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1116 additions and 31 deletions

View File

@ -1,6 +1,7 @@
sub init()
m.options = m.top.findNode("options")
m.tvGuide = invalid
m.itemGrid = m.top.findNode("itemGrid")
m.backdrop = m.top.findNode("backdrop")
@ -59,6 +60,11 @@ sub loadInitialItems()
'For LiveTV, we want to "Fit" the item images, not zoom
m.top.imageDisplayMode = "scaleToFit"
if get_user_setting("display.livetv.landing") = "guide" then
showTvGuid()
end if
else if m.top.parentItem.collectionType = "CollectionFolder" then
' Non-recursive, to not show subfolder contents
m.loadItemsTask.recursive = false
@ -78,6 +84,7 @@ end sub
sub SetUpOptions()
options = {}
options.filter = []
'Movies
if m.top.parentItem.collectionType = "movies" then
@ -113,13 +120,18 @@ sub SetUpOptions()
options.filter = []
'Live TV
else if m.top.parentItem.collectionType = "livetv" then
options.views = [{"Title": tr("Live TV"), "Name": "livetv" }]
options.views = [
{"Title": tr("Channels"), "Name": "livetv" },
{"Title": tr("TV Guide"), "Name": "tvGuide", "Selected": get_user_setting("display.livetv.landing") = "guide" }
]
options.sort = [
{ "Title": tr("TITLE"), "Name": "SortName" }
]
options.filter = []
else
options.views = [{ "Title": tr("Default"), "Name": "default" }]
options.views = [
{"Title": tr("Default"), "Name": "default" }
]
options.sort = [
{ "Title": tr("TITLE"), "Name": "SortName" }
]
@ -259,6 +271,15 @@ end sub
'
'Check if options updated and any reloading required
sub optionsClosed()
if (m.options.view = "tvGuide") then
showTVGuid()
return
else if m.tvGuide <> invalid then
' Try to hide the TV Guide
m.top.removeChild(m.tvGuide)
end if
reload = false
if m.options.sortField <> m.sortField or m.options.sortAscending <> m.sortAscending then
m.sortField = m.options.sortField
@ -279,6 +300,24 @@ sub optionsClosed()
m.itemGrid.setFocus(true)
end sub
sub showTVGuid()
m.top.signalBeacon("EPGLaunchInitiate") ' Required Roku Performance monitoring
if m.tvGuide = invalid then
m.tvGuide = createObject("roSGNode", "Schedule")
endif
m.tvGuide.observeField("watchChannel", "onChannelSelected")
m.top.appendChild(m.tvGuide)
m.tvGuide.lastFocus.setFocus(true)
end sub
sub onChannelSelected(msg)
node = msg.getRoSGNode()
m.top.lastFocus = lastFocusedChild(node)
if node.watchChannel <> invalid then
' Clone the node when it's reused/update in the TimeGrid it doesn't automatically start playing
m.top.selectedItem = node.watchChannel.clone(false)
end if
end sub
function onKeyEvent(key as string, press as boolean) as boolean
@ -287,9 +326,11 @@ function onKeyEvent(key as string, press as boolean) as boolean
if key = "options"
if m.options.visible = true then
m.options.visible = false
m.top.removeChild(m.options)
optionsClosed()
else
m.options.visible = true
m.top.appendChild(m.options)
m.options.setFocus(true)
end if
return true

View File

@ -35,5 +35,7 @@
<field id="selectedItem" type="node" alwaysNotify="true" />
<field id="imageDisplayMode" type="string" value="scaleToZoom" />
</interface>
<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="ItemGrid2.brs" />
</component>

View File

@ -39,7 +39,7 @@ sub optionsSet()
entry = viewContent.CreateChild("ContentNode")
entry.title = view.Title
m.viewNames.push(view.Name)
if view.selected <> invalid and view.selected = true then
if (view.selected <> invalid and view.selected = true) or viewContent.Name = m.top.view then
selectedViewIndex = index
end if
index = index + 1
@ -136,6 +136,12 @@ function onKeyEvent(key as string, press as boolean) as boolean
return true
else if key = "OK"
if(m.menus[m.selectedItem].isInFocusChain()) then
' Handle View Screen
if(m.selectedItem = 0) then
m.selectedViewIndex = m.menus[0].itemSelected
m.top.view = m.viewNames[m.selectedViewIndex]
end if
' Handle Sort screen
if(m.selectedItem = 1) then
if m.menus[1].itemSelected <> m.selectedSortIndex then

View File

@ -2,7 +2,7 @@ sub setFields()
json = m.top.json
m.top.id = json.id
m.top.Title = json.name
m.top.title = json.name
m.top.live = true
m.top.Type = "TvChannel"
setPoster()
@ -11,7 +11,7 @@ end sub
sub setPoster()
if m.top.image <> invalid
m.top.posterURL = m.top.image.url
else if m.top.json.ImageTags.Primary <> invalid then
else if m.top.json.ImageTags <> invalid and m.top.json.ImageTags.Primary <> invalid then
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
end if

View File

@ -2,7 +2,6 @@
<component name="ChannelData" extends="JFContentItem">
<interface>
<field id="channelID" type="string" />
<field id="Title" type="string" />
</interface>
<script type="text/brightscript" uri="ChannelData.brs" />
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />

View File

@ -22,7 +22,7 @@ sub setData()
' Add Icon URLs for display if there is no Poster
if datum.CollectionType = "livetv" then
m.top.iconUrl = "pkg:/images/baseline_live_tv_white_48dp.png"
m.top.iconUrl = "pkg:/images/media_type_icons/live_tv_white.png"
end if
else if datum.type = "Episode" then
@ -142,7 +142,7 @@ sub setData()
params = { "Tag" : datum.ImageTags.Primary, "maxHeight" : 261, "maxWidth" : 464 }
m.top.thumbnailURL = ImageURL(datum.id, "Primary", params)
m.top.widePosterUrl = m.top.thumbnailURL
m.top.iconUrl = "pkg:/images/baseline_live_tv_white_48dp.png"
m.top.iconUrl = "pkg:/images/media_type_icons/live_tv_white.png"
end if
end sub

View File

@ -0,0 +1,43 @@
sub setFields()
json = m.top.json
startDate = createObject("roDateTime")
endDate = createObject("roDateTime")
startDate.FromISO8601String(json.StartDate)
endDate.FromISO8601String(json.EndDate)
m.top.Title = json.Name
m.top.PlayStart = startDate.AsSeconds()
m.top.PlayDuration = endDate.AsSeconds() - m.top.PlayStart
m.top.Id = json.Id
m.top.Description = json.overview
m.top.EpisodeTitle = json.EpisodeTitle
m.top.isLive = json.isLive
m.top.isRepeat = json.isRepeat
m.top.startDate = json.startDate
m.top.endDate = json.endDate
m.top.channelId = json.channelId
if json.IsSeries <> invalid and json.IsSeries = true then
if json.IndexNumber <> invalid
m.top.episodeNumber = json.IndexNumber
end if
if json.ParentIndexNumber <> invalid
m.top.seasonNumber = json.ParentIndexNumber
end if
end if
setPoster()
end sub
sub setPoster()
if m.top.image <> invalid
m.top.posterURL = m.top.image.url
else
if m.top.json.ImageTags <> invalid and m.top.json.ImageTags.Thumb <> invalid then
imgParams = { "maxHeight": 500, "maxWidth": 500, "Tag" : m.top.json.ImageTags.Thumb }
m.top.posterURL = ImageURL(m.top.json.id, "Thumb", imgParams)
end if
end if
end sub

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="ScheduleProgramData" extends="JFContentItem">
<interface>
<field id="fullyLoaded" type="boolean" value="false" />
<field id="channelIndex" type="integer" />
<field id="programIndex" type="integer" />
<field id="episodeTitle" type="string" />
<field id="isLive" type="boolean" value="false" />
<field id="isRepeat" type="boolean" value="false" />
<field id="startDate" type="string" />
<field id="endDate" type="string" />
<field id="seasonNumber" type="integer" value="-1" />
<field id="episodeNumber" type="integer" value="-1" />
<field id="channelId" type="string" />
<field id="channelLogoUri" type="string" />
<field id="channelName" type="string" />
</interface>
<script type="text/brightscript" uri="ScheduleProgramData.brs" />
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
</component>

View File

@ -0,0 +1,34 @@
sub init()
m.top.functionName = "loadChannels"
end sub
sub loadChannels()
results = []
params = {
UserId: get_setting("active_user")
limit: m.top.limit,
StartIndex: m.top.startIndex
}
url = "LiveTv/Channels"
resp = APIRequest(url, params)
data = getJson(resp)
if data.TotalRecordCount = invalid then
m.top.channels = results
return
end if
for each item in data.Items
channel = createObject("roSGNode", "ChannelData")
channel.json = item
results.push(channel)
end for
m.top.channels = results
end sub

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="LoadChannelsTask" extends="Task">
<interface>
<field id="limit" type="integer" value="500" />
<field id="startIndex" type="integer" value="0" />
<!-- Total records available from server-->
<field id="channels" type="array" />
</interface>
<script type="text/brightscript" uri="LoadChannelsTask.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" /> -->
</component>

View File

@ -0,0 +1,32 @@
sub init()
m.top.functionName = "loadProgramDetails"
end sub
sub loadProgramDetails()
channelIndex = m.top.ChannelIndex
programIndex = m.top.ProgramIndex
params = {
UserId: get_setting("active_user"),
}
url = Substitute("LiveTv/Programs/{0}", m.top.programId)
resp = APIRequest(url, params)
data = getJson(resp)
if data = invalid then
m.top.programDetails = {}
return
end if
program = createObject("roSGNode", "ScheduleProgramData")
program.json = data
program.channelIndex = ChannelIndex
program.programIndex = ProgramIndex
program.fullyLoaded = true
m.top.programDetails = program
end sub

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="LoadProgramDetailsTask" extends="Task">
<interface>
<field id="programId" type="string" />
<field id="ChannelIndex" type="integer" />
<field id="ProgramIndex" type="integer" />
<field id="programDetails" type="node" />
</interface>
<script type="text/brightscript" uri="LoadProgramDetailsTask.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
</component>

View File

@ -0,0 +1,41 @@
sub init()
m.top.functionName = "loadSchedule"
end sub
sub loadSchedule()
results = []
params = {
UserId: get_setting("active_user"),
SortBy: "startDate",
EnableImages: false
EnableTotalRecordCount: false,
EnableUserData: false
channelIds: m.top.channelIds
MaxStartDate: m.top.endTime,
MinEndDate: m.top.startTime
}
url = "LiveTv/Programs"
resp = APIRequest(url)
data = postJson(resp, FormatJson(params))
if data = invalid then
m.top.schedule = results
return
end if
results = []
for each item in data.Items
program = createObject("roSGNode", "ScheduleProgramData")
program.json = item
results.push(program)
end for
m.top.schedule = results
end sub

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="LoadScheduleTask" extends="Task">
<interface>
<field id="startTime" type="string" />
<field id="endTime" type="string" />
<field id="channelIds" type="string" />
<field id="schedule" type="array" />
</interface>
<script type="text/brightscript" uri="LoadSheduleTask.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
</component>

View File

@ -0,0 +1,230 @@
sub init()
' Max "Overview" lines to show in Preview and Detail
m.maxPreviewLines = 5
m.maxDetailLines = 14
m.detailsView = m.top.findNode("detailsView")
m.noInfoView = m.top.findNode("noInformation")
m.programName = m.top.findNode("programName")
m.episodeTitle = m.top.findNode("episodeTitle")
m.episodeNumber = m.top.findNode("episodeNumber")
m.overview = m.top.findNode("overview")
m.episodeDetailsGroup = m.top.findNode("episodeDetailsGroup")
m.isLiveGroup = m.top.findNode("isLive")
m.isRepeatGroup = m.top.findNode("isRepeat")
m.broadcastDetails = m.top.findNode("broadcastDetails")
m.duration = m.top.findNode("duration")
m.channelName = m.top.findNode("channelName")
m.image = m.top.findNode("image")
m.focusAnimationOpacity = m.top.findNode("focusAnimationOpacity")
m.focusAnimation = m.top.findNode("focusAnimation")
m.viewChannelButton = m.top.findNode("viewChannelButton")
m.focusAnimation.observeField("state", "onAnimationComplete")
setupLabels()
end sub
' Set up Live and Repeat label sizes
sub setupLabels()
boundingRect = m.top.findNode("isLiveText").boundingRect()
isLiveBackground = m.top.findNode("isLiveBackground")
isLiveBackground.width = boundingRect.width + 16
isLiveBackground.height = boundingRect.height + 8
m.episodeDetailsGroup.removeChildIndex(0)
boundingRect = m.top.findNode("isRepeatText").boundingRect()
isRepeatBackground = m.top.findNode("isRepeatBackground")
isRepeatBackground.width = boundingRect.width + 16
isRepeatBackground.height = boundingRect.height + 8
m.episodeDetailsGroup.removeChildIndex(0)
boundingRect = m.viewChannelButton.boundingRect()
buttonBackground = m.top.findNode("viewChannelButtonBackground")
buttonBackground.width = boundingRect.width + 20
buttonBackground.height = boundingRect.height + 20
end sub
sub channelUpdated()
if m.top.channel = invalid
m.top.findNode("noInfoChannelName").text = ""
m.channelName.text = ""
else
m.top.findNode("noInfoChannelName").text = m.top.channel.Title
m.channelName.text= m.top.channel.Title
if m.top.programDetails = invalid then
m.image.uri = m.top.channel.posterURL
end if
end if
end sub
sub programUpdated()
m.top.watchSelectedChannel = false
m.overview.maxLines = m.maxDetailLines
prog = m.top.programDetails
' If no program selected, hide details view
if prog = invalid then
channelUpdated()
m.detailsView.visible = "false"
m.noInfoView.visible = "true"
return
end if
m.programName.text = prog.Title
m.overview.text = prog.description
m.episodeDetailsGroup.removeChildrenIndex(m.episodeDetailsGroup.getChildCount(), 0)
if prog.isLive then
m.episodeDetailsGroup.appendChild(m.isLiveGroup)
else if prog.isRepeat then
m.episodeDetailsGroup.appendChild(m.isRepeatGroup)
end if
' Episode Number
if prog.seasonNumber > 0 and prog.episodeNumber > 0 then
m.episodeNumber.text = "S" + StrI(prog.seasonNumber).trim() + ":E" + StrI(prog.episodeNumber).trim()
if prog.episodeTitle <> "" then m.episodeNumber.text = m.episodeNumber.text + " -" ' Add a Dash if showing Episode Number and Title
m.episodeDetailsGroup.appendChild(m.episodeNumber)
end if
if prog.episodeTitle <> invalid and prog.episodeTitle <> "" then
m.episodeTitle.text = prog.episodeTitle
m.episodeTitle.visible = true
m.episodeDetailsGroup.appendChild(m.episodeTitle)
end if
m.duration.text = getDurationStringFromSeconds(prog.PlayDuration)
' Calculate Broadcast Details
now = createObject("roDateTime")
startDate = createObject("roDateTime")
endDate = createObject("roDateTime")
startDate.FromISO8601String(prog.StartDate)
endDate.FromISO8601String(prog.EndDate)
day = getRelativeDayName(startDate)
if startDate.AsSeconds() < now.AsSeconds() and endDate.AsSeconds() > now.AsSeconds() then
if day = "today" then
m.broadcastDetails.text = tr("Started at") + " " + formatTime(startDate)
else
m.broadcastDetails.text = tr("Started") + " " + tr(day) + ", " + formatTime(startDate)
end if
else if startDate.AsSeconds() > now.AsSeconds()
if day = "today" then
m.broadcastDetails.text = tr("Starts at") + " " + formatTime(startDate)
else
m.broadcastDetails.text = tr("Starts") + " " + tr(day) + ", " + formatTime(startDate)
end if
else
if day = "today" then
m.broadcastDetails.text = tr("Ended at") + " " + formatTime(endDate)
else
m.broadcastDetails.text = tr("Ended") + " " + tr(day) + ", " + formatTime(endDate)
end if
end if
m.image.uri = prog.PosterURL
m.detailsView.visible = "true"
m.noInfoView.visible = "false"
m.top.height = m.detailsView.boundingRect().height
m.overview.maxLines = m.maxPreviewLines
end sub
'
' Get relative date name for a date (yesterday, today, tomorrow, or otherwise weekday name )
function getRelativeDayName(date) as string
now = createObject("roDateTime")
' Check for Today
if now.AsDateString("short-date-dashes") = date.AsDateString("short-date-dashes") then
return "today"
end if
' Check for Yesterday
todayMidnight = now.AsSeconds() - (now.AsSeconds() MOD 86400)
dateMidnight = date.AsSeconds() - (date.AsSeconds() MOD 86400)
if todayMidnight - dateMidnight = 86400 then
return "yesterday"
end if
if dateMidnight - todayMidnight = 86400 then
return "tomorrow"
end if
return date.GetWeekday()
end function
'
' Get program duration string (e.g. 1h 20m)
function getDurationStringFromSeconds(seconds) as string
hours = 0
minutes = seconds / 60.0
if minutes > 60 then
hours = (minutes - (minutes MOD 60)) / 60
minutes = minutes MOD 60
end if
if hours > 0 then
return "%1h %2m".Replace("%1", StrI(hours).trim()).Replace("%2", StrI(minutes).trim())
else
return "%1m".Replace("%1", StrI(minutes).trim())
end if
end function
'
' Show view channel button when item has Focus
sub focusChanged()
if m.top.hasFocus = true then
m.overview.maxLines = m.maxDetailLines
m.focusAnimationOpacity.keyValue = [0, 1]
else
m.top.watchSelectedChannel = false
m.focusAnimationOpacity.keyValue = [1, 0]
end if
m.focusAnimation.control = "start"
end sub
sub onAnimationComplete()
if m.focusAnimation.state = "stopped" and m.top.hasFocus = false then
m.overview.maxLines = m.maxPreviewLines
end if
end sub
function onKeyEvent(key as string, press as boolean) as boolean
if not press then return false
if key = "OK" then
m.top.watchSelectedChannel = true
return true
end if
if key = "left" or key = "right" or key = "up" or key = "down" then
return true
end if
return false
end function

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="ProgramDetails" extends="JFGroup">
<children>
<!-- Selected Item Details -->
<maskGroup id="backgroundMask" maskUri="pkg:/images/backgroundmask.png" translation="[1208, 127]" maskSize="[712,400]">
<Poster id="image" height="400" width="712" loadDisplayMode="scaleToFit" />
</maskGroup>
<Group id="detailsView" visible="false">
<Group translation = "[ 96, 160 ]">
<LayoutGroup itemSpacings="[4,20, 20, 20, 40]">
<Label id="programName" font="font:LargeBoldSystemFont" />
<LayoutGroup id="episodeDetailsGroup" layoutDirection="horiz" itemSpacings="[10]">
<Group id="isLive">
<Poster id="isLiveBackground" uri="pkg:/images/white.9.png" blendColor="#FF0000" />
<Label id="isLiveText" text="Live" font="font:SmallestBoldSystemFont" translation="[8,4]" />
</Group>
<Group id="isRepeat">
<Poster id="isRepeatBackground" uri="pkg:/images/white.9.png" blendColor="#009688" />
<Label id="isRepeatText" text="Repeat" font="font:SmallestBoldSystemFont" translation="[8,4]" />
</Group>
<Label id="episodeNumber" font="font:SmallSystemFont" />
<Label id="episodeTitle" font="font:SmallSystemFont" />
</LayoutGroup>
<LayoutGroup layoutDirection="horiz" itemSpacings="[30]">
<Label id="duration" />
<Label id="broadcastDetails" />
<Label id="channelName" />
</LayoutGroup>
<label id="overview" wrap="true" width="1250" font="font:SmallestSystemFont" />
<!-- View Channel button -->
<Group id="viewChannelButton" opacity="0">
<Poster id="viewChannelButtonBackground" uri="pkg:/images/white.9.png" blendColor="#006fab" />
<Label text="View Channel" translation="[20,20]" />
</Group>
</LayoutGroup>
</Group>
</Group>
<!-- When no schedule information to display -->
<LayoutGroup id="noInformation" translation="[96, 300]">
<Label id="noInfoChannelName" font="font:LargeBoldSystemFont" />
<Label font="font:SmallSystemFont" text="No schedule information" />
</LayoutGroup>
<Animation id="focusAnimation" duration="0.66" repeat="false" easeFunction="linear" >
<FloatFieldInterpolator id="focusAnimationOpacity" key="[0.0, 1]" fieldToInterp="viewChannelButton.opacity" />
</Animation>
</children>
<interface>
<field id="WatchSelectedChannel" type="boolean" value="false" />
<field id="channel" type="node" onchange="channelUpdated" />
<field id="programDetails" type="node" onchange="programUpdated" />
<field id="height" type="integer" />
<field id="hasFocus" type="boolean" onChange="focusChanged" />
</interface>
<script type="text/brightscript" uri="ProgramDetails.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component>

View File

@ -0,0 +1,189 @@
sub init()
m.scheduleGrid = m.top.findNode("scheduleGrid")
m.detailsPane = m.top.findNode("detailsPane")
m.detailsPane.observeField("watchSelectedChannel", "onWatchChannelSelected")
m.gridStartDate = CreateObject("roDateTime")
m.scheduleGrid.contentStartTime = m.gridStartDate.AsSeconds() - 1800
m.gridEndDate = createObject("roDateTime")
m.gridEndDate.FromSeconds(m.gridStartDate.AsSeconds() + (24 * 60 * 60))
m.scheduleGrid.observeField("programFocused", "onProgramFocused")
m.scheduleGrid.observeField("programSelected", "onProgramSelected")
m.scheduleGrid.observeField("leftEdgeTargetTime", "onGridScrolled")
m.scheduleGrid.channelInfoWidth = 350
m.gridMoveAnimation = m.top.findNode("gridMoveAnimation")
m.gridMoveAnimationPosition = m.top.findNode("gridMoveAnimationPosition")
m.LoadChannelsTask = createObject("roSGNode", "LoadChannelsTask")
m.LoadChannelsTask.observeField("channels", "onChannelsLoaded")
m.LoadChannelsTask.control = "RUN"
m.top.lastFocus = m.scheduleGrid
m.channelIndex = {}
end sub
' Initial list of channels loaded
sub onChannelsLoaded()
gridData = createObject("roSGNode", "ContentNode")
counter = 0
channelIdList = ""
for each item in m.LoadChannelsTask.channels
gridData.appendChild(item)
m.channelIndex[item.Id] = counter
counter = counter + 1
channelIdList = channelIdList + item.Id + ","
end for
m.scheduleGrid.content = gridData
m.LoadScheduleTask = createObject("roSGNode", "LoadScheduleTask")
m.LoadScheduleTask.observeField("schedule", "onScheduleLoaded")
m.LoadScheduleTask.startTime = m.gridStartDate.ToISOString()
m.LoadScheduleTask.endTime = m.gridEndDate.ToISOString()
m.LoadScheduleTask.channelIds = channelIdList
m.LoadScheduleTask.control = "RUN"
m.LoadProgramDetailsTask = createObject("roSGNode", "LoadProgramDetailsTask")
m.LoadProgramDetailsTask.observeField("programDetails", "onProgramDetailsLoaded")
m.scheduleGrid.setFocus(true)
m.top.signalBeacon("EPGLaunchComplete") ' Required Roku Performance monitoring
end sub
' When LoadScheduleTask completes (initial or more data) and we have a schedule to display
sub onScheduleLoaded()
for each item in m.LoadScheduleTask.schedule
channel = m.scheduleGrid.content.GetChild(m.channelIndex[item.ChannelId])
if channel.PosterUrl <> "" then
item.channelLogoUri = channel.PosterUrl
end if
if channel.Title <> "" then
item.channelName = channel.Title
end if
channel.appendChild(item)
end for
m.scheduleGrid.showLoadingDataFeedback = false
end sub
sub onProgramFocused()
m.top.watchChannel = invalid
channel = m.scheduleGrid.content.GetChild(m.scheduleGrid.programFocusedDetails.focusChannelIndex)
m.detailsPane.channel = channel
' Exit if Channels not yet loaded
if channel.getChildCount() = 0 then
m.detailsPane.programDetails = invalid
return
end if
prog = channel.GetChild(m.scheduleGrid.programFocusedDetails.focusIndex)
if prog <> invalid and prog.fullyLoaded = false then
m.LoadProgramDetailsTask.programId = prog.Id
m.LoadProgramDetailsTask.channelIndex = m.scheduleGrid.programFocusedDetails.focusChannelIndex
m.LoadProgramDetailsTask.programIndex = m.scheduleGrid.programFocusedDetails.focusIndex
m.LoadProgramDetailsTask.control = "RUN"
end if
m.detailsPane.programDetails = prog
end sub
' Update the Program Details with full information
sub onProgramDetailsLoaded()
if m.LoadProgramDetailsTask.programDetails = invalid then return
channel = m.scheduleGrid.content.GetChild(m.LoadProgramDetailsTask.programDetails.channelIndex)
' If TV Show does not have its own image, use the channel logo
if m.LoadProgramDetailsTask.programDetails.PosterUrl = invalid or m.LoadProgramDetailsTask.programDetails.PosterUrl = "" then
m.LoadProgramDetailsTask.programDetails.PosterUrl = channel.PosterUrl
end if
channel.ReplaceChild(m.LoadProgramDetailsTask.programDetails, m.LoadProgramDetailsTask.programDetails.programIndex)
end sub
sub onProgramSelected()
' If there is no program data - view the channel
if m.detailsPane.programDetails = invalid then
m.top.watchChannel = m.scheduleGrid.content.GetChild(m.scheduleGrid.programFocusedDetails.focusChannelIndex)
return
end if
' Move Grid Down
focusProgramDetails(true)
end sub
' Move the TV Guide Grid down or up depending whether details are selected
sub focusProgramDetails(setFocused)
h = m.detailsPane.height
if h < 400 then h = 400
h = h + 160 + 80
if setFocused = true then
m.gridMoveAnimationPosition.keyValue = [ [0,600], [0, h] ]
m.detailsPane.setFocus(true)
m.detailsPane.hasFocus = true
m.top.lastFocus = m.detailsPane
else
m.detailsPane.hasFocus = false
m.gridMoveAnimationPosition.keyValue = [ [0, h], [0,600] ]
m.scheduleGrid.setFocus(true)
m.top.lastFocus = m.scheduleGrid
end if
m.gridMoveAnimation.control = "start"
end sub
' Handle user selecting "Watch Channel" from Program Details
sub onWatchChannelSelected()
if m.detailsPane.watchSelectedChannel = false then return
' Set focus back to grid before showing channel, to ensure grid has focus when we return
focusProgramDetails(false)
m.top.watchChannel = m.scheduleGrid.content.GetChild(m.LoadProgramDetailsTask.programDetails.channelIndex)
end sub
' As user scrolls grid, check if more data requries to be loaded
sub onGridScrolled()
' If we're within 12 hours of end of grid, load next 24hrs of data
if m.scheduleGrid.leftEdgeTargetTime + (12 * 60 * 60) > m.gridEndDate.AsSeconds() then
' Ensure the task is not already (still) running,
if m.LoadScheduleTask.state <> "run" then
m.LoadScheduleTask.startTime = m.gridEndDate.ToISOString()
m.gridEndDate.FromSeconds(m.gridEndDate.AsSeconds() + (24 * 60 * 60))
m.LoadScheduleTask.endTime = m.gridEndDate.ToISOString()
m.LoadScheduleTask.control = "RUN"
end if
end if
end sub
function onKeyEvent(key as string, press as boolean) as boolean
if not press then return false
if key = "back" and m.detailsPane.isInFocusChain() then
focusProgramDetails(false)
return true
end if
return false
end function

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="Schedule" extends="JFGroup">
<children>
<rectangle translation="[0,125]" width="1920" height="955" color="#262626" />
<!-- Selected Item Details -->
<ProgramDetails id="detailsPane" focusable="true" />
<TimeGrid id="scheduleGrid" translation="[0,600]"
automaticLoadingDataFeedback="false" showLoadingDataFeedback="true"
focusBitmapUri="pkg:/images/white.9.png" focusBitmapBlendColor="#006fab"
programTitleFocusedColor="#ffffff"
showPastTimeScreen="true" pastTimeScreenBlendColor="#555555"
/>
<Animation id="gridMoveAnimation" duration="1" repeat="false" easeFunction="outQuad" >
<Vector2DFieldInterpolator id="gridMoveAnimationPosition" key="[0.0, 0.5]" fieldToInterp="scheduleGrid.translation" />
</Animation>
</children>
<interface>
<field id="watchChannel" type="node" alwaysNotify="false" />
</interface>
<script type="text/brightscript" uri="schedule.brs" />
</component>

BIN
images/backgroundmask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 KiB

View File

@ -200,5 +200,105 @@
<source>Unable to load Channel Data from the server</source>
<translation>Unable to load Channel Data from the server</translation>
</message>
<message>
<source>today</source>
<translation>today</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>yesterday</source>
<translation>yesterday</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>tomorrow</source>
<translation>tomorrow</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Sunday</source>
<translation>Sunday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Monday</source>
<translation>Monday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Tuesday</source>
<translation>Tuesday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Wednesday</source>
<translation>Wednesday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Thursday</source>
<translation>Thursday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Friday</source>
<translation>Friday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Saturday</source>
<translation>Saturday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started at</source>
<translation>Started at</translation>
<extracomment>(Past Tense) For defining time when a program started today (e.g. Started at 08:00) </extracomment>
</message>
<message>
<source>Started</source>
<translation>Started</translation>
<extracomment>(Past Tense) For defining a day and time when a program started (e.g. Started Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Starts at</source>
<translation>Starts at</translation>
<extracomment>(Future Tense) For defining time when a program will start today (e.g. Starts at 08:00) </extracomment>
</message>
<message>
<source>Starts</source>
<translation>Starts</translation>
<extracomment>(Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Ended at</source>
<translation>Ended at</translation>
<extracomment>(Past Tense) For defining time when a program will ended (e.g. Ended at 08:00) </extracomment>
</message>
<message>
<source>Ends at</source>
<translation>Ends at</translation>
<extracomment>(Past Tense) For defining a day and time when a program ended (e.g. Ended Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Live</source>
<translation>Live</translation>
<extracomment>If TV Show is being broadcast live (not pre-recorded)</extracomment>
</message>
<message>
<source>Repeat</source>
<translation>Repeat</translation>
<extracomment>If TV Shows has previously been broadcasted</extracomment>
</message>
<message>
<source>Channels</source>
<translation>Channels</translation>
<extracomment>Menu option for showing Live TV Channel List</extracomment>
</message>
<message>
<source>TV Guide</source>
<translation>TV Guide</translation>
<extracomment>Menu option for showing Live TV Guide / Schedule</extracomment>
</message>
</context>
</TS>

View File

@ -314,6 +314,106 @@
<source>TAB_FILTER</source>
<translation>Filter</translation>
</message>
<message>
<source>today</source>
<translation>today</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>yesterday</source>
<translation>yesterday</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>tomorrow</source>
<translation>tomorrow</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Sunday</source>
<translation>Sunday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Monday</source>
<translation>Monday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Tuesday</source>
<translation>Tuesday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Wednesday</source>
<translation>Wednesday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Thursday</source>
<translation>Thursday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Friday</source>
<translation>Friday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Saturday</source>
<translation>Saturday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started at</source>
<translation>Started at</translation>
<extracomment>(Past Tense) For defining time when a program started today (e.g. Started at 08:00) </extracomment>
</message>
<message>
<source>Started</source>
<translation>Started</translation>
<extracomment>(Past Tense) For defining a day and time when a program started (e.g. Started Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Starts at</source>
<translation>Starts at</translation>
<extracomment>(Future Tense) For defining time when a program will start today (e.g. Starts at 08:00) </extracomment>
</message>
<message>
<source>Starts</source>
<translation>Starts</translation>
<extracomment>(Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Ended at</source>
<translation>Ended at</translation>
<extracomment>(Past Tense) For defining time when a program will ended (e.g. Ended at 08:00) </extracomment>
</message>
<message>
<source>Ends at</source>
<translation>Ends at</translation>
<extracomment>(Past Tense) For defining a day and time when a program ended (e.g. Ended Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Live</source>
<translation>Live</translation>
<extracomment>If TV Show is being broadcast live (not pre-recorded)</extracomment>
</message>
<message>
<source>Repeat</source>
<translation>Repeat</translation>
<extracomment>If TV Shows has previously been broadcasted</extracomment>
</message>
<message>
<source>Channels</source>
<translation>Channels</translation>
<extracomment>Menu option for showing Live TV Channel List</extracomment>
</message>
<message>
<source>TV Guide</source>
<translation>TV Guide</translation>
<extracomment>Menu option for showing Live TV Guide / Schedule</extracomment>
</message>
</context>
</TS>

View File

@ -225,7 +225,7 @@
<translation>Error During Playback</translation>
<extracomment>Dialog title when error occurs during playback</extracomment>
</message>
<message>
<source>There was an error retrieving the data for this item from the server.</source>
<translation>There was an error retrieving the data for this item from the server.</translation>
@ -237,7 +237,7 @@
<translation>An error was encountered while playing this item.</translation>
<extracomment>Dialog detail when error occurs during playback</extracomment>
</message>
<message>
<source>Loading Channel Data</source>
<translation>Loading Channel Data</translation>
@ -314,6 +314,106 @@
<source>TAB_FILTER</source>
<translation>Filter</translation>
</message>
<message>
<source>today</source>
<translation>today</translation>
<extracomment>Current day</extracomment>
</message>
<message>
<source>yesterday</source>
<translation>yesterday</translation>
<extracomment>Previous day</extracomment>
</message>
<message>
<source>tomorrow</source>
<translation>tomorrow</translation>
<extracomment>Next day</extracomment>
</message>
<message>
<source>Sunday</source>
<translation>Sunday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Monday</source>
<translation>Monday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Tuesday</source>
<translation>Tuesday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Wednesday</source>
<translation>Wednesday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Thursday</source>
<translation>Thursday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Friday</source>
<translation>Friday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Saturday</source>
<translation>Saturday</translation>
<extracomment>Day of Week</extracomment>
</message>
<message>
<source>Started at</source>
<translation>Started at</translation>
<extracomment>(Past Tense) For defining time when a program started today (e.g. Started at 08:00) </extracomment>
</message>
<message>
<source>Started</source>
<translation>Started</translation>
<extracomment>(Past Tense) For defining a day and time when a program started (e.g. Started Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Starts at</source>
<translation>Starts at</translation>
<extracomment>(Future Tense) For defining time when a program will start today (e.g. Starts at 08:00) </extracomment>
</message>
<message>
<source>Starts</source>
<translation>Starts</translation>
<extracomment>(Future Tense) For defining a day and time when a program will start (e.g. Starts Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Ended at</source>
<translation>Ended at</translation>
<extracomment>(Past Tense) For defining time when a program will ended (e.g. Ended at 08:00) </extracomment>
</message>
<message>
<source>Ends at</source>
<translation>Ends at</translation>
<extracomment>(Past Tense) For defining a day and time when a program ended (e.g. Ended Wednesday, 08:00) </extracomment>
</message>
<message>
<source>Live</source>
<translation>Live</translation>
<extracomment>If TV Show is being broadcast live (not pre-recorded)</extracomment>
</message>
<message>
<source>Repeat</source>
<translation>Repeat</translation>
<extracomment>If TV Shows has previously been broadcasted</extracomment>
</message>
<message>
<source>Channels</source>
<translation>Channels</translation>
<extracomment>Menu option for showing Live TV Channel List</extracomment>
</message>
<message>
<source>TV Guide</source>
<translation>TV Guide</translation>
<extracomment>Menu option for showing Live TV Guide / Schedule</extracomment>
</message>
</context>
</TS>

View File

@ -82,7 +82,7 @@ sub Main()
' If you select a library from ANYWHERE, follow this flow
selectedItem = msg.getData()
if (selectedItem.type = "CollectionFolder" OR selectedItem.type = "UserView" OR selectedItem.type = "Folder") AND ( selectedItem.collectionType = "movies" or selectedItem.collectionType = "CollectionFolder")
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
m.overhang.title = selectedItem.title
@ -90,7 +90,7 @@ sub Main()
group.overhangTitle = selectedItem.title
m.scene.appendChild(group)
else if (selectedItem.type = "CollectionFolder" OR selectedItem.type = "UserView") AND selectedItem.collectionType = "tvshows"
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
@ -99,7 +99,7 @@ sub Main()
group.overhangTitle = selectedItem.title
m.scene.appendChild(group)
else if (selectedItem.type = "CollectionFolder" OR selectedItem.type = "UserView") AND selectedItem.collectionType = "boxsets" OR selectedItem.type = "Boxset"
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
@ -108,7 +108,7 @@ sub Main()
group.overhangTitle = selectedItem.title
m.scene.appendChild(group)
else if ((selectedItem.type = "CollectionFolder" OR selectedItem.type = "UserView") AND selectedItem.collectionType = "livetv") OR selectedItem.type = "Channel"
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
@ -116,9 +116,9 @@ sub Main()
group = CreateChannelList(selectedItem)
group.overhangTitle = selectedItem.title
m.scene.appendChild(group)
else if selectedItem.type = "Boxset" then
else if selectedItem.type = "Boxset" or selectedItem.collectionType = "folders" then
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
@ -141,7 +141,7 @@ sub Main()
video_id = selectedItem.id
video = CreateVideoPlayerGroup(video_id)
if video <> invalid then
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
group = video
@ -152,7 +152,7 @@ sub Main()
m.overhang.visible = false
end if
else if selectedItem.type = "Series" then
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
@ -164,7 +164,7 @@ sub Main()
m.scene.appendChild(group)
else if selectedItem.type = "Movie" then
' open movie detail page
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
@ -188,7 +188,7 @@ sub Main()
dialog.close = true
if video <> invalid then
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
group = video
@ -217,7 +217,7 @@ sub Main()
' If you select a movie from ANYWHERE, follow this flow
node = getMsgPicker(msg, "picker")
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
@ -231,7 +231,7 @@ sub Main()
' If you select a TV Series from ANYWHERE, follow this flow
node = getMsgPicker(msg, "picker")
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
@ -248,7 +248,7 @@ sub Main()
series = msg.getRoSGNode()
node = series.seasonData.items[ptr[1]]
group.lastFocus = group.focusedChild.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild.focusedChild
group.setFocus(false)
group.visible = false
@ -263,7 +263,7 @@ sub Main()
video_id = node.id
video = CreateVideoPlayerGroup(video_id)
if video <> invalid then
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
group = video
@ -298,7 +298,7 @@ sub Main()
else if isNodeEvent(msg, "itemSelected")
' Search item selected
node = getMsgPicker(msg)
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
@ -327,7 +327,7 @@ sub Main()
video_id = group.id
video = CreateVideoPlayerGroup(video_id, audio_stream_idx)
if video <> invalid then
group.lastFocus = group.focusedChild.focusedChild.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild.focusedChild.focusedChild
group.setFocus(false)
group.visible = false
group = video
@ -364,7 +364,7 @@ sub Main()
else
group.setFocus(true)
end if
group.lastFocus = group.focusedChild
if group.lastFocus = invalid then group.lastFocus = group.focusedChild
group.setFocus(false)
group.visible = false
m.overhang.showOptions = false
@ -437,7 +437,7 @@ sub Main()
print msg.GetInfo()
end if
else
print type(msg)
print "Unhandled " type(msg)
print msg
end if
end while
@ -486,6 +486,7 @@ function LoginFlow(startOver = false as boolean)
get_token(userSelected, "")
if get_setting("active_user") <> invalid then
m.user = AboutMe()
LoadUserPreferences()
SendPerformanceBeacon("AppDialogComplete") ' Roku Performance monitoring - Dialog Closed
return true
end if
@ -508,6 +509,7 @@ function LoginFlow(startOver = false as boolean)
goto start_login
end if
LoadUserPreferences()
wipe_groups()
'Send Device Profile information to server

View File

@ -261,7 +261,6 @@ end function
function getContainerType(meta as object) as string
' Determine the file type of the video file source
print type(meta)
if meta.json.mediaSources = invalid then return ""
container = meta.json.mediaSources[0].container

View File

@ -67,3 +67,19 @@ function GetPublicUsers()
resp = APIRequest(url)
return getJson(resp)
end function
' Load and parse Display Settings from server
sub LoadUserPreferences()
id = get_setting("active_user")
' Currently using client "emby", which is what website uses so we get same Display prefs as web.
' May want to change to specific Roku display settings
url = Substitute("DisplayPreferences/usersettings?userId={0}&client=emby", id)
resp = APIRequest(url)
jsonResponse = getJson(resp)
if jsonResponse <> invalid and jsonResponse.CustomPrefs <> invalid and jsonResponse.CustomPrefs["landing-livetv"] <> invalid then
set_user_setting("display.livetv.landing", jsonResponse.CustomPrefs["landing-livetv"])
else
unset_user_setting("display.livetv.landing")
end if
end sub

View File

@ -94,7 +94,7 @@ function lastFocusedChild(obj as object) as object
child = obj
for i = 0 to obj.getChildCount()
if obj.focusedChild <> invalid then
child = child.focusedChild
child = obj.focusedChild
end if
end for
return child
@ -102,7 +102,7 @@ end function
function show_dialog(message as string, options = [], defaultSelection = 0) as integer
group = m.scene.focusedChild
lastFocus = lastFocusedChild(m.scene)
if group.lastFocus = invalid then lastFocus = lastFocusedChild(m.scene)
'We want to handle backPressed instead of the main loop
m.scene.unobserveField("backPressed")