Merge remote-tracking branch 'origin/master' into selectAudio

This commit is contained in:
1hitsong 2024-01-08 20:35:15 -05:00
commit 301cd5c58f
381 changed files with 7621 additions and 1644 deletions

View File

@ -10,7 +10,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9
with:
days-before-issue-stale: -1
days-before-issue-close: -1

View File

@ -6,8 +6,6 @@ concurrency:
on:
push:
branches:
- unstable
pull_request_target:
jobs:

View File

@ -2,11 +2,10 @@ name: build-dev
on:
pull_request:
branches-ignore:
- master
push:
branches:
- unstable
- master
- "*.*.z"
jobs:
dev:
@ -23,7 +22,7 @@ jobs:
run: npm run ropm
- name: Build app
run: npm run build
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
- uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4
with:
name: Jellyfin-Roku-dev-${{ github.sha }}
path: ${{ github.workspace }}/build/staging

View File

@ -3,7 +3,7 @@ name: build-docs
on:
push:
branches:
- unstable
- master
jobs:
docs:

View File

@ -1,64 +1,15 @@
# Builds the production version of the app
name: build-prod
on:
pull_request:
branches:
- master
push:
branches:
- master
- "*.*.z"
jobs:
version-check:
runs-on: ubuntu-latest
steps:
- name: Checkout master (the latest release)
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
ref: master
- name: Install jq to parse json
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: jq
- name: Save old package.json version
run: echo "oldPackVersion=$(jq -r ".version" package.json)" >> $GITHUB_ENV
- name: Find and save old major_version from manifest
run: awk 'BEGIN { FS="=" } /^major_version/ { print "oldMajor="$2; }' manifest >> $GITHUB_ENV
- name: Find and save old minor_version from manifest
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "oldMinor="$2; }' manifest >> $GITHUB_ENV
- name: Find and save old build_version from manifest
run: awk 'BEGIN { FS="=" } /^build_version/ { print "oldBuild="$2; }' manifest >> $GITHUB_ENV
- name: Save old manifest version
run: echo "oldManVersion=${{ env.oldMajor }}.${{ env.oldMinor }}.${{ env.oldBuild }}" >> $GITHUB_ENV
- name: Save old Makefile version
run: awk 'BEGIN { FS=" = " } /^VERSION/ { print "oldMakeVersion="$2; }' Makefile >> $GITHUB_ENV
- name: Checkout PR branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- name: Save new package.json version
run: echo "newPackVersion=$(jq -r ".version" package.json)" >> $GITHUB_ENV
- name: package.json version must be updated
if: env.oldPackVersion == env.newPackVersion
run: exit 1
- name: Find and save new major_version from manifest
run: awk 'BEGIN { FS="=" } /^major_version/ { print "newMajor="$2; }' manifest >> $GITHUB_ENV
- name: Find and save new minor_version from manifest
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "newMinor="$2; }' manifest >> $GITHUB_ENV
- name: Find and save new build_version from manifest
run: awk 'BEGIN { FS="=" } /^build_version/ { print "newBuild="$2; }' manifest >> $GITHUB_ENV
- name: Save new manifest version
run: echo "newManVersion=${{ env.newMajor }}.${{ env.newMinor }}.${{ env.newBuild }}" >> $GITHUB_ENV
- name: Manifest version must be updated
if: env.oldManVersion == env.newManVersion
run: exit 1
- name: Save new Makefile version
run: awk 'BEGIN { FS=" = " } /^VERSION/ { print "newMakeVersion="$2; }' Makefile >> $GITHUB_ENV
- name: Makefile version must be updated
if: env.oldMakeVersion == env.newMakeVersion
run: exit 1
- name: All new versions must match
if: (env.newManVersion != env.newPackVersion) || (env.newManVersion != env.newMakeVersion)
run: exit 1
prod:
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'release-prep') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
@ -72,7 +23,7 @@ jobs:
run: npm run ropm
- name: Build app for production
run: npm run build-prod
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
- uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4
with:
name: Jellyfin-Roku-v${{ env.newManVersion }}-${{ github.sha }}
path: ${{ github.workspace }}/build/staging

View File

@ -3,7 +3,7 @@ name: deploy-api-docs
on:
push:
branches: ["unstable"]
branches: ["master"]
paths: ["docs/**"] # only run if the docs are updated
# Allows you to run this workflow manually from the Actions tab
@ -32,12 +32,12 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- name: Setup Pages
uses: actions/configure-pages@f156874f8191504dae5b037505266ed5dda6c382 # v3
uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4
- name: Upload artifact
uses: actions/upload-pages-artifact@a753861a5debcf57bf8b404356158c8e1e33150c # v2
uses: actions/upload-pages-artifact@0252fc4ba7626f0298f0cf00902a25c6afc77fa8 # v3
with:
# Only upload the api docs folder
path: "docs/api"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@9dbe3824824f8a1377b8e298bafde1a50ede43e5 # v2
uses: actions/deploy-pages@7a9bd943aa5e5175aeb8502edcc6c1c02d398e10 # v4

View File

@ -2,7 +2,6 @@ name: lint
on:
pull_request:
jobs:
brightscript:
runs-on: ubuntu-latest
@ -79,4 +78,4 @@ jobs:
- name: Install roku package dependencies
run: npx ropm install
- name: Check markdown files for spelling errors
run: npm run lint-spelling
run: npm run lint-spelling

80
.github/workflows/release-prep.yml vendored Normal file
View File

@ -0,0 +1,80 @@
# All of the jobs in this workflow will only run if the PR that triggered it has a 'release-prep' label
name: release-prep
on:
pull_request:
types: [labeled, opened, reopened, synchronize]
jobs:
version-check:
if: ${{ contains(github.event.pull_request.labels.*.name, 'release-prep') }}
runs-on: ubuntu-latest
steps:
- name: DEBUG ${{ github.event.pull_request.base.ref }}
run: echo ${{ github.event.pull_request.base.ref }}
- name: Checkout the branch this PR wants to update
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
ref: ${{ github.event.pull_request.base.ref }}
- name: Install jq to parse json
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: jq
- name: Save old package.json version
run: echo "oldPackVersion=$(jq -r ".version" package.json)" >> $GITHUB_ENV
- name: Find and save old major_version from manifest
run: awk 'BEGIN { FS="=" } /^major_version/ { print "oldMajor="$2; }' manifest >> $GITHUB_ENV
- name: Find and save old minor_version from manifest
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "oldMinor="$2; }' manifest >> $GITHUB_ENV
- name: Find and save old build_version from manifest
run: awk 'BEGIN { FS="=" } /^build_version/ { print "oldBuild="$2; }' manifest >> $GITHUB_ENV
- name: Save old manifest version
run: echo "oldManVersion=${{ env.oldMajor }}.${{ env.oldMinor }}.${{ env.oldBuild }}" >> $GITHUB_ENV
- name: Save old Makefile version
run: awk 'BEGIN { FS=" := " } /^VERSION/ { print "oldMakeVersion="$2; }' Makefile >> $GITHUB_ENV
- name: Checkout PR branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- name: Save new package.json version
run: echo "newPackVersion=$(jq -r ".version" package.json)" >> $GITHUB_ENV
- name: package.json version must be updated
if: env.oldPackVersion == env.newPackVersion
run: exit 1
- name: Find and save new major_version from manifest
run: awk 'BEGIN { FS="=" } /^major_version/ { print "newMajor="$2; }' manifest >> $GITHUB_ENV
- name: Find and save new minor_version from manifest
run: awk 'BEGIN { FS="=" } /^minor_version/ { print "newMinor="$2; }' manifest >> $GITHUB_ENV
- name: Find and save new build_version from manifest
run: awk 'BEGIN { FS="=" } /^build_version/ { print "newBuild="$2; }' manifest >> $GITHUB_ENV
- name: Save new manifest version
run: echo "newManVersion=${{ env.newMajor }}.${{ env.newMinor }}.${{ env.newBuild }}" >> $GITHUB_ENV
- name: Manifest version must be updated
if: env.oldManVersion == env.newManVersion
run: exit 1
- name: Save new Makefile version
run: awk 'BEGIN { FS=" := " } /^VERSION/ { print "newMakeVersion="$2; }' Makefile >> $GITHUB_ENV
- name: Makefile version must be updated
if: env.oldMakeVersion == env.newMakeVersion
run: exit 1
- name: All new versions must match
if: (env.newManVersion != env.newPackVersion) || (env.newManVersion != env.newMakeVersion)
run: exit 1
build-prod:
if: ${{ contains(github.event.pull_request.labels.*.name, 'release-prep') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4
with:
node-version: "lts/*"
cache: "npm"
- name: NPM install
run: npm ci
- name: Install roku module dependencies
run: npm run ropm
- name: Build app for production
run: npm run build-prod
- uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4
with:
name: Jellyfin-Roku-v${{ env.newManVersion }}-${{ github.sha }}
path: ${{ github.workspace }}/build/staging
if-no-files-found: error

View File

@ -27,7 +27,7 @@ jobs:
if: env.BRANCH_NAME == 'master'
run: npm run build-prod
- name: Use Java 17
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
with:
distribution: "temurin"
java-version: "17"

View File

@ -18,4 +18,4 @@
"docs/api/**": true
},
"brightscriptcomment.addExtraAtStartAndEnd": false
}
}

View File

@ -3,7 +3,7 @@
# If you want to get_images, you'll also need convert from ImageMagick
##########################################################################
VERSION := 1.6.6
VERSION := 2.0.1
## usage

View File

@ -1,8 +1,16 @@
import "pkg:/source/utils/misc.bs"
' @fileoverview Clock component to display current time formatted based on user's chosen 12 or 24 hour setting
' Possible clock formats
enum ClockFormat
h12 = "12h"
h24 = "24h"
end enum
sub init()
' If hideclick setting is checked, exit without setting any variables
' If hideclick setting is enabled, exit without setting any variables
if m.global.session.user.settings["ui.design.hideclock"]
return
end if
@ -16,11 +24,11 @@ sub init()
m.currentTimeTimer.control = "start"
' Default to 12 hour clock
m.format = "short-h12"
m.format = ClockFormat.h12
' If user has selected a 24 hour clock, update date display format
if LCase(m.global.device.clockFormat) = "24h"
m.format = "short-h24"
if LCase(m.global.device.clockFormat) = ClockFormat.h24
m.format = ClockFormat.h24
end if
end sub
@ -34,6 +42,64 @@ sub onCurrentTimeTimerFire()
' Convert to local time zone
m.dateTimeObject.ToLocalTime()
' Format time as requested
m.clockTime.text = m.dateTimeObject.asTimeStringLoc(m.format)
' Format time for display - based on 12h/24h setting
formattedTime = formatTimeAsString()
' Display time
m.clockTime.text = formattedTime
end sub
' formatTimeAsString: Returns a string with the current time formatted for either a 12 or 24 hour clock
'
' @return {string} current time formatted for either a 12 hour or 24 hour clock
function formatTimeAsString() as string
return m.format = ClockFormat.h12 ? format12HourTime() : format24HourTime()
end function
' format12HourTime: Returns a string with the current time formatted for a 12 hour clock
'
' @return {string} current time formatted for a 12 hour clock
function format12HourTime() as string
currentHour = m.dateTimeObject.GetHours()
currentMinute = m.dateTimeObject.GetMinutes()
displayedHour = StrI(currentHour).trim()
displayedMinute = StrI(currentMinute).trim()
meridian = currentHour < 12 ? "am" : "pm"
if currentHour = 0
displayedHour = "12"
end if
if currentHour > 12
correctedHour = currentHour - 12
displayedHour = StrI(correctedHour).trim()
end if
if currentMinute < 10
displayedMinute = `0${displayedMinute}`
end if
return `${displayedHour}:${displayedMinute} ${meridian}`
end function
' format24HourTime: Returns a string with the current time formatted for a 24 hour clock
'
' @return {string} current time formatted for a 24 hour clock
function format24HourTime() as string
currentHour = m.dateTimeObject.GetHours()
currentMinute = m.dateTimeObject.GetMinutes()
displayedHour = StrI(currentHour).trim()
displayedMinute = StrI(currentMinute).trim()
if currentHour < 10
displayedHour = `0${displayedHour}`
end if
if currentMinute < 10
displayedMinute = `0${displayedMinute}`
end if
return `${displayedHour}:${displayedMinute}`
end function

View File

@ -15,6 +15,10 @@ sub init()
m.unplayedCount = m.top.findNode("unplayedCount")
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
m.playedIndicator = m.top.findNode("playedIndicator")
m.checkmark = m.top.findNode("checkmark")
m.checkmark.width = 90
m.checkmark.height = 60
m.itemText.translation = [0, m.itemPoster.height + 7]
@ -44,6 +48,10 @@ sub itemContentChanged()
if itemData = invalid then return
if itemData.type = "Movie"
if isValid(itemData.json) and isValid(itemData.json.UserData) and isValid(itemData.json.UserData.Played) and itemData.json.UserData.Played
m.playedIndicator.visible = true
end if
m.itemPoster.uri = itemData.PosterUrl
m.itemIcon.uri = itemData.iconUrl
m.itemText.text = itemData.Title
@ -59,6 +67,9 @@ sub itemContentChanged()
end if
end if
end if
if isValid(itemData.json) and isValid(itemData.json.UserData) and isValid(itemData.json.UserData.Played) and itemData.json.UserData.Played = true
m.playedIndicator.visible = true
end if
m.itemPoster.uri = itemData.PosterUrl
m.itemIcon.uri = itemData.iconUrl

View File

@ -4,9 +4,10 @@
<maskGroup id="posterMask" maskUri="pkg:/images/postermask.png" scaleRotateCenter="[145, 212.5]" scale="[0.85,0.85]">
<Poster id="backdrop" width="290" height="425" loadDisplayMode="scaleToZoom" uri="pkg:/images/white.9.png" />
<Poster id="itemPoster" width="290" height="425" loadDisplayMode="scaleToZoom">
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" translation="[201, 0]">
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" opacity=".99" translation="[201, 0]">
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:MediumBoldSystemFont" horizAlign="center" vertAlign="center" />
</Rectangle>
<PlayedCheckmark id="playedIndicator" color="#00a4dcFF" width="90" height="60" opacity=".99" translation="[201, 0]" visible="false" />
</Poster>
<Poster id="itemIcon" width="50" height="50" translation="[230,10]" />
<Label id="posterText" width="280" height="415" translation="[5,5]" horizAlign="center" vertAlign="center" ellipsizeOnBoundary="true" wrap="true" />

View File

@ -8,6 +8,7 @@ sub init()
m.posterText.font.size = 30
m.title.font.size = 25
m.backdrop = m.top.findNode("backdrop")
m.playedIndicator = m.top.findNode("playedIndicator")
m.itemPoster.observeField("loadStatus", "onPosterLoadStatusChanged")
@ -37,6 +38,10 @@ sub itemContentChanged()
if not isValid(itemData) then return
if isValid(itemData.json) and isValid(itemData.json.UserData) and isValid(itemData.json.UserData.Played) and itemData.json.UserData.Played
m.playedIndicator.visible = true
end if
m.itemPoster.uri = itemData.PosterUrl
m.posterText.text = itemData.title
m.title.text = itemData.title

View File

@ -6,6 +6,7 @@
<ScrollingLabel translation="[0,340]" id="title" horizAlign="center" font="font:SmallSystemFont" repeatCount="0" maxWidth="230" />
<Poster id="itemIcon" width="50" height="50" translation="[230,10]" />
<Label id="posterText" width="230" height="320" translation="[5,5]" horizAlign="center" vertAlign="center" ellipsizeOnBoundary="true" wrap="true" />
<PlayedCheckmark id="playedIndicator" color="#00a4dcFF" width="60" height="46" translation="[170, 15]" visible="false" />
</children>
<interface>
<field id="itemContent" type="node" onChange="itemContentChanged" />

View File

@ -64,9 +64,6 @@ sub init()
'set inital counts for overhang before content is loaded.
m.loadItemsTask.totalRecordCount = 0
m.spinner = m.top.findNode("spinner")
m.spinner.visible = true
m.Alpha = m.top.findNode("AlphaMenu")
m.AlphaSelected = m.top.findNode("AlphaSelected")
@ -92,7 +89,7 @@ end sub
'Load initial set of Data
sub loadInitialItems()
m.loadItemsTask.control = "stop"
m.spinner.visible = true
startLoadingSpinner()
if m.top.parentItem.json.Type = "CollectionFolder" 'or m.top.parentItem.json.Type = "Folder"
m.top.HomeLibraryItem = m.top.parentItem.Id
@ -238,7 +235,7 @@ sub loadInitialItems()
end if
m.loadItemsTask.observeField("content", "ItemDataLoaded")
m.spinner.visible = true
startLoadingSpinner(false)
m.loadItemsTask.control = "RUN"
SetUpOptions()
end sub
@ -259,7 +256,8 @@ sub setMoviesOptions(options)
{ "Title": tr("OFFICIAL_RATING"), "Name": "OfficialRating" },
{ "Title": tr("PLAY_COUNT"), "Name": "PlayCount" },
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
{ "Title": tr("RUNTIME"), "Name": "Runtime" }
{ "Title": tr("RUNTIME"), "Name": "Runtime" },
{ "Title": tr("Random"), "Name": "Random" },
]
options.filter = [
{ "Title": tr("All"), "Name": "All" },
@ -278,6 +276,7 @@ sub setBoxsetsOptions(options)
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
{ "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
{ "Title": tr("Random"), "Name": "Random" },
]
options.filter = [
{ "Title": tr("All"), "Name": "All" },
@ -302,6 +301,7 @@ sub setTvShowsOptions(options)
{ "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
{ "Title": tr("OFFICIAL_RATING"), "Name": "OfficialRating" },
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
{ "Title": tr("Random"), "Name": "Random" },
]
options.filter = [
{ "Title": tr("All"), "Name": "All" },
@ -348,6 +348,7 @@ sub setMusicOptions(options)
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
{ "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
{ "Title": tr("Random"), "Name": "Random" },
]
options.filter = [
{ "Title": tr("All"), "Name": "All" },
@ -450,6 +451,7 @@ end sub
'
'Handle loaded data, and add to Grid
sub ItemDataLoaded(msg)
stopLoadingSpinner()
m.top.alphaActive = false
itemData = msg.GetData()
m.loadItemsTask.unobserveField("content")
@ -475,7 +477,7 @@ sub ItemDataLoaded(msg)
m.genreList.setFocus(true)
m.loading = false
m.spinner.visible = false
stopLoadingSpinner()
return
end if
@ -498,7 +500,7 @@ sub ItemDataLoaded(msg)
m.itemGrid.setFocus(true)
m.genreList.setFocus(false)
m.spinner.visible = false
stopLoadingSpinner()
end sub
'
@ -571,8 +573,9 @@ end sub
'
'Load next set of items
sub loadMoreData()
m.spinner.visible = true
if m.Loading = true then return
startLoadingSpinner(false)
m.Loading = true
m.loadItemsTask.startIndex = m.loadedItems
m.loadItemsTask.observeField("content", "ItemDataLoaded")
@ -594,7 +597,7 @@ sub onItemalphaSelected()
m.loadItemsTask.searchTerm = ""
m.VoiceBox.text = ""
m.loadItemsTask.nameStartsWith = m.alpha.itemAlphaSelected
m.spinner.visible = true
startLoadingSpinner(false)
loadInitialItems()
end if
end sub
@ -609,7 +612,7 @@ sub onvoiceFilter()
m.loadItemsTask.NameStartsWith = " "
m.loadItemsTask.searchTerm = m.voiceBox.text
m.loadItemsTask.recursive = true
m.spinner.visible = true
startLoadingSpinner(false)
loadInitialItems()
end if
end sub
@ -735,6 +738,9 @@ sub onChannelSelected(msg)
if node.watchChannel <> invalid
' Clone the node when it's reused/update in the TimeGrid it doesn't automatically start playing
m.top.selectedItem = node.watchChannel.clone(false)
' Make sure to set watchChanel to invalid in case the user hits back and then selects
' the same channel on the guide (without moving away from the currently selected channel)
m.tvGuide.watchChannel = invalid
end if
end sub
@ -842,7 +848,6 @@ function onKeyEvent(key as string, press as boolean) as boolean
end if
if key = "replay"
m.spinner.visible = true
m.loadItemsTask.searchTerm = ""
m.loadItemsTask.nameStartsWith = ""
m.voiceBox.text = ""

View File

@ -22,7 +22,6 @@
<Button id="micButton" maxWidth="20" translation="[20, 120]" iconUri="pkg:/images/icons/mic_icon.png" />
<Label translation="[0,540]" id="emptyText" font="font:LargeSystemFont" width="1910" horizAlign="center" vertAlign="center" height="64" visible="false" />
<ItemGridOptions id="options" visible="false" />
<Spinner id="spinner" translation="[900, 450]" />
<Animation id="backroundSwapAnimation" duration="1" repeat="false" easeFunction="linear">
<FloatFieldInterpolator id="fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 0.25 ]" fieldToInterp="backdropTransition.opacity" />
<FloatFieldInterpolator id="fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 0.25, 0.00 ]" fieldToInterp="backdrop.opacity" />

View File

@ -7,6 +7,11 @@ import "pkg:/source/api/Image.bs"
import "pkg:/source/api/userauth.bs"
import "pkg:/source/utils/deviceCapabilities.bs"
enum SubtitleSelection
notset = -2
none = -1
end enum
sub init()
m.user = AboutMe()
m.top.functionName = "loadItems"
@ -44,19 +49,18 @@ sub loadItems()
id = m.top.itemId
mediaSourceId = invalid
audio_stream_idx = m.top.selectedAudioStreamIndex
subtitle_idx = m.top.selectedSubtitleIndex
forceTranscoding = false
m.top.content = [LoadItems_VideoPlayer(id, mediaSourceId, audio_stream_idx, subtitle_idx, forceTranscoding)]
m.top.content = [LoadItems_VideoPlayer(id, mediaSourceId, audio_stream_idx, forceTranscoding)]
end sub
function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, forceTranscoding = false as boolean) as dynamic
function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean) as dynamic
video = {}
video.id = id
video.content = createObject("RoSGNode", "ContentNode")
LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, forceTranscoding)
LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, forceTranscoding)
if video.content = invalid
return invalid
@ -65,9 +69,10 @@ function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic,
return video
end function
sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, forceTranscoding = false as boolean)
sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean)
meta = ItemMetaData(video.id)
subtitle_idx = m.top.selectedSubtitleIndex
if not isValid(meta)
video.errorMsg = "Error loading metadata"
@ -77,6 +82,21 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
videotype = LCase(meta.type)
' Check for any Live TV streams coming from other places other than the TV Guide
if isValid(meta.json) and isValid(meta.json.ChannelId)
if isValid(meta.json.EpisodeTitle)
meta.title = meta.json.EpisodeTitle
else if isValid(meta.json.Name)
meta.title = meta.json.Name
end if
meta.live = true
if LCase(meta.json.type) = "program"
video.id = meta.json.ChannelId
else
video.id = meta.json.id
end if
end if
if videotype = "episode" or videotype = "series"
video.content.contenttype = "episode"
end if
@ -107,16 +127,41 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
if meta.live then mediaSourceId = ""
m.playbackInfo = ItemPostPlaybackInfo(video.id, mediaSourceId, audio_stream_idx, subtitle_idx, playbackPosition)
video.videoId = video.id
video.mediaSourceId = mediaSourceId
video.audioIndex = audio_stream_idx
if not isValid(m.playbackInfo)
video.errorMsg = "Error loading playback info"
video.content = invalid
return
end if
addSubtitlesToVideo(video, meta)
' Enable default subtitle track
if subtitle_idx = SubtitleSelection.notset
defaultSubtitleIndex = defaultSubtitleTrackFromVid(video.id)
if defaultSubtitleIndex <> SubtitleSelection.none
video.SelectedSubtitle = defaultSubtitleIndex
subtitle_idx = defaultSubtitleIndex
m.playbackInfo = ItemPostPlaybackInfo(video.id, mediaSourceId, audio_stream_idx, subtitle_idx, playbackPosition)
if not isValid(m.playbackInfo)
video.errorMsg = "Error loading playback info"
video.content = invalid
return
end if
addSubtitlesToVideo(video, meta)
else
video.SelectedSubtitle = subtitle_idx
end if
else
video.SelectedSubtitle = subtitle_idx
end if
video.videoId = video.id
video.mediaSourceId = mediaSourceId
video.audioIndex = audio_stream_idx
video.PlaySessionId = m.playbackInfo.PlaySessionId
if meta.live
@ -130,9 +175,7 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
m.playbackInfo = meta.json
end if
addSubtitlesToVideo(video, meta)
addAudioStreamsToVideo(video)
if meta.live
video.transcodeParams = {
"MediaSourceId": m.playbackInfo.MediaSources[0].Id,
@ -184,13 +227,106 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
setCertificateAuthority(video.content)
video.audioTrack = (audio_stream_idx + 1).ToStr() ' Roku's track indexes count from 1. Our index is zero based
video.SelectedSubtitle = subtitle_idx
if not fully_external
video.content = authRequest(video.content)
end if
end sub
' defaultSubtitleTrackFromVid: Identifies the default subtitle track given video id
'
' @param {dynamic} videoID - id of video user is playing
' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} is one is not found
function defaultSubtitleTrackFromVid(videoID) as integer
if m.global.session.user.configuration.SubtitleMode = "None"
return SubtitleSelection.none ' No subtitles desired: return none
end if
meta = ItemMetaData(videoID)
if not isValid(meta) then return SubtitleSelection.none
if not isValid(meta.json) then return SubtitleSelection.none
if not isValidAndNotEmpty(meta.json.mediaSources) then return SubtitleSelection.none
if not isValidAndNotEmpty(meta.json.MediaSources[0].MediaStreams) then return SubtitleSelection.none
subtitles = sortSubtitles(meta.id, meta.json.MediaSources[0].MediaStreams)
selectedAudioLanguage = meta.json.MediaSources[0].MediaStreams[m.top.selectedAudioStreamIndex].Language ?? ""
defaultTextSubs = defaultSubtitleTrack(subtitles["all"], selectedAudioLanguage, true) ' Find correct subtitle track (forced text)
if defaultTextSubs <> SubtitleSelection.none
return defaultTextSubs
end if
if not m.global.session.user.settings["playback.subs.onlytext"]
return defaultSubtitleTrack(subtitles["all"], selectedAudioLanguage) ' if no appropriate text subs exist, allow non-text
end if
return SubtitleSelection.none
end function
' defaultSubtitleTrack:
'
' @param {dynamic} sortedSubtitles - array of subtitles sorted by type and language
' @param {string} selectedAudioLanguage - language for selected audio track
' @param {boolean} [requireText=false] - indicates if only text subtitles should be considered
' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} is one is not found
function defaultSubtitleTrack(sortedSubtitles, selectedAudioLanguage as string, requireText = false as boolean) as integer
userConfig = m.global.session.user.configuration
subtitleMode = isValid(userConfig.SubtitleMode) ? LCase(userConfig.SubtitleMode) : ""
allowSmartMode = false
' Only evaluate selected audio language if we have a value
if selectedAudioLanguage <> ""
allowSmartMode = selectedAudioLanguage <> userConfig.SubtitleLanguagePreference
end if
for each item in sortedSubtitles
' Only auto-select subtitle if language matches SubtitleLanguagePreference
languageMatch = true
if userConfig.SubtitleLanguagePreference <> ""
languageMatch = (userConfig.SubtitleLanguagePreference = item.Track.Language)
end if
' Ensure textuality of subtitle matches preferenced passed as arg
matchTextReq = ((requireText and item.IsTextSubtitleStream) or not requireText)
if languageMatch and matchTextReq
if subtitleMode = "default" and (item.isForced or item.IsDefault)
' Return first forced or default subtitle track
return item.Index
else if subtitleMode = "always"
' Return the first found subtitle track
return item.Index
else if subtitleMode = "onlyforced" and item.IsForced
' Return first forced subtitle track
return item.Index
else if subtitleMode = "smart" and allowSmartMode
' Return the first found subtitle track
return item.Index
end if
end if
end for
' User has chosed smart subtitle mode
' We already attempted to load subtitles in preferred language, but none were found.
' Fall back to default behaviour while ignoring preferredlanguage
if subtitleMode = "smart" and allowSmartMode
for each item in sortedSubtitles
' Ensure textuality of subtitle matches preferenced passed as arg
matchTextReq = ((requireText and item.IsTextSubtitleStream) or not requireText)
if matchTextReq
if item.isForced or item.IsDefault
' Return first forced or default subtitle track
return item.Index
end if
end if
end for
end if
return SubtitleSelection.none ' Keep current default behavior of "None", if no correct subtitle is identified
end function
sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
if protocol <> "file"

View File

@ -4,7 +4,7 @@
<interface>
<field id="itemId" type="string" />
<field id="selectedAudioStreamIndex" type="integer" value="0" />
<field id="selectedSubtitleIndex" type="integer" value="-1" />
<field id="selectedSubtitleIndex" type="integer" value="-2" />
<field id="isIntro" type="boolean" />
<field id="startIndex" type="integer" value="0" />
<field id="itemType" type="string" value="" />

View File

@ -17,7 +17,6 @@ sub setupNodes()
m.selectedMovieOfficialRating = m.top.findNode("selectedMovieOfficialRating")
m.movieLogo = m.top.findNode("movieLogo")
m.swapAnimation = m.top.findNode("backroundSwapAnimation")
m.spinner = m.top.findNode("spinner")
m.Alpha = m.top.findNode("AlphaMenu")
m.AlphaSelected = m.top.findNode("AlphaSelected")
m.micButton = m.top.findNode("micButton")
@ -83,8 +82,6 @@ sub init()
'set inital counts for overhang before content is loaded.
m.loadItemsTask.totalRecordCount = 0
m.spinner.visible = true
'Get reset folder setting
m.resetGrid = m.global.session.user.settings["itemgrid.reset"]
@ -117,7 +114,7 @@ end sub
'Load initial set of Data
sub loadInitialItems()
m.loadItemsTask.control = "stop"
m.spinner.visible = true
startLoadingSpinner(false)
if m.top.parentItem.json.Type = "CollectionFolder"
m.top.HomeLibraryItem = m.top.parentItem.Id
@ -219,7 +216,6 @@ sub loadInitialItems()
end if
m.loadItemsTask.observeField("content", "ItemDataLoaded")
m.spinner.visible = true
m.loadItemsTask.control = "RUN"
m.getFiltersTask.observeField("filters", "FilterDataLoaded")
@ -257,7 +253,8 @@ sub setMoviesOptions(options)
{ "Title": tr("OFFICIAL_RATING"), "Name": "OfficialRating" },
{ "Title": tr("PLAY_COUNT"), "Name": "PlayCount" },
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
{ "Title": tr("RUNTIME"), "Name": "Runtime" }
{ "Title": tr("RUNTIME"), "Name": "Runtime" },
{ "Title": tr("Random"), "Name": "Random" },
]
options.filter = [
@ -276,7 +273,7 @@ sub setMoviesOptions(options)
if m.options.view = "Studios" or m.view = "Studios"
options.sort = [
{ "Title": tr("TITLE"), "Name": "SortName" },
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" }
]
options.filter = [
{ "Title": tr("All"), "Name": "All" },
@ -436,7 +433,7 @@ sub ItemDataLoaded(msg)
m.genreList.setFocus(true)
m.loading = false
m.spinner.visible = false
stopLoadingSpinner()
' Return focus to options menu if it was opened while library was loading
if m.options.visible
m.options.setFocus(true)
@ -486,7 +483,7 @@ sub ItemDataLoaded(msg)
m.emptyText.visible = true
end if
m.spinner.visible = false
stopLoadingSpinner()
' Return focus to options menu if it was opened while library was loading
if m.options.visible
m.options.setFocus(true)
@ -691,8 +688,9 @@ end sub
'
'Load next set of items
sub loadMoreData()
m.spinner.visible = true
if m.Loading = true then return
startLoadingSpinner(false)
m.Loading = true
m.loadItemsTask.startIndex = m.loadedItems
m.loadItemsTask.observeField("content", "ItemDataLoaded")
@ -736,7 +734,6 @@ sub onItemalphaSelected()
m.loadItemsTask.searchTerm = ""
m.VoiceBox.text = ""
m.loadItemsTask.nameStartsWith = m.alpha.itemAlphaSelected
m.spinner.visible = true
loadInitialItems()
end if
end sub
@ -751,7 +748,6 @@ sub onvoiceFilter()
m.loadItemsTask.NameStartsWith = " "
m.loadItemsTask.searchTerm = m.voiceBox.text
m.loadItemsTask.recursive = true
m.spinner.visible = true
loadInitialItems()
end if
end sub
@ -924,7 +920,6 @@ function onKeyEvent(key as string, press as boolean) as boolean
end if
if key = "replay"
m.spinner.visible = true
m.loadItemsTask.searchTerm = ""
m.loadItemsTask.nameStartsWith = ""
m.voiceBox.text = ""

View File

@ -39,7 +39,6 @@
<Button id="micButton" maxWidth="20" translation="[20, 120]" iconUri="pkg:/images/icons/mic_icon.png" />
<Label translation="[0,540]" id="emptyText" font="font:LargeSystemFont" width="1910" horizAlign="center" vertAlign="center" height="64" visible="false" />
<ItemGridOptions id="options" visible="false" />
<Spinner id="spinner" translation="[900, 450]" />
<Animation id="backroundSwapAnimation" duration="1" repeat="false" easeFunction="linear">
<FloatFieldInterpolator id="fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 1.00 ]" fieldToInterp="backdropTransition.opacity" />
<FloatFieldInterpolator id="fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 1.00, 0.00 ]" fieldToInterp="backdrop.opacity" />

View File

@ -17,7 +17,6 @@ sub setupNodes()
m.selectedArtistGenres = m.top.findNode("selectedArtistGenres")
m.artistLogo = m.top.findNode("artistLogo")
m.swapAnimation = m.top.findNode("backroundSwapAnimation")
m.spinner = m.top.findNode("spinner")
m.Alpha = m.top.findNode("AlphaMenu")
m.AlphaSelected = m.top.findNode("AlphaSelected")
m.micButton = m.top.findNode("micButton")
@ -77,8 +76,6 @@ sub init()
'set inital counts for overhang before content is loaded.
m.loadItemsTask.totalRecordCount = 0
m.spinner.visible = true
'Get reset folder setting
m.resetGrid = m.global.session.user.settings["itemgrid.reset"]
@ -111,7 +108,7 @@ end sub
'Load initial set of Data
sub loadInitialItems()
m.loadItemsTask.control = "stop"
m.spinner.visible = true
startLoadingSpinner(false)
if LCase(m.top.parentItem.json.Type) = "collectionfolder"
m.top.HomeLibraryItem = m.top.parentItem.Id
@ -143,14 +140,12 @@ sub loadInitialItems()
m.loadItemsTask.itemId = m.top.parentItem.parentFolder
else if LCase(m.view) = "artistspresentation" or LCase(m.options.view) = "artistspresentation"
m.loadItemsTask.genreIds = ""
m.top.showItemTitles = "hidealways"
else if LCase(m.view) = "artistsgrid" or LCase(m.options.view) = "artistsgrid"
m.loadItemsTask.genreIds = ""
else if LCase(m.view) = "albumartistsgrid" or LCase(m.options.view) = "albumartistsgrid"
m.loadItemsTask.genreIds = ""
else if LCase(m.view) = "albumartistspresentation" or LCase(m.options.view) = "albumartistspresentation"
m.loadItemsTask.genreIds = ""
m.top.showItemTitles = "hidealways"
else
m.loadItemsTask.itemId = m.top.parentItem.Id
end if
@ -204,7 +199,6 @@ sub loadInitialItems()
end if
m.loadItemsTask.observeField("content", "ItemDataLoaded")
m.spinner.visible = true
m.loadItemsTask.control = "RUN"
SetUpOptions()
end sub
@ -232,6 +226,7 @@ sub setMusicOptions(options)
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
{ "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
{ "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
{ "Title": tr("Random"), "Name": "Random" },
]
options.filter = [
@ -242,6 +237,7 @@ sub setMusicOptions(options)
if LCase(m.options.view) = "genres" or LCase(m.view) = "genres"
options.sort = [
{ "Title": tr("TITLE"), "Name": "SortName" },
{ "Title": tr("Random"), "Name": "Random" },
]
options.filter = []
end if
@ -250,6 +246,7 @@ sub setMusicOptions(options)
options.sort = [
{ "Title": tr("TITLE"), "Name": "SortName" },
{ "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
{ "Title": tr("Random"), "Name": "Random" },
]
end if
end sub
@ -326,6 +323,7 @@ end sub
'
'Handle loaded data, and add to Grid
sub ItemDataLoaded(msg)
stopLoadingSpinner()
m.top.alphaActive = false
itemData = msg.GetData()
m.loadItemsTask.unobserveField("content")
@ -351,7 +349,6 @@ sub ItemDataLoaded(msg)
m.loadedRows = m.loadedItems / m.genreList.numColumns
m.loading = false
m.spinner.visible = false
return
end if
@ -374,8 +371,6 @@ sub ItemDataLoaded(msg)
m.emptyText.text = tr("NO_ITEMS").Replace("%1", m.top.parentItem.Type)
m.emptyText.visible = true
end if
m.spinner.visible = false
end sub
'
@ -556,8 +551,9 @@ end sub
'
'Load next set of items
sub loadMoreData()
m.spinner.visible = true
if m.Loading = true then return
startLoadingSpinner(false)
m.Loading = true
m.loadItemsTask.startIndex = m.loadedItems
m.loadItemsTask.observeField("content", "ItemDataLoaded")
@ -612,7 +608,6 @@ sub onItemalphaSelected()
m.loadItemsTask.searchTerm = ""
m.VoiceBox.text = ""
m.loadItemsTask.nameStartsWith = m.alpha.itemAlphaSelected
m.spinner.visible = true
loadInitialItems()
end if
end sub
@ -627,7 +622,6 @@ sub onvoiceFilter()
m.loadItemsTask.NameStartsWith = " "
m.loadItemsTask.searchTerm = m.voiceBox.text
m.loadItemsTask.recursive = true
m.spinner.visible = true
loadInitialItems()
end if
end sub
@ -787,7 +781,6 @@ function onKeyEvent(key as string, press as boolean) as boolean
end if
if key = "replay"
m.spinner.visible = true
m.loadItemsTask.searchTerm = ""
m.loadItemsTask.nameStartsWith = ""
m.voiceBox.text = ""

View File

@ -24,7 +24,6 @@
<Button id="micButton" maxWidth="20" translation="[20, 120]" iconUri="pkg:/images/icons/mic_icon.png" />
<Label translation="[0,540]" id="emptyText" font="font:LargeSystemFont" width="1910" horizAlign="center" vertAlign="center" height="64" visible="false" />
<ItemGridOptions id="options" visible="false" />
<Spinner id="spinner" translation="[900, 450]" />
<Animation id="backroundSwapAnimation" duration="1" repeat="false" easeFunction="linear">
<FloatFieldInterpolator id="fadeinLoading" key="[0.0, 1.0]" keyValue="[ 0.00, 1.00 ]" fieldToInterp="backdropTransition.opacity" />
<FloatFieldInterpolator id="fadeoutLoaded" key="[0.0, 1.0]" keyValue="[ 1.00, 0.00 ]" fieldToInterp="backdrop.opacity" />

View File

@ -3,6 +3,27 @@ import "pkg:/source/utils/misc.bs"
sub init()
m.top.backgroundColor = "#262626" '"#101010"
m.top.backgroundURI = ""
m.spinner = m.top.findNode("spinner")
end sub
' Triggered when the isLoading boolean component field is changed
sub isLoadingChanged()
m.spinner.visible = m.top.isLoading
end sub
' Triggered when the disableRemote boolean component field is changed
sub disableRemoteChanged()
if m.top.disableRemote
dialog = createObject("roSGNode", "ProgressDialog")
dialog.id = "invisibiledialog"
dialog.visible = false
dialog.opacity = 0
m.top.dialog = dialog
else
if isValid(m.top.dialog)
m.top.dialog.close = true
end if
end if
end sub
function onKeyEvent(key as string, press as boolean) as boolean

View File

@ -3,8 +3,11 @@
<children>
<Group id="content" />
<JFOverhang id="overhang" />
<Spinner id="spinner" translation="[897, 477]" visible="false" />
</children>
<interface>
<field id="disableRemote" type="boolean" value="false" onchange="disableRemoteChanged" />
<field id="isLoading" type="boolean" value="false" onchange="isLoadingChanged" />
<field id="exit" type="boolean" alwaysNotify="true" />
</interface>
</component>

View File

@ -8,6 +8,10 @@ sub init()
m.poster = m.top.findNode("poster")
m.unplayedCount = m.top.findNode("unplayedCount")
m.unplayedEpisodeCount = m.top.findNode("unplayedEpisodeCount")
m.playedIndicator = m.top.findNode("playedIndicator")
m.checkmark = m.top.findNode("checkmark")
m.checkmark.width = 90
m.checkmark.height = 60
m.backdrop = m.top.findNode("backdrop")
@ -65,6 +69,10 @@ sub itemContentChanged() as void
end if
end if
if isValid(itemData.json) and isValid(itemData.json.UserData) and isValid(itemData.json.UserData.Played) and itemData.json.UserData.Played
m.playedIndicator.visible = true
end if
if itemData.json.lookup("Type") = "Episode" and isValid(itemData.json.IndexNumber)
m.title.text = StrI(itemData.json.IndexNumber) + ". " + m.title.text

View File

@ -4,9 +4,10 @@
<Rectangle id="backdrop" />
<ScrollingLabel id="Series" horizAlign="center" font="font:SmallSystemFont" repeatCount="0" visible="false" />
<Poster id="poster" translation="[2,0]" loadDisplayMode="scaleToFit">
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" translation="[104, 0]">
<Rectangle id="unplayedCount" visible="false" width="90" height="60" color="#00a4dcFF" translation="[102, 0]">
<Label id="unplayedEpisodeCount" width="90" height="60" font="font:MediumBoldSystemFont" horizAlign="center" vertAlign="center" />
</Rectangle>
<PlayedCheckmark id="playedIndicator" color="#00a4dcFF" width="90" height="60" translation="[102, 0]" visible="false" />
</Poster>
<ScrollingLabel id="title" horizAlign="center" font="font:SmallSystemFont" repeatCount="0" visible="false" />
<Label id="staticTitle" horizAlign="center" font="font:SmallSystemFont" wrap="false" />

View File

@ -2,5 +2,5 @@ sub init()
m.top.poster.uri = "pkg:/images/spinner.png"
m.top.control = "start"
m.top.clockwise = true
m.top.spinInterval = 3
m.top.spinInterval = 1
end sub

View File

@ -5,7 +5,7 @@ sub init()
m.top.id = "OKDialog"
m.top.height = 900
m.top.title = "What's New?"
m.top.title = m.global.app.version + " - " + tr("What's New?")
m.top.buttons = [tr("OK")]
dialogStyles = {
@ -21,7 +21,7 @@ sub init()
}
}
whatsNewList = ParseJSON(ReadAsciiFile("pkg:/source/static/whatsNew.json"))
whatsNewList = ParseJSON(ReadAsciiFile("pkg:/source/static/whatsNew/" + m.global.app.version.ToStr().trim() + ".json"))
for each item in whatsNewList
textLine = m.content.CreateChild("StdDlgMultiStyleTextItem")

View File

@ -5,7 +5,6 @@ sub init()
m.log = log.Logger("SetServerScreen")
m.top.setFocus(true)
m.spinner = m.top.findNode("spinner")
m.serverPicker = m.top.findNode("serverPicker")
m.serverUrlTextbox = m.top.findNode("serverUrlTextbox")
m.serverUrlContainer = m.top.findNode("serverUrlContainer")
@ -76,7 +75,7 @@ sub ScanForServers()
'run the task
m.ssdpScanner.observeField("content", "ScanForServersComplete")
m.ssdpScanner.control = "RUN"
m.spinner.visible = true
startLoadingSpinner(false)
end sub
sub ScanForServersComplete(event)
@ -109,7 +108,7 @@ sub ScanForServersComplete(event)
end if
m.serverPicker.content = items
m.spinner.visible = false
stopLoadingSpinner()
'if we have at least one server, focus on the server picker
if m.servers.Count() > 0

View File

@ -15,7 +15,6 @@
</LayoutGroup>
<!--background for server picker-->
<Rectangle color="0x00000020" width="1620" height="400">
<Spinner id="spinner" translation="[717, 136]" />
<MarkupList id="serverPicker" translation="[50, 20]" itemComponentName="JFServer" itemSpacing="[0, 10]" itemSize="[1520, 100]" numRows="3" vertFocusAnimationStyle="floatingFocus" />
</Rectangle>

View File

@ -1,4 +1,5 @@
import "pkg:/source/roku_modules/log/LogMixin.brs"
import "pkg:/source/utils/misc.bs"
sub init()
m.log = log.Logger("SceneManager")
@ -120,7 +121,7 @@ sub popScene()
' Exit app if the stack is empty after removing group
m.scene.exit = true
end if
stopLoadingSpinner()
end sub

View File

@ -53,12 +53,6 @@ function setPreference(key as string, value as string)
return set_user_setting("pref-" + key, value)
end function
sub setActive()
if m.global.session.user.settings["global.rememberme"]
set_setting("active_user", m.top.id)
end if
end sub
sub setServer(hostname as string)
m.top.server = hostname
end sub

View File

@ -11,6 +11,5 @@
<function name="setPreference" />
<function name="loadFromRegistry" />
<function name="saveToRegistry" />
<function name="setActive" />
</interface>
</component>

View File

@ -9,6 +9,10 @@ sub init()
m.top.optionsAvailable = true
m.postTask = createObject("roSGNode", "PostTask")
m.homeRows = m.top.findNode("homeRows")
m.fadeInFocusBitmap = m.top.findNode("fadeInFocusBitmap")
if m.global.session.user.settings["ui.home.splashBackground"] = true
m.backdrop = m.top.findNode("backdrop")
m.backdrop.uri = buildURL("/Branding/Splashscreen?format=jpg&foregroundLayer=0.15&fillWidth=1280&width=1280&fillHeight=720&height=720&tag=splash")
@ -16,11 +20,14 @@ sub init()
end sub
sub refresh()
m.top.findNode("homeRows").callFunc("updateHomeRows")
m.homeRows.focusBitmapBlendColor = "0xFFFFFFFF"
m.homeRows.callFunc("updateHomeRows")
end sub
sub loadLibraries()
m.top.findNode("homeRows").callFunc("loadLibraries")
m.homeRows.focusBitmapBlendColor = "0xFFFFFF00"
m.homeRows.callFunc("loadLibraries")
m.fadeInFocusBitmap.control = "start"
end sub
' JFScreen hook that gets ran as needed.

View File

@ -2,8 +2,12 @@
<component name="Home" extends="JFScreen">
<children>
<Poster id="backdrop" loadDisplayMode="scaleToZoom" width="1920" height="1200" />
<HomeRows id="homeRows" />
<HomeRows id="homeRows" focusBitmapBlendColor="0xFFFFFF00" />
<OptionsSlider id="options" />
<Animation id="fadeInFocusBitmap" delay="1" duration=".2" repeat="false" easeFunction="inQuad">
<ColorFieldInterpolator id="fadeInFocusBitmapInterpolator" key="[0.0, 1.0]" keyValue="[0xFFFFFF00, 0xFFFFFFFF]" fieldToInterp="homeRows.focusBitmapBlendColor" />
</Animation>
</children>
<interface>
<field id="selectedItem" alias="homeRows.selectedItem" />

View File

@ -28,6 +28,7 @@ end sub
sub itemContentChanged()
m.unplayedCount.visible = false
itemData = m.top.itemContent
if itemData = invalid then return
@ -65,6 +66,10 @@ sub itemContentChanged()
' Format the Data based on the type of Home Data
if itemData.type = "CollectionFolder" or itemData.type = "UserView" or itemData.type = "Channel"
m.itemText.font.size = 35
m.itemText.height = 64
m.itemText.horizAlign = "center"
m.itemText.vertAlign = "bottom"
m.itemText.text = itemData.name
m.itemPoster.uri = itemData.widePosterURL
return

View File

@ -2,6 +2,7 @@
<component name="HomeRow" extends="ContentNode">
<interface>
<field id="imageWidth" type="integer" value="464" />
<field id="cursorSize" type="array" value="[464, 331]" />
<field id="usePoster" type="bool" value="false" />
</interface>
</component>

View File

@ -1,4 +1,7 @@
import "pkg:/source/utils/misc.bs"
import "pkg:/source/constants/HomeRowItemSizes.bs"
const LOADING_WAIT_TIME = 2
sub init()
m.top.itemComponentName = "HomeItem"
@ -10,11 +13,14 @@ sub init()
m.top.showRowLabel = [true]
m.top.rowLabelOffset = [0, 20]
m.top.showRowCounter = [true]
' Hide the row counter to prevent flicker. We'll show it once loading timer fires
m.top.showRowCounter = [false]
m.homeSectionIndexes = {
count: 0
}
m.top.content = CreateObject("roSGNode", "ContentNode")
m.loadingTimer = createObject("roSGNode", "Timer")
m.loadingTimer.duration = LOADING_WAIT_TIME
m.loadingTimer.observeField("fire", "loadingTimerComplete")
updateSize()
@ -26,7 +32,7 @@ sub init()
m.LoadLibrariesTask = createObject("roSGNode", "LoadItemsTask")
m.LoadLibrariesTask.observeField("content", "onLibrariesLoaded")
' set up tesk nodes for other rows
' set up task nodes for other rows
m.LoadContinueWatchingTask = createObject("roSGNode", "LoadItemsTask")
m.LoadContinueWatchingTask.itemsToLoad = "continue"
@ -57,139 +63,239 @@ sub updateSize()
' spacing between items in a row
m.top.rowItemSpacing = [20, 0]
' Default size to wide poster, the most used size
m.top.rowItemSize = homeRowItemSizes.WIDE_POSTER
m.top.visible = true
end sub
sub onLibrariesLoaded()
' save data for other functions
m.libraryData = m.LoadLibrariesTask.content
m.LoadLibrariesTask.unobserveField("content")
m.LoadLibrariesTask.content = []
' processUserSections: Loop through user's chosen home section settings and generate the content for each row
'
sub processUserSections()
m.expectedRowCount = 1 ' the favorites row is hardcoded to always show atm
m.processedRowCount = 0
content = CreateObject("roSGNode", "ContentNode")
sizeArray = []
loadedSections = 0
' Add sections in order based on user settings
' calculate expected row count by processing homesections
for i = 0 to 6
sectionName = LCase(m.global.session.user.settings["homesection" + i.toStr()])
sectionLoaded = addHomeSection(content, sizeArray, sectionName)
if sectionName = "latestmedia"
' expect 1 row per filtered media library
m.filteredLatest = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.LatestItemsExcludes)
for each latestLibrary in m.filteredLatest
if latestLibrary.collectionType <> "boxsets" and latestLibrary.collectionType <> "livetv" and latestLibrary.json.CollectionType <> "Program"
m.expectedRowCount++
end if
end for
else if sectionName <> "none"
m.expectedRowCount++
end if
end for
' Add home sections in order based on user settings
loadedSections = 0
for i = 0 to 6
sectionName = LCase(m.global.session.user.settings["homesection" + i.toStr()])
sectionLoaded = false
if sectionName <> "none"
sectionLoaded = addHomeSection(sectionName)
end if
' Count how many sections with data are loaded
if sectionLoaded then loadedSections++
' If 2 sections with data are loaded or we're at the end of the web client section data, consider the home view loaded
if loadedSections = 2 or i = 6
if not m.global.app_loaded
if not m.global.app_loaded
if loadedSections = 2 or i = 6
m.top.signalBeacon("AppLaunchComplete") ' Roku Performance monitoring
m.global.app_loaded = true
end if
end if
end for
' Favorites isn't an option on Web settings, so we must manually add it for now
addHomeSection(content, sizeArray, "favorites")
' Favorites isn't an option in Web settings, so we manually add it to the end for now
addHomeSection("favorites")
m.top.rowItemSize = sizeArray
m.top.content = content
' Start the timer for creating the content rows before we set the cursor size
m.loadingTimer.control = "start"
end sub
' Removes a home section from the home rows
sub removeHomeSection(sectionType as string)
sectionName = LCase(sectionType)
' onLibrariesLoaded: Handler when LoadLibrariesTask returns data
'
sub onLibrariesLoaded()
' save data for other functions
m.libraryData = m.LoadLibrariesTask.content
m.LoadLibrariesTask.unobserveField("content")
m.LoadLibrariesTask.content = []
removedSectionIndex = m.homeSectionIndexes[sectionName]
processUserSections()
end sub
if not isValid(removedSectionIndex) then return
' getOriginalSectionIndex: Gets the index of a section from user settings and adds count of currently known latest media sections
'
' @param {string} sectionName - Name of section we're looking up
'
' @return {integer} indicating index of section taking latest media sections into account
function getOriginalSectionIndex(sectionName as string) as integer
searchSectionName = LCase(sectionName).Replace(" ", "")
for each section in m.homeSectionIndexes
if m.homeSectionIndexes[section] > removedSectionIndex
m.homeSectionIndexes[section]--
sectionIndex = 0
indexLatestMediaSection = 0
for i = 0 to 6
settingSectionName = LCase(m.global.session.user.settings["homesection" + i.toStr()])
if settingSectionName = "latestmedia"
indexLatestMediaSection = i
end if
if settingSectionName = searchSectionName
sectionIndex = i
end if
end for
m.homeSectionIndexes.Delete(sectionName)
' If the latest media section is before the section we're searching for, then we need to account for how many latest media rows there are
addLatestMediaSectionCount = (indexLatestMediaSection < sectionIndex)
m.top.content.removeChildIndex(removedSectionIndex)
if addLatestMediaSectionCount
for i = sectionIndex to m.top.content.getChildCount() - 1
sectionToTest = m.top.content.getChild(i)
if LCase(Left(sectionToTest.title, 6)) = "latest"
sectionIndex++
end if
end for
end if
return sectionIndex
end function
' removeHomeSection: Removes a home section from the home rows
'
' @param {string} sectionToRemove - Title property of section we're removing
sub removeHomeSection(sectionTitleToRemove as string)
if not isValid(sectionTitleToRemove) then return
sectionTitle = LCase(sectionTitleToRemove).Replace(" ", "")
if not sectionExists(sectionTitle) then return
sectionIndexToRemove = getSectionIndex(sectionTitle)
m.top.content.removeChildIndex(sectionIndexToRemove)
setRowItemSize()
end sub
' Adds a new home section to the home rows.
' Returns a boolean indicating whether the section was handled.
function addHomeSection(content as dynamic, sizeArray as dynamic, sectionName as string) as boolean
' setRowItemSize: Loops through all home sections and sets the correct item sizes per row
'
sub setRowItemSize()
if not isValid(m.top.content) then return
homeSections = m.top.content.getChildren(-1, 0)
newSizeArray = CreateObject("roArray", homeSections.count(), false)
for i = 0 to homeSections.count() - 1
newSizeArray[i] = isValid(homeSections[i].cursorSize) ? homeSections[i].cursorSize : homeRowItemSizes.WIDE_POSTER
end for
m.top.rowItemSize = newSizeArray
' If we have processed the expected number of content rows, stop the loading timer and run the complete function
if m.expectedRowCount = m.processedRowCount
m.loadingTimer.control = "stop"
loadingTimerComplete()
end if
end sub
' loadingTimerComplete: Event handler for when loading wait time has expired
'
sub loadingTimerComplete()
if not m.top.showRowCounter[0]
' Show the row counter to prevent flicker
m.top.showRowCounter = [true]
end if
end sub
' addHomeSection: Adds a new home section to the home rows.
'
' @param {string} sectionType - Type of section to add
' @return {boolean} indicating if the section was handled
function addHomeSection(sectionType as string) as boolean
' Poster size library items
if sectionName = "livetv"
createLiveTVRow(content, sizeArray)
if sectionType = "livetv"
createLiveTVRow()
return true
end if
' Poster size library items
if sectionName = "smalllibrarytiles"
createLibraryRow(content, sizeArray)
if sectionType = "smalllibrarytiles"
createLibraryRow()
return true
end if
' Continue Watching items
if sectionName = "resume"
createContinueWatchingRow(content, sizeArray)
if sectionType = "resume"
createContinueWatchingRow()
return true
end if
' Next Up items
if sectionName = "nextup"
createNextUpRow(content, sizeArray)
if sectionType = "nextup"
createNextUpRow()
return true
end if
' Latest items in each library
if sectionName = "latestmedia"
createLatestInRows(content, sizeArray)
if sectionType = "latestmedia"
createLatestInRows()
return true
end if
' Favorite Items
if sectionName = "favorites"
createFavoritesRow(content, sizeArray)
if sectionType = "favorites"
createFavoritesRow()
return true
end if
' This section type isn't supported.
' Count it as processed since we aren't going to do anything else with it
m.processedRowCount++
return false
end function
' Create a row displaying the user's libraries
sub createLibraryRow(content as dynamic, sizeArray as dynamic)
' createLibraryRow: Creates a row displaying the user's libraries
'
sub createLibraryRow()
m.processedRowCount++
' Ensure we have data
if not isValidAndNotEmpty(m.libraryData) then return
mediaRow = content.CreateChild("HomeRow")
mediaRow.title = tr("My Media")
sectionName = tr("My Media")
m.homeSectionIndexes.AddReplace("library", m.homeSectionIndexes.count)
m.homeSectionIndexes.count++
' We don't refresh library data, so if section already exists, exit
if sectionExists(sectionName)
return
end if
sizeArray.push([464, 331])
row = CreateObject("roSGNode", "HomeRow")
row.title = sectionName
row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
row.cursorSize = homeRowItemSizes.WIDE_POSTER
filteredMedia = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.MyMediaExcludes)
for each item in filteredMedia
mediaRow.appendChild(item)
row.appendChild(item)
end for
' Row does not exist, insert it into the home view
m.top.content.insertChild(row, getOriginalSectionIndex("smalllibrarytiles"))
setRowItemSize()
end sub
' Create a row displaying latest items in each of the user's libraries
sub createLatestInRows(content as dynamic, sizeArray as dynamic)
' createLatestInRows: Creates a row displaying latest items in each of the user's libraries
'
sub createLatestInRows()
' Ensure we have data
if not isValidAndNotEmpty(m.libraryData) then return
' create a "Latest In" row for each library
filteredLatest = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.LatestItemsExcludes)
for each lib in filteredLatest
for each lib in m.filteredLatest
if lib.collectionType <> "boxsets" and lib.collectionType <> "livetv" and lib.json.CollectionType <> "Program"
latestInRow = content.CreateChild("HomeRow")
latestInRow.title = tr("Latest in") + " " + lib.name + " >"
m.homeSectionIndexes.AddReplace("latestin" + LCase(lib.name).Replace(" ", ""), m.homeSectionIndexes.count)
m.homeSectionIndexes.count++
sizeArray.Push([464, 331])
loadLatest = createObject("roSGNode", "LoadItemsTask")
loadLatest.itemsToLoad = "latest"
loadLatest.itemId = lib.id
@ -204,149 +310,168 @@ sub createLatestInRows(content as dynamic, sizeArray as dynamic)
end for
end sub
' Create a row displaying the live tv now on section
sub createLiveTVRow(content as dynamic, sizeArray as dynamic)
contentRow = content.CreateChild("HomeRow")
contentRow.title = tr("On Now")
m.homeSectionIndexes.AddReplace("livetv", m.homeSectionIndexes.count)
m.homeSectionIndexes.count++
sizeArray.push([464, 331])
' sectionExists: Checks if passed section exists in home row content
'
' @param {string} sectionTitle - Title of section we're checking for
'
' @return {boolean} indicating if the section currently exists in the home row content
function sectionExists(sectionTitle as string) as boolean
if not isValid(sectionTitle) then return false
if not isValid(m.top.content) then return false
searchSectionTitle = LCase(sectionTitle).Replace(" ", "")
homeSections = m.top.content.getChildren(-1, 0)
for each section in homeSections
if LCase(section.title).Replace(" ", "") = searchSectionTitle
return true
end if
end for
return false
end function
' getSectionIndex: Returns index of requested section in home row content
'
' @param {string} sectionTitle - Title of section we're checking for
'
' @return {integer} indicating index of request section
function getSectionIndex(sectionTitle as string) as integer
if not isValid(sectionTitle) then return false
if not isValid(m.top.content) then return false
searchSectionTitle = LCase(sectionTitle).Replace(" ", "")
homeSections = m.top.content.getChildren(-1, 0)
sectionIndex = homeSections.count()
i = 0
for each section in homeSections
if LCase(section.title).Replace(" ", "") = searchSectionTitle
sectionIndex = i
exit for
end if
i++
end for
return sectionIndex
end function
' createLiveTVRow: Creates a row displaying the live tv now on section
'
sub createLiveTVRow()
m.LoadOnNowTask.observeField("content", "updateOnNowItems")
m.LoadOnNowTask.control = "RUN"
end sub
' Create a row displaying items the user can continue watching
sub createContinueWatchingRow(content as dynamic, sizeArray as dynamic)
continueWatchingRow = content.CreateChild("HomeRow")
continueWatchingRow.title = tr("Continue Watching")
m.homeSectionIndexes.AddReplace("resume", m.homeSectionIndexes.count)
m.homeSectionIndexes.count++
sizeArray.push([464, 331])
' createContinueWatchingRow: Creates a row displaying items the user can continue watching
'
sub createContinueWatchingRow()
' Load the Continue Watching Data
m.LoadContinueWatchingTask.observeField("content", "updateContinueWatchingItems")
m.LoadContinueWatchingTask.control = "RUN"
end sub
' Create a row displaying next episodes up to watch
sub createNextUpRow(content as dynamic, sizeArray as dynamic)
nextUpRow = content.CreateChild("HomeRow")
nextUpRow.title = tr("Next Up >")
m.homeSectionIndexes.AddReplace("nextup", m.homeSectionIndexes.count)
m.homeSectionIndexes.count++
sizeArray.push([464, 331])
' createNextUpRow: Creates a row displaying next episodes up to watch
'
sub createNextUpRow()
sectionName = tr("Next Up") + ">"
if not sectionExists(sectionName)
nextUpRow = m.top.content.CreateChild("HomeRow")
nextUpRow.title = sectionName
nextUpRow.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
nextUpRow.cursorSize = homeRowItemSizes.WIDE_POSTER
end if
' Load the Next Up Data
m.LoadNextUpTask.observeField("content", "updateNextUpItems")
m.LoadNextUpTask.control = "RUN"
end sub
' Create a row displaying items from the user's favorites list
sub createFavoritesRow(content as dynamic, sizeArray as dynamic)
favoritesRow = content.CreateChild("HomeRow")
favoritesRow.title = tr("Favorites")
sizeArray.Push([464, 331])
m.homeSectionIndexes.AddReplace("favorites", m.homeSectionIndexes.count)
m.homeSectionIndexes.count++
' createFavoritesRow: Creates a row displaying items from the user's favorites list
'
sub createFavoritesRow()
' Load the Favorites Data
m.LoadFavoritesTask.observeField("content", "updateFavoritesItems")
m.LoadFavoritesTask.control = "RUN"
end sub
' Update home row data
' updateHomeRows: Update function exposed to outside components
'
sub updateHomeRows()
' If resume section exists, reload row's data
if m.homeSectionIndexes.doesExist("resume")
m.LoadContinueWatchingTask.observeField("content", "updateContinueWatchingItems")
m.LoadContinueWatchingTask.control = "RUN"
end if
' If next up section exists, reload row's data
if m.homeSectionIndexes.doesExist("nextup")
m.LoadNextUpTask.observeField("content", "updateNextUpItems")
m.LoadNextUpTask.control = "RUN"
end if
' If favorites section exists, reload row's data
if m.homeSectionIndexes.doesExist("favorites")
m.LoadFavoritesTask.observeField("content", "updateFavoritesItems")
m.LoadFavoritesTask.control = "RUN"
end if
' If live tv's on now section exists, reload row's data
if m.homeSectionIndexes.doesExist("livetv")
m.LoadOnNowTask.observeField("content", "updateOnNowItems")
m.LoadOnNowTask.control = "RUN"
end if
' If latest in library section exists, reload row's data
hasLatestHomeSection = false
for each section in m.homeSectionIndexes
if LCase(Left(section, 6)) = "latest"
hasLatestHomeSection = true
exit for
end if
end for
if hasLatestHomeSection
updateLatestInRows()
end if
' Hide the row counter to prevent flicker. We'll show it once loading timer fires
m.top.showRowCounter = [false]
processUserSections()
end sub
' updateFavoritesItems: Processes LoadFavoritesTask content. Removes, Creates, or Updates favorites row as needed
'
sub updateFavoritesItems()
m.processedRowCount++
itemData = m.LoadFavoritesTask.content
m.LoadFavoritesTask.unobserveField("content")
m.LoadFavoritesTask.content = []
if itemData = invalid then return
sectionName = tr("Favorites")
rowIndex = m.homeSectionIndexes.favorites
if itemData.count() < 1
removeHomeSection("favorites")
return
else
' remake row using the new data
row = CreateObject("roSGNode", "HomeRow")
row.title = tr("Favorites")
for each item in itemData
usePoster = true
if lcase(item.type) = "episode" or lcase(item.type) = "audio" or lcase(item.type) = "musicartist"
usePoster = false
end if
item.usePoster = usePoster
item.imageWidth = row.imageWidth
row.appendChild(item)
end for
' replace the old row
m.top.content.replaceChild(row, rowIndex)
end if
end sub
sub updateContinueWatchingItems()
itemData = m.LoadContinueWatchingTask.content
m.LoadContinueWatchingTask.unobserveField("content")
m.LoadContinueWatchingTask.content = []
if itemData = invalid then return
if itemData.count() < 1
removeHomeSection("resume")
if not isValidAndNotEmpty(itemData)
removeHomeSection(sectionName)
return
end if
' remake row using the new data
row = CreateObject("roSGNode", "HomeRow")
row.title = tr("Continue Watching")
row.title = sectionName
row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
row.cursorSize = homeRowItemSizes.WIDE_POSTER
for each item in itemData
usePoster = true
if lcase(item.type) = "episode" or lcase(item.type) = "audio" or lcase(item.type) = "musicartist"
usePoster = false
end if
item.usePoster = usePoster
item.imageWidth = row.imageWidth
row.appendChild(item)
end for
if sectionExists(sectionName)
m.top.content.replaceChild(row, getSectionIndex(sectionName))
setRowItemSize()
return
end if
m.top.content.insertChild(row, getSectionIndex(sectionName))
setRowItemSize()
end sub
' updateContinueWatchingItems: Processes LoadContinueWatchingTask content. Removes, Creates, or Updates continue watching row as needed
'
sub updateContinueWatchingItems()
m.processedRowCount++
itemData = m.LoadContinueWatchingTask.content
m.LoadContinueWatchingTask.unobserveField("content")
m.LoadContinueWatchingTask.content = []
sectionName = tr("Continue Watching")
if not isValidAndNotEmpty(itemData)
removeHomeSection(sectionName)
return
end if
sectionName = tr("Continue Watching")
' remake row using the new data
row = CreateObject("roSGNode", "HomeRow")
row.title = sectionName
row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
row.cursorSize = homeRowItemSizes.WIDE_POSTER
for each item in itemData
if isValid(item.json) and isValid(item.json.UserData) and isValid(item.json.UserData.PlayedPercentage)
@ -358,198 +483,161 @@ sub updateContinueWatchingItems()
row.appendChild(item)
end for
' replace the old row
m.top.content.replaceChild(row, m.homeSectionIndexes.resume)
' Row already exists, replace it with new content
if sectionExists(sectionName)
m.top.content.replaceChild(row, getSectionIndex(sectionName))
setRowItemSize()
return
end if
' Row does not exist, insert it into the home view
m.top.content.insertChild(row, getOriginalSectionIndex("resume"))
setRowItemSize()
end sub
' updateNextUpItems: Processes LoadNextUpTask content. Removes, Creates, or Updates next up row as needed
'
sub updateNextUpItems()
m.processedRowCount++
itemData = m.LoadNextUpTask.content
m.LoadNextUpTask.unobserveField("content")
m.LoadNextUpTask.content = []
m.LoadNextUpTask.control = "STOP"
if itemData = invalid then return
sectionName = tr("Next Up") + " >"
if itemData.count() < 1
removeHomeSection("nextup")
if not isValidAndNotEmpty(itemData)
removeHomeSection(sectionName)
return
else
' remake row using the new data
row = CreateObject("roSGNode", "HomeRow")
row.title = tr("Next Up") + " >"
for each item in itemData
item.usePoster = row.usePoster
item.imageWidth = row.imageWidth
row.appendChild(item)
end for
' replace the old row
m.top.content.replaceChild(row, m.homeSectionIndexes.nextup)
end if
end sub
' Iterate over user's libraries and update data for each Latest In section
sub updateLatestInRows()
' Ensure we have data
if not isValidAndNotEmpty(m.libraryData) then return
' remake row using the new data
row = CreateObject("roSGNode", "HomeRow")
row.title = tr("Next Up") + " >"
row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
row.cursorSize = homeRowItemSizes.WIDE_POSTER
' Load new data for each library
filteredLatest = filterNodeArray(m.libraryData, "id", m.global.session.user.configuration.LatestItemsExcludes)
for each lib in filteredLatest
if lib.collectionType <> "boxsets" and lib.collectionType <> "livetv" and lib.json.CollectionType <> "Program"
loadLatest = createObject("roSGNode", "LoadItemsTask")
loadLatest.itemsToLoad = "latest"
loadLatest.itemId = lib.id
metadata = {
"title": lib.name,
"contentType": lib.json.CollectionType
}
loadLatest.metadata = metadata
loadLatest.observeField("content", "updateLatestItems")
loadLatest.control = "RUN"
end if
for each item in itemData
item.usePoster = row.usePoster
item.imageWidth = row.imageWidth
row.appendChild(item)
end for
' Row already exists, replace it with new content
if sectionExists(sectionName)
m.top.content.replaceChild(row, getSectionIndex(sectionName))
setRowItemSize()
return
end if
' Row does not exist, insert it into the home view
m.top.content.insertChild(row, getSectionIndex(sectionName))
setRowItemSize()
end sub
' updateLatestItems: Processes LoadItemsTask content. Removes, Creates, or Updates latest in {library} row as needed
'
' @param {dynamic} msg - LoadItemsTask
sub updateLatestItems(msg)
m.processedRowCount++
itemData = msg.GetData()
node = msg.getRoSGNode()
node.unobserveField("content")
node.content = []
if itemData = invalid then return
sectionName = tr("Latest in") + " " + node.metadata.title + " >"
sectionName = "latestin" + LCase(node.metadata.title).Replace(" ", "")
if itemData.count() < 1
if not isValidAndNotEmpty(itemData)
removeHomeSection(sectionName)
return
else
' remake row using new data
row = CreateObject("roSGNode", "HomeRow")
row.title = tr("Latest in") + " " + node.metadata.title + " >"
row.usePoster = true
' Handle specific types with different item widths
if node.metadata.contentType = "movies"
row.imageWidth = 180
itemSize = [188, 331]
else if node.metadata.contentType = "music"
row.imageWidth = 261
itemSize = [261, 331]
else
row.imageWidth = 464
itemSize = [464, 331]
end if
imagesize = homeRowItemSizes.WIDE_POSTER
if isValid(node.metadata.contentType)
if LCase(node.metadata.contentType) = "movies"
imagesize = homeRowItemSizes.MOVIE_POSTER
else if LCase(node.metadata.contentType) = "music"
imagesize = homeRowItemSizes.MUSIC_ALBUM
end if
end if
for each item in itemData
item.usePoster = row.usePoster
item.imageWidth = row.imageWidth
row.appendChild(item)
end for
' remake row using new data
row = CreateObject("roSGNode", "HomeRow")
row.title = sectionName
row.imageWidth = imagesize[0]
row.cursorSize = imagesize
row.usePoster = true
rowIndex = m.homeSectionIndexes[sectionName]
' Replace the old row
if isValid(rowIndex)
updateSizeArray(itemSize, rowIndex, "replace")
m.top.content.replaceChild(row, rowIndex)
return
end if
' Determine highest index of a Lastest In section so we can append the new section after it
highestLatestHomeSectionIndex = 0
for each section in m.homeSectionIndexes
if LCase(Left(section, 6)) = "latest"
if m.homeSectionIndexes[section] > highestLatestHomeSectionIndex
highestLatestHomeSectionIndex = m.homeSectionIndexes[section]
end if
end if
end for
' We have data for a section that doesn't currently exist
rowIndex = highestLatestHomeSectionIndex + 1
' Advance all the indexes greater than or equal than our new row
for each section in m.homeSectionIndexes
if m.homeSectionIndexes[section] >= rowIndex
m.homeSectionIndexes[section]++
end if
end for
m.homeSectionIndexes.AddReplace(sectionName, rowIndex)
m.top.content.insertChild(row, rowIndex)
updateSizeArray(itemSize, rowIndex)
for each item in itemData
item.usePoster = row.usePoster
item.imageWidth = row.imageWidth
row.appendChild(item)
end for
if sectionExists(sectionName)
' Row already exists, replace it with new content
m.top.content.replaceChild(row, getSectionIndex(sectionName))
setRowItemSize()
return
end if
m.top.content.insertChild(row, getOriginalSectionIndex("latestmedia"))
setRowItemSize()
end sub
' updateOnNowItems: Processes LoadOnNowTask content. Removes, Creates, or Updates latest in on now row as needed
'
sub updateOnNowItems()
m.processedRowCount++
itemData = m.LoadOnNowTask.content
m.LoadOnNowTask.unobserveField("content")
m.LoadOnNowTask.content = []
if itemData = invalid then return
sectionName = tr("On Now")
if itemData.count() < 1
removeHomeSection("livetv")
if not isValidAndNotEmpty(itemData)
removeHomeSection(sectionName)
return
else
' remake row using the new data
row = CreateObject("roSGNode", "HomeRow")
row.title = tr("On Now")
itemSize = [464, 331]
row.imageWidth = 464
for each item in itemData
row.usePoster = false
if (not isValid(item.thumbnailURL) or item.thumbnailURL = "") and isValid(item.json) and isValid(item.json.imageURL)
item.thumbnailURL = item.json.imageURL
row.usePoster = true
row.imageWidth = 180
itemSize = [188, 331]
end if
item.usePoster = row.usePoster
item.imageWidth = row.imageWidth
row.appendChild(item)
end for
' replace the old row
updateSizeArray(itemSize, m.homeSectionIndexes.livetv, "replace")
m.top.content.replaceChild(row, m.homeSectionIndexes.livetv)
end if
end sub
sub updateSizeArray(rowItemSize, rowIndex = invalid, action = "insert")
sizeArray = m.top.rowItemSize
' append by default
if rowIndex = invalid
rowIndex = sizeArray.count()
end if
newSizeArray = []
for i = 0 to sizeArray.count()
if rowIndex = i
if action = "replace"
newSizeArray.Push(rowItemSize)
else if action = "insert"
newSizeArray.Push(rowItemSize)
if isValid(sizeArray[i])
newSizeArray.Push(sizeArray[i])
end if
end if
else if isValid(sizeArray[i])
newSizeArray.Push(sizeArray[i])
' remake row using the new data
row = CreateObject("roSGNode", "HomeRow")
row.title = tr("On Now")
row.imageWidth = homeRowItemSizes.WIDE_POSTER[0]
row.cursorSize = homeRowItemSizes.WIDE_POSTER
for each item in itemData
row.usePoster = false
if (not isValid(item.thumbnailURL) or item.thumbnailURL = "") and isValid(item.json) and isValid(item.json.imageURL)
item.thumbnailURL = item.json.imageURL
row.usePoster = true
row.imageWidth = homeRowItemSizes.MOVIE_POSTER[0]
row.cursorSize = homeRowItemSizes.MOVIE_POSTER
end if
item.usePoster = row.usePoster
item.imageWidth = row.imageWidth
row.appendChild(item)
end for
m.top.rowItemSize = newSizeArray
' Row already exists, replace it with new content
if sectionExists(sectionName)
m.top.content.replaceChild(row, getSectionIndex(sectionName))
setRowItemSize()
return
end if
' Row does not exist, insert it into the home view
m.top.content.insertChild(row, getOriginalSectionIndex("livetv"))
setRowItemSize()
end sub
sub itemSelected()
m.selectedRowItem = m.top.rowItemSelected
m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1])
'Prevent the selected item event from double firing

View File

@ -1,3 +1,5 @@
import "pkg:/source/utils/misc.bs"
sub init()
m.EPGLaunchCompleteSignaled = false
m.scheduleGrid = m.top.findNode("scheduleGrid")
@ -26,8 +28,6 @@ sub init()
m.top.lastFocus = m.scheduleGrid
m.channelIndex = {}
m.spinner = m.top.findNode("spinner")
end sub
sub channelFilterSet()
@ -48,14 +48,14 @@ sub channelsearchTermSet()
if LCase(m.top.searchTerm) = LCase(tr("all")) or m.LoadChannelsTask.searchTerm = LCase(tr("all"))
m.top.searchTerm = " "
m.LoadChannelsTask.searchTerm = " "
m.spinner.visible = true
startLoadingSpinner()
m.LoadChannelsTask.control = "RUN"
'filter if the searterm is not invalid
else if m.top.searchTerm <> invalid and LCase(m.LoadChannelsTask.searchTerm) <> LCase(m.top.searchTerm)
if m.LoadChannelsTask.state = "run" then m.LoadChannelsTask.control = "stop"
m.LoadChannelsTask.searchTerm = m.top.searchTerm
m.spinner.visible = true
startLoadingSpinner()
m.LoadChannelsTask.control = "RUN"
end if
@ -125,7 +125,7 @@ sub onScheduleLoaded()
m.scheduleGrid.showLoadingDataFeedback = false
m.scheduleGrid.setFocus(true)
m.LoadScheduleTask.schedule = []
m.spinner.visible = false
stopLoadingSpinner()
end sub
sub onProgramFocused()

View File

@ -12,7 +12,6 @@
programTitleFocusedColor="#ffffff" iconFocusedColor="#ff0000"
showPastTimeScreen="true" pastTimeScreenBlendColor="#555555"
/>
<Spinner id="spinner" translation="[900, 450]" />
<Animation id="gridMoveAnimation" duration="1" repeat="false" easeFunction="outQuad">
<Vector2DFieldInterpolator id="gridMoveAnimationPosition" key="[0.0, 0.5]" fieldToInterp="scheduleGrid.translation" />
</Animation>

View File

@ -1,8 +1,11 @@
import "pkg:/source/utils/misc.bs"
sub init()
m.top.optionsAvailable = false
end sub
sub itemContentChanged()
stopLoadingSpinner()
m.top.findNode("UserRow").ItemContent = m.top.itemContent
redraw()
end sub

View File

@ -161,19 +161,20 @@ sub processSubtitleSelection()
end if
if m.selectedSubtitle.IsEncoded
' Roku can not natively display these subtitles, so turn off the caption mode on the device
m.view.globalCaptionMode = "Off"
else
' Roku can natively display these subtitles, ensure the caption mode on the device is on
m.view.globalCaptionMode = "On"
end if
if m.selectedSubtitle.IsExternal
' Roku may rearrange subtitle tracks. Look up track based on name to ensure we get the correct index
availableSubtitleTrackIndex = availSubtitleTrackIdx(m.selectedSubtitle.Track.TrackName)
if availableSubtitleTrackIndex = -1 then return
m.view.subtitleTrack = m.view.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName
else
m.view.selectedSubtitle = m.selectedSubtitle.Index
end if
m.view.selectedSubtitle = m.selectedSubtitle.Index
end sub
' User requested playback info

View File

@ -21,8 +21,6 @@ sub init()
m.buttonGrp.setFocus(true)
m.top.lastFocus = m.buttonGrp
m.spinner = m.top.findNode("spinner")
m.top.observeField("itemContent", "itemContentChanged")
end sub
@ -150,7 +148,7 @@ sub itemContentChanged()
SetUpVideoOptions(itemData.mediaSources)
SetUpAudioOptions(itemData.mediaStreams)
m.buttonGrp.visible = true
m.spinner.visible = false
stopLoadingSpinner()
end sub

View File

@ -40,7 +40,6 @@
</LayoutGroup>
</LayoutGroup>
<MovieOptions id="movieOptions" visible="false" />
<Spinner id="spinner" translation="[900, 450]" visible="true" />
<!-- "Cast and Crew" row -->
<extrasSlider id="movieExtras" />

View File

@ -12,8 +12,6 @@ sub init()
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()
@ -99,8 +97,6 @@ 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()
@ -169,9 +165,5 @@ end sub
sub onDoneLoading()
m.songList.unobservefield("doneLoading")
m.spinner.visible = false
end sub
sub OnScreenHidden()
m.spinner.visible = false
stopLoadingSpinner()
end sub

View File

@ -20,7 +20,6 @@
</LayoutGroup>
</LayoutGroup>
</LayoutGroup>
<Spinner id="spinner" translation="[920, 540]" visible="false" />
</children>
<interface>
<field id="pageContent" type="node" onChange="pageContentChanged" />

View File

@ -76,6 +76,7 @@ sub OnScreenShown()
m.overhang.isVisible = false
m.overhang.opacity = "1"
end if
stopLoadingSpinner()
end sub
sub OnScreenHidden()

View File

@ -447,6 +447,7 @@ sub loadButtons()
end sub
sub onAudioStreamLoaded()
stopLoadingSpinner()
data = m.LoadAudioStreamTask.content[0]
m.LoadAudioStreamTask.unobserveField("content")
if data <> invalid and data.count() > 0

View File

@ -11,8 +11,6 @@ sub init()
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()
@ -98,8 +96,6 @@ 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()
@ -160,9 +156,5 @@ end sub
sub onDoneLoading()
m.songList.unobservefield("doneLoading")
m.spinner.visible = false
end sub
sub OnScreenHidden()
m.spinner.visible = false
stopLoadingSpinner()
end sub

View File

@ -19,7 +19,6 @@
</LayoutGroup>
</LayoutGroup>
</LayoutGroup>
<Spinner id="spinner" translation="[920, 540]" visible="false" />
</children>
<interface>
<field id="pageContent" type="node" onChange="pageContentChanged" />

View File

@ -41,6 +41,7 @@ sub itemContentChanged()
end sub
sub onPhotoLoaded()
stopLoadingSpinner()
if m.LoadLibrariesTask.results <> invalid
photo = m.top.findNode("photo")
photo.uri = m.LoadLibrariesTask.results

View File

@ -6,7 +6,6 @@ import "pkg:/source/utils/deviceCapabilities.bs"
sub init()
m.top.optionsAvailable = false
m.searchSpinner = m.top.findnode("searchSpinner")
m.searchSelect = m.top.findnode("searchSelect")
m.searchTask = CreateObject("roSGNode", "SearchTask")
@ -20,12 +19,12 @@ sub searchMedias()
query = m.top.searchAlpha
'if user deletes the search string hide the spinner
if query.len() = 0
m.searchSpinner.visible = false
stopLoadingSpinner()
end if
'if search task is running and user selectes another letter stop the search and load the next letter
m.searchTask.control = "stop"
if query <> invalid and query <> ""
m.searchSpinner.visible = true
startLoadingSpinner(false)
end if
m.searchTask.observeField("results", "loadResults")
m.searchTask.query = query
@ -37,7 +36,7 @@ end sub
sub loadResults()
m.searchTask.unobserveField("results")
m.searchSpinner.visible = false
stopLoadingSpinner()
m.searchSelect.itemdata = m.searchTask.results
m.searchSelect.query = m.top.SearchAlpha
m.searchHelpText.visible = false

View File

@ -10,7 +10,6 @@
<SearchRow id="searchSelect" visible="true" focusable="true" />
</LayoutGroup>
<OptionsSlider id="options" />
<Spinner id="searchSpinner" visible="false" translation="[1050, 500]" />
</children>
<interface>
<field id="query" type="string" alwaysNotify="true" />

View File

@ -93,6 +93,8 @@ function onKeyEvent(key as string, press as boolean) as boolean
focusedItem = getFocusedItem()
if isValid(focusedItem)
m.top.selectedItem = focusedItem
'Prevent the selected item event from double firing
m.top.selectedItem = invalid
end if
return true
end if

View File

@ -268,6 +268,9 @@ end sub
' Event handler for when selectedSubtitle changes
sub onSubtitleChange()
' If the global caption mode is on, that means Roku can display the subtitles natively and doesn't need a video stop/start
if LCase(m.top.globalCaptionMode) = "on" then return
' Save the current video position
m.global.queueManager.callFunc("setTopStartingPoint", int(m.top.position) * 10000000&)
@ -327,6 +330,8 @@ sub onVideoContentLoaded()
videoContent = m.LoadMetaDataTask.content
m.LoadMetaDataTask.content = []
stopLoadingSpinner()
' If we have nothing to play, return to previous screen
if not isValid(videoContent)
showPlaybackErrorDialog(tr("There was an error retrieving the data for this item from the server."))
@ -361,6 +366,29 @@ sub onVideoContentLoaded()
m.top.allowCaptions = true
end if
' Allow default subtitles
m.top.unobserveField("selectedSubtitle")
' Set subtitleTrack property is subs are natively supported by Roku
selectedSubtitle = invalid
for each subtitle in m.top.fullSubtitleData
if subtitle.Index = videoContent[0].selectedSubtitle
selectedSubtitle = subtitle
exit for
end if
end for
if isValid(selectedSubtitle)
availableSubtitleTrackIndex = availSubtitleTrackIdx(selectedSubtitle.Track.TrackName)
if availableSubtitleTrackIndex <> -1
m.top.subtitleTrack = m.top.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName
end if
end if
m.top.selectedSubtitle = videoContent[0].selectedSubtitle
m.top.observeField("selectedSubtitle", "onSubtitleChange")
if isValid(m.top.audioIndex)
m.top.audioTrack = (m.top.audioIndex + 1).toStr()
else
@ -608,6 +636,25 @@ function stateAllowsOSD() as boolean
return inArray(validStates, m.top.state)
end function
' availSubtitleTrackIdx: Returns Roku's index for requested subtitle track
'
' @param {string} tracknameToFind - TrackName for subtitle we're looking to match
' @return {integer} indicating Roku's index for requested subtitle track. Returns -1 if not found
function availSubtitleTrackIdx(tracknameToFind as string) as integer
idx = 0
for each availTrack in m.top.availableSubtitleTracks
' The TrackName must contain the URL we supplied originally, though
' Roku mangles the name a bit, so we check if the URL is a substring, rather
' than strict equality
if Instr(1, availTrack.TrackName, tracknameToFind)
return idx
end if
idx = idx + 1
end for
return -1
end function
function onKeyEvent(key as string, press as boolean) as boolean
' Keypress handler while user is inside the chapter menu

View File

@ -7,7 +7,7 @@
<field id="selectPlaybackInfoPressed" type="boolean" alwaysNotify="true" />
<field id="PlaySessionId" type="string" />
<field id="Subtitles" type="array" />
<field id="SelectedSubtitle" type="integer" value="-1" alwaysNotify="true" />
<field id="SelectedSubtitle" type="integer" value="-2" alwaysNotify="true" />
<field id="container" type="string" />
<field id="directPlaySupported" type="boolean" />
<field id="systemOverlay" type="boolean" value="false" />

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More