mirror of
https://github.com/jellyfin/jellyfin-roku.git
synced 2024-11-23 22:29:43 +00:00
Merge remote-tracking branch 'upstream/unstable' into update-build-workflow
This commit is contained in:
commit
7366b48a2a
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -7,33 +7,34 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Software Versions**
|
||||
Jellyfin Server Version:
|
||||
Roku Client Version:
|
||||
## Software Versions
|
||||
|
||||
**Describe the bug**
|
||||
- Jellyfin Server Version:
|
||||
- Roku Client Version:
|
||||
|
||||
## Describe the bug
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**How To Reproduce**
|
||||
## How To Reproduce
|
||||
<!-- Steps to reproduce the behavior: -->
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. Bug occurs
|
||||
|
||||
**Expected behavior**
|
||||
## Expected behavior
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Logs**
|
||||
## Logs
|
||||
<!-- Please paste any log errors. -->
|
||||
|
||||
**Screenshots**
|
||||
## Screenshots
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Connection Information**
|
||||
Is server local or remote?
|
||||
## Connection Information
|
||||
|
||||
Is server connection http or https?
|
||||
- Is server local or remote?
|
||||
- Is server connection HTTP or HTTPS?
|
||||
|
||||
**Additional context**
|
||||
## Additional context
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
10
.github/ISSUE_TEMPLATE/enhancement-request.md
vendored
10
.github/ISSUE_TEMPLATE/enhancement-request.md
vendored
@ -1,20 +1,20 @@
|
||||
---
|
||||
name: Enhancement request
|
||||
about: Suggest an modification to an existing feature
|
||||
about: Suggest a modification to an existing feature
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
## Is your feature request related to a problem? Please describe
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
## Describe the solution you'd like
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
## Describe alternatives you've considered
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
## Additional context
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
|
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -7,8 +7,8 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the feature you'd like**
|
||||
## Describe the feature you'd like
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Additional context**
|
||||
## Additional context
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
|
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@ -2,10 +2,10 @@
|
||||
Ensure your title is short, descriptive, and in the imperative mood (Fix X, Change Y, instead of Fixed X, Changed Y).
|
||||
For a good inspiration of what to write in commit messages and PRs please review https://chris.beams.io/posts/git-commit/ and our https://jellyfin.readthedocs.io/en/latest/developer-docs/contributing/ page.
|
||||
-->
|
||||
|
||||
**Changes**
|
||||
<!-- markdownlint-disable MD041 first-line-heading -->
|
||||
## Changes
|
||||
<!-- Describe your changes here in 1-5 sentences. -->
|
||||
|
||||
**Issues**
|
||||
## Issues
|
||||
<!-- Tag any issues that this PR solves here.
|
||||
ex. Fixes # -->
|
||||
|
2
.github/workflows/build-dev.yml
vendored
2
.github/workflows/build-dev.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
dev:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
- uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
|
||||
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
|
28
.github/workflows/lint.yml
vendored
28
.github/workflows/lint.yml
vendored
@ -26,9 +26,14 @@ jobs:
|
||||
with:
|
||||
packages: libxml2-utils xmlstarlet
|
||||
- name: Validate XML syntax
|
||||
run: xmllint --noout ./locale/*/*.ts
|
||||
- name: Check XML for duplicate entries
|
||||
run: xmlstarlet sel -t -m '/TS/context/message/source' -o 'Duplicate entry found in:' -f -o ' ' -c '.' -nl ./locale/*/*.ts | sort | uniq -d
|
||||
run: xmllint --noout ./locale/en_US/translations.ts
|
||||
- name: Save output of duplicate check
|
||||
run: echo "tsDuplicates=$(xmlstarlet sel -t -m '/TS/context/message/source' -c '.' -nl ./locale/en_US/translations.ts | sort | uniq -d | awk '{ printf "%s", $0 }')" >> $GITHUB_ENV
|
||||
- name: Check for duplicates
|
||||
run: xmlstarlet sel -t -m '/TS/context/message/source' -f -o ' ' -c '.' -nl ./locale/en_US/translations.ts | sort | uniq -d
|
||||
- name: Duplicates found
|
||||
if: env.tsDuplicates != ''
|
||||
run: exit 1
|
||||
json:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -59,4 +64,19 @@ jobs:
|
||||
run: npx ropm install
|
||||
- uses: xt0rted/markdownlint-problem-matcher@98d94724052d20ca2e06c091f202e4c66c3c59fb # v2
|
||||
- name: Lint markdown files
|
||||
run: npm run lint-markdown
|
||||
run: npm run lint-markdown
|
||||
spelling:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone github repo
|
||||
uses: actions/checkout@master
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
- name: Install roku package dependencies
|
||||
run: npx ropm install
|
||||
- name: Check markdown files for spelling errors
|
||||
run: npm run lint-spelling
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"ignore": [
|
||||
"node_modules/**/*"
|
||||
]
|
||||
}
|
17
.spelling
17
.spelling
@ -1,17 +0,0 @@
|
||||
jellyfin
|
||||
brightscript
|
||||
vscode
|
||||
roku
|
||||
github
|
||||
pre-release
|
||||
sideload
|
||||
dev
|
||||
repo
|
||||
hardcode
|
||||
hardcoding
|
||||
breakpoint
|
||||
DEVGUIDE
|
||||
runtime
|
||||
translations.ts
|
||||
en_US
|
||||
ing
|
12
DEVGUIDE.md
12
DEVGUIDE.md
@ -60,16 +60,16 @@ We recommend using Visual Studio Code when working on this project. The [BrightS
|
||||
|
||||
### Usage
|
||||
|
||||
1. Open the `jellyfin-roku` folder in vscode
|
||||
2. Press `F5` on your keyboard or click `Run` -> `Start Debugging` from the vscode menu. ![image](https://user-images.githubusercontent.com/2544493/170696233-8ba49bf4-bebb-4655-88f3-ac45150dda02.png)
|
||||
1. Open the `jellyfin-roku` folder in VSCode
|
||||
2. Press `F5` on your keyboard or click `Run` -> `Start Debugging` from the VSCode menu. ![image](https://user-images.githubusercontent.com/2544493/170696233-8ba49bf4-bebb-4655-88f3-ac45150dda02.png)
|
||||
|
||||
3. Enter your Roku IP address and developer password when prompted
|
||||
|
||||
That's it! vscode will auto-package the project, sideload it to the specified device, and the channel is up and running. (assuming you remembered to put your device in [developer mode](#developer-mode))
|
||||
That's it! VSCode will auto-package the project, sideload it to the specified device, and the channel is up and running. (assuming you remembered to put your device in [developer mode](#developer-mode))
|
||||
|
||||
### Hardcoding Roku Information
|
||||
|
||||
Out of the box, the Brightscript extension will prompt you to pick a Roku device (from devices found on your local network) and enter a password on every launch. If you'd prefer to hardcode this information rather than entering it every time, you can set these values in your vscode user settings:
|
||||
Out of the box, the BrightScript extension will prompt you to pick a Roku device (from devices found on your local network) and enter a password on every launch. If you'd prefer to hardcode this information rather than entering it every time, you can set these values in your VSCode user settings:
|
||||
|
||||
```js
|
||||
{
|
||||
@ -95,7 +95,7 @@ Build the package
|
||||
make dev
|
||||
```
|
||||
|
||||
This will create a zip in `out/apps/Jellyfin_Roku-dev.zip`. Login to your roku's device in your browser and upload the zip file then run install.
|
||||
This will create a zip in `out/apps/Jellyfin_Roku-dev.zip`. Login to your Roku's device in your browser and upload the zip file then run install.
|
||||
|
||||
## Method 3: Direct load to Roku Device
|
||||
|
||||
@ -154,7 +154,7 @@ make install
|
||||
|
||||
Modify code -> `make install` -> Use Roku remote to test changes -> `telnet ${ROKU_DEV_TARGET} 8085` -> `CTRL + ]` -> `quit + ENTER`
|
||||
|
||||
Unfortunately there is no debugger. You will need to use telnet to see log statements, warnings, and error reports. You won't always need to telnet into your device but the workflow above is typical when you are new to Brightscript or are working on tricky code.
|
||||
Unfortunately there is no debugger. You will need to use telnet to see log statements, warnings, and error reports. You won't always need to telnet into your device but the workflow above is typical when you are new to BrightScript or are working on tricky code.
|
||||
|
||||
Install necessary packages:
|
||||
|
||||
|
@ -30,7 +30,7 @@ The channel is available on the [Roku Channel Store](https://my.roku.com/add/jel
|
||||
|
||||
## Getting Involved<a name="get_involved"></a>
|
||||
|
||||
No matter what your interests or skill are, you can help to make this client better for everyone by simply using the client and letting us know if you find a problem with it. Either give us a shout on [matrix](https://matrix.to/#/+jellyfin:matrix.org) or create a github issue.
|
||||
No matter what your interests or skill are, you can help to make this client better for everyone by simply using the client and letting us know if you find a problem with it. Either give us a shout on [matrix](https://matrix.to/#/+jellyfin:matrix.org) or create a GitHub issue.
|
||||
|
||||
Feature requests are always welcome too, but please have a read though the existing issues to see if someone has already raised one for something similar.
|
||||
|
||||
|
@ -22,7 +22,7 @@ sub init()
|
||||
'Parent is MarkupGrid and it's parent is the ItemGrid
|
||||
m.topParent = m.top.GetParent().GetParent()
|
||||
'Get the imageDisplayMode for these grid items
|
||||
if m.topParent.imageDisplayMode <> invalid
|
||||
if isValid(m.topParent.imageDisplayMode)
|
||||
m.itemPoster.loadDisplayMode = m.topParent.imageDisplayMode
|
||||
end if
|
||||
|
||||
@ -44,7 +44,7 @@ sub itemContentChanged()
|
||||
m.itemText.text = itemData.Title
|
||||
else if itemData.type = "Series"
|
||||
if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
|
||||
if itemData?.json?.UserData?.UnplayedItemCount <> invalid
|
||||
if isValid(itemData.json) and isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
|
||||
if itemData.json.UserData.UnplayedItemCount > 0
|
||||
m.unplayedCount.visible = true
|
||||
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
|
||||
@ -98,7 +98,7 @@ sub itemContentChanged()
|
||||
|
||||
m.posterText.height = 200
|
||||
m.posterText.width = 280
|
||||
else if itemData.json.type = "MusicAlbum"
|
||||
else if isValid(itemData.json.type) and itemData.json.type = "MusicAlbum"
|
||||
m.itemPoster.uri = itemData.PosterUrl
|
||||
m.itemText.text = itemData.Title
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="GridItem" extends="Group">
|
||||
<children>
|
||||
<maskGroup id="posterMask" maskUri="pkg:/images/postermask.png" scaleRotateCenter="[145, 212.5]" scale="[0.85,0.85]">
|
||||
@ -20,4 +20,5 @@
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="GridItem.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
</component>
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
</component>
|
@ -16,6 +16,11 @@ sub init()
|
||||
m.menus.push(m.top.findNode("sortMenu"))
|
||||
m.menus.push(m.top.findNode("filterMenu"))
|
||||
|
||||
m.filterOptions = m.top.findNode("filterOptions")
|
||||
|
||||
m.filterMenu = m.top.findNode("filterMenu")
|
||||
m.filterMenu.observeField("itemFocused", "onFilterFocusChange")
|
||||
|
||||
m.viewNames = []
|
||||
m.sortNames = []
|
||||
m.filterNames = []
|
||||
@ -24,14 +29,46 @@ sub init()
|
||||
m.fadeAnim = m.top.findNode("fadeAnim")
|
||||
m.fadeOutAnimOpacity = m.top.findNode("outOpacity")
|
||||
m.fadeInAnimOpacity = m.top.findNode("inOpacity")
|
||||
m.showChecklistAnimation = m.top.findNode("showChecklistAnimation")
|
||||
m.hideChecklistAnimation = m.top.findNode("hideChecklistAnimation")
|
||||
|
||||
m.buttons.observeField("focusedIndex", "buttonFocusChanged")
|
||||
m.favoriteMenu.observeField("buttonSelected", "toggleFavorite")
|
||||
end sub
|
||||
|
||||
sub showChecklist()
|
||||
if m.filterOptions.opacity = 0
|
||||
if m.showChecklistAnimation.state = "stopped"
|
||||
m.showChecklistAnimation.control = "start"
|
||||
end if
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub hideChecklist()
|
||||
if m.filterOptions.opacity = 1
|
||||
if m.hideChecklistAnimation.state = "stopped"
|
||||
m.hideChecklistAnimation.control = "start"
|
||||
end if
|
||||
end if
|
||||
end sub
|
||||
|
||||
sub onFilterFocusChange()
|
||||
if m.filterMenu.content.getChild(m.filterMenu.itemFocused).getChildCount() > 0
|
||||
showChecklist()
|
||||
else
|
||||
hideChecklist()
|
||||
end if
|
||||
|
||||
m.filterOptions.content = m.filterMenu.content.getChild(m.filterMenu.itemFocused)
|
||||
if isValid(m.filterMenu.content.getChild(m.filterMenu.itemFocused).checkedState)
|
||||
m.filterOptions.checkedState = m.filterMenu.content.getChild(m.filterMenu.itemFocused).checkedState
|
||||
else
|
||||
m.filterOptions.checkedState = []
|
||||
end if
|
||||
end sub
|
||||
|
||||
|
||||
sub optionsSet()
|
||||
|
||||
' Views Tab
|
||||
if m.top.options.views <> invalid
|
||||
viewContent = CreateObject("roSGNode", "ContentNode")
|
||||
@ -90,8 +127,19 @@ sub optionsSet()
|
||||
m.selectedFilterIndex = 0
|
||||
|
||||
for each filterItem in m.top.options.filter
|
||||
entry = filterContent.CreateChild("ContentNode")
|
||||
entry = filterContent.CreateChild("OptionNode")
|
||||
entry.title = filterItem.Title
|
||||
entry.name = filterItem.Name
|
||||
entry.delimiter = filterItem.Delimiter
|
||||
|
||||
if isValid(filterItem.options)
|
||||
for each filterItemOption in filterItem.options
|
||||
entryOption = entry.CreateChild("ContentNode")
|
||||
entryOption.title = toString(filterItemOption)
|
||||
end for
|
||||
entry.checkedState = filterItem.checkedState
|
||||
end if
|
||||
|
||||
m.filterNames.push(filterItem.Name)
|
||||
if filterItem.selected <> invalid and filterItem.selected = true
|
||||
m.selectedFilterIndex = index
|
||||
@ -193,12 +241,31 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||
end if
|
||||
|
||||
return true
|
||||
else if key = "right"
|
||||
if m.menus[m.selectedItem].isInFocusChain()
|
||||
' Handle Filter screen
|
||||
if m.selectedItem = 2
|
||||
' Selected filter has options, move cursor to it
|
||||
if m.filterMenu.content.getChild(m.filterMenu.itemFocused).getChildCount() > 0
|
||||
m.menus[m.selectedItem].setFocus(false)
|
||||
m.filterOptions.setFocus(true)
|
||||
return true
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
else if key = "left"
|
||||
if m.favoriteMenu.hasFocus()
|
||||
m.favoriteMenu.setFocus(false)
|
||||
m.menus[m.selectedItem].visible = true
|
||||
m.buttons.setFocus(true)
|
||||
end if
|
||||
|
||||
' User wants to escape filter options
|
||||
if m.filterOptions.isInFocusChain()
|
||||
m.filterOptions.setFocus(false)
|
||||
m.menus[m.selectedItem].setFocus(true)
|
||||
return true
|
||||
end if
|
||||
else if key = "OK"
|
||||
if m.menus[m.selectedItem].isInFocusChain()
|
||||
' Handle View Screen
|
||||
@ -229,14 +296,58 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
|
||||
' Handle Filter screen
|
||||
if m.selectedItem = 2
|
||||
m.selectedFilterIndex = m.menus[2].itemSelected
|
||||
m.top.filter = m.filterNames[m.selectedFilterIndex]
|
||||
' If filter has no options, select it
|
||||
if m.filterMenu.content.getChild(m.filterMenu.itemFocused).getChildCount() = 0
|
||||
m.menus[2].checkedItem = m.menus[2].itemSelected
|
||||
m.selectedFilterIndex = m.menus[2].itemSelected
|
||||
m.top.filter = m.filterNames[m.selectedFilterIndex]
|
||||
m.top.filterOptions = {}
|
||||
return true
|
||||
end if
|
||||
|
||||
' Selected filter has options, move cursor to it
|
||||
m.filterOptions.setFocus(true)
|
||||
m.menus[m.selectedItem].setFocus(false)
|
||||
return true
|
||||
end if
|
||||
end if
|
||||
|
||||
' User pressed OK from inside the filter's options
|
||||
if m.filterOptions.isInFocusChain()
|
||||
selectedOptions = []
|
||||
for i = 0 to m.filterOptions.checkedState.count() - 1
|
||||
if m.filterOptions.checkedState[i]
|
||||
selectedValue = toString(m.filterOptions.content.getChild(i).title)
|
||||
selectedOptions.push(selectedValue)
|
||||
end if
|
||||
end for
|
||||
|
||||
if selectedOptions.Count() > 0
|
||||
m.menus[2].checkedItem = m.menus[2].itemFocused
|
||||
m.selectedFilterIndex = m.menus[2].itemFocused
|
||||
m.top.filter = m.filterMenu.content.getChild(m.filterMenu.itemFocused).Name
|
||||
|
||||
newFilter = {}
|
||||
newFilter[m.top.filter] = selectedOptions.join(m.filterMenu.content.getChild(m.filterMenu.itemFocused).delimiter)
|
||||
m.top.filterOptions = newFilter
|
||||
else
|
||||
m.menus[2].checkedItem = 0
|
||||
m.selectedFilterIndex = 0
|
||||
m.top.filter = m.filterNames[0]
|
||||
m.top.filterOptions = {}
|
||||
end if
|
||||
|
||||
m.filterMenu.content.getChild(m.filterMenu.itemFocused).checkedState = m.filterOptions.checkedState
|
||||
|
||||
return true
|
||||
end if
|
||||
return true
|
||||
else if key = "back" or key = "up"
|
||||
if key = "back" then hideChecklist()
|
||||
|
||||
m.menus[2].visible = true ' Show Filter contents in case hidden by favorite button
|
||||
if m.menus[m.selectedItem].isInFocusChain()
|
||||
m.buttons.setFocus(true)
|
||||
@ -244,6 +355,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||
return true
|
||||
end if
|
||||
else if key = "options"
|
||||
hideChecklist()
|
||||
m.menus[2].visible = true ' Show Filter contents in case hidden by favorite button
|
||||
m.menus[m.selectedItem].drawFocusFeedback = false
|
||||
return false
|
||||
|
@ -6,15 +6,18 @@
|
||||
<Poster width="1720" height="880" uri="pkg:/images/dialog.9.png" />
|
||||
<LayoutGroup horizAlignment="center" translation="[860,50]" itemSpacings="[50]">
|
||||
<JFButtons id="buttons" />
|
||||
</LayoutGroup>
|
||||
<LayoutGroup id="menuOptions" horizAlignment="center" translation="[860,200]" itemSpacings="[50]">
|
||||
<Group>
|
||||
<RadiobuttonList id="viewMenu" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="0" drawFocusFeedback="false">
|
||||
</RadiobuttonList>
|
||||
<RadiobuttonList id="sortMenu" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="1" numRows="8" drawFocusFeedback="false">
|
||||
</RadiobuttonList>
|
||||
<RadiobuttonList id="filterMenu" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="0" drawFocusFeedback="false">
|
||||
<RadiobuttonList id="filterMenu" checkOnSelect="false" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="0" drawFocusFeedback="false">
|
||||
</RadiobuttonList>
|
||||
</Group>
|
||||
</LayoutGroup>
|
||||
<CheckList opacity="0" translation="[900, 200]" id="filterOptions" numRows="8" itemSize="[250, 70]" />
|
||||
<ButtonGroup translation="[1250,50]">
|
||||
<Button id="favoriteMenu" iconUri="pkg:/images/icons/favorite.png" focusedIconUri="pkg:/images/icons/favorite.png" focusBitmapUri="" focusFootprintBitmapUri="" text="Favorite" showFocusFootprint="false"></Button>
|
||||
</ButtonGroup>
|
||||
@ -25,6 +28,16 @@
|
||||
<FloatFieldInterpolator id="inOpacity" key="[0.0, 0.5, 1.0]" keyValue="[ 0, 0, 1 ]" fieldToInterp="focus.opacity" />
|
||||
</Animation>
|
||||
|
||||
<Animation id="showChecklistAnimation" duration="0.5" repeat="false">
|
||||
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0, 1]" fieldToInterp="filterOptions.opacity" />
|
||||
<Vector2DFieldInterpolator key="[0.0, 1.0]" keyValue="[[860, 200], [560, 200]]" fieldToInterp="menuOptions.translation" />
|
||||
</Animation>
|
||||
|
||||
<Animation id="hideChecklistAnimation" duration="0.5" repeat="false">
|
||||
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[1, 0]" fieldToInterp="filterOptions.opacity" />
|
||||
<Vector2DFieldInterpolator key="[0.0, 1.0]" keyValue="[[560, 200], [860, 200]]" fieldToInterp="menuOptions.translation" />
|
||||
</Animation>
|
||||
|
||||
</children>
|
||||
<interface>
|
||||
<field id="buttons" type="nodearray" />
|
||||
@ -35,8 +48,10 @@
|
||||
<field id="sortField" type="string" value="SortName" />
|
||||
<field id="sortAscending" type="boolean" value="false" />
|
||||
<field id="filter" type="string" value="All" />
|
||||
<field id="filterOptions" type="assocarray" value="" />
|
||||
<field id="favorite" type="string" value="Favorite" />
|
||||
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="ItemGridOptions.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
</component>
|
||||
|
@ -83,6 +83,12 @@ sub loadItems()
|
||||
params.append({ Filters: "IsResumable" })
|
||||
end if
|
||||
|
||||
if isValid(m.top.filterOptions)
|
||||
if m.top.filterOptions.count() > 0
|
||||
params.append(m.top.filterOptions)
|
||||
end if
|
||||
end if
|
||||
|
||||
if m.top.ItemType <> ""
|
||||
params.append({ IncludeItemTypes: m.top.ItemType })
|
||||
end if
|
||||
|
@ -12,6 +12,7 @@
|
||||
<field id="nameStartsWith" type="string" value="" />
|
||||
<field id="recursive" type="boolean" value="true" />
|
||||
<field id="filter" type="string" value="All" />
|
||||
<field id="filterOptions" type="assocarray" value="" />
|
||||
<field id="searchTerm" type="string" value="" />
|
||||
<field id="studioIds" type="string" value="" />
|
||||
<field id="genreIds" type="string" value="" />
|
||||
@ -25,6 +26,7 @@
|
||||
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
|
||||
</component>
|
@ -4,7 +4,7 @@ sub init()
|
||||
m.top.limit = 60
|
||||
usersettingLimit = get_user_setting("itemgrid.Limit")
|
||||
|
||||
if usersettingLimit <> invalid
|
||||
if isValid(usersettingLimit)
|
||||
m.top.limit = usersettingLimit
|
||||
end if
|
||||
end sub
|
||||
@ -135,7 +135,7 @@ sub LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtit
|
||||
if m.playbackInfo.MediaSources[0].MediaStreams.Count() > 0 and meta.live = false
|
||||
tryDirectPlay = get_user_setting("playback.tryDirect.h264ProfileLevel") = "true" and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "h264"
|
||||
tryDirectPlay = tryDirectPlay or (get_user_setting("playback.tryDirect.hevcProfileLevel") = "true" and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "hevc")
|
||||
if tryDirectPlay and m.playbackInfo.MediaSources[0].TranscodingUrl <> invalid and forceTranscoding = false
|
||||
if tryDirectPlay and isValid(m.playbackInfo.MediaSources[0].TranscodingUrl) and forceTranscoding = false
|
||||
transcodingReasons = getTranscodeReasons(m.playbackInfo.MediaSources[0].TranscodingUrl)
|
||||
if transcodingReasons.Count() = 1 and transcodingReasons[0] = "VideoLevelNotSupported"
|
||||
video.directPlaySupported = true
|
||||
@ -262,7 +262,7 @@ end function
|
||||
|
||||
function directPlaySupported(meta as object) as boolean
|
||||
devinfo = CreateObject("roDeviceInfo")
|
||||
if meta.json.MediaSources[0] <> invalid and meta.json.MediaSources[0].SupportsDirectPlay = false
|
||||
if isValid(meta.json.MediaSources[0]) and meta.json.MediaSources[0].SupportsDirectPlay = false
|
||||
return false
|
||||
end if
|
||||
|
||||
@ -271,10 +271,10 @@ function directPlaySupported(meta as object) as boolean
|
||||
end if
|
||||
|
||||
streamInfo = { Codec: meta.json.MediaStreams[0].codec }
|
||||
if meta.json.MediaStreams[0].Profile <> invalid and meta.json.MediaStreams[0].Profile.len() > 0
|
||||
if isValid(meta.json.MediaStreams[0].Profile) and meta.json.MediaStreams[0].Profile.len() > 0
|
||||
streamInfo.Profile = LCase(meta.json.MediaStreams[0].Profile)
|
||||
end if
|
||||
if meta.json.MediaSources[0].container <> invalid and meta.json.MediaSources[0].container.len() > 0
|
||||
if isValid(meta.json.MediaSources[0].container) and meta.json.MediaSources[0].container.len() > 0
|
||||
'CanDecodeVideo() requires the .container to be format: “mp4”, “hls”, “mkv”, “ism”, “dash”, “ts” if its to direct stream
|
||||
if meta.json.MediaSources[0].container = "mov"
|
||||
streamInfo.Container = "mp4"
|
||||
@ -333,12 +333,12 @@ sub autoPlayNextEpisode(videoID as string, showID as string)
|
||||
resp = APIRequest(url, urlParams)
|
||||
data = getJson(resp)
|
||||
|
||||
if data <> invalid and data.Items.Count() = 2
|
||||
if isValid(data) and data.Items.Count() = 2
|
||||
' setup new video node
|
||||
nextVideo = invalid
|
||||
' remove last videoplayer scene
|
||||
m.global.sceneManager.callFunc("clearPreviousScene")
|
||||
if nextVideo <> invalid
|
||||
if isValid(nextVideo)
|
||||
m.global.sceneManager.callFunc("pushScene", nextVideo)
|
||||
else
|
||||
m.global.sceneManager.callFunc("popScene")
|
||||
@ -356,7 +356,7 @@ end sub
|
||||
' In the future, with a custom playback info view, we can return an associated array.
|
||||
function GetPlaybackInfo()
|
||||
sessions = api_API().sessions.get()
|
||||
if sessions <> invalid and sessions.Count() > 0
|
||||
if isValid(sessions) and sessions.Count() > 0
|
||||
return GetTranscodingStats(sessions[0])
|
||||
end if
|
||||
|
||||
@ -507,7 +507,7 @@ end function
|
||||
' returns the server-side track index for the appriate subtitle
|
||||
function defaultSubtitleTrackFromVid(video_id) as integer
|
||||
meta = ItemMetaData(video_id)
|
||||
if meta?.json?.mediaSources <> invalid
|
||||
if isValid(meta) and isValid(meta.json) and isValid(meta.json.mediaSources)
|
||||
subtitles = sortSubtitles(meta.id, meta.json.MediaSources[0].MediaStreams)
|
||||
default_text_subs = defaultSubtitleTrack(subtitles["all"], true) ' Find correct subtitle track (forced text)
|
||||
if default_text_subs <> -1
|
||||
@ -608,7 +608,7 @@ function sortSubtitles(id as string, MediaStreams)
|
||||
if stream.type = "Subtitle"
|
||||
|
||||
url = ""
|
||||
if stream.DeliveryUrl <> invalid
|
||||
if isValid(stream.DeliveryUrl)
|
||||
url = buildURL(stream.DeliveryUrl)
|
||||
end if
|
||||
|
||||
@ -1199,11 +1199,15 @@ function CreateMovieDetailsGroup(movie)
|
||||
group.optionsAvailable = false
|
||||
m.global.sceneManager.callFunc("pushScene", group)
|
||||
|
||||
movie = ItemMetaData(movie.id)
|
||||
group.itemContent = movie
|
||||
movieMetaData = ItemMetaData(movie.id)
|
||||
group.itemContent = movieMetaData
|
||||
group.trailerAvailable = false
|
||||
|
||||
trailerData = api_API().users.getlocaltrailers(get_setting("active_user"), movie.id)
|
||||
activeUser = get_setting("active_user")
|
||||
trailerData = invalid
|
||||
if isValid(activeUser) and isValid(movie.id)
|
||||
trailerData = api_API().users.getlocaltrailers(activeUser, movie.id)
|
||||
end if
|
||||
if isValid(trailerData)
|
||||
group.trailerAvailable = trailerData.Count() > 0
|
||||
end if
|
||||
@ -1215,7 +1219,7 @@ function CreateMovieDetailsGroup(movie)
|
||||
|
||||
extras = group.findNode("extrasGrid")
|
||||
extras.observeField("selectedItem", m.port)
|
||||
extras.callFunc("loadParts", movie.json)
|
||||
extras.callFunc("loadParts", movieMetaData.json)
|
||||
|
||||
return group
|
||||
end function
|
||||
|
@ -67,10 +67,12 @@ sub init()
|
||||
m.sortAscending = true
|
||||
|
||||
m.filter = "All"
|
||||
m.filterOptions = {}
|
||||
m.favorite = "Favorite"
|
||||
|
||||
m.loadItemsTask = createObject("roSGNode", "LoadItemsTask2")
|
||||
m.loadLogoTask = createObject("roSGNode", "LoadItemsTask2")
|
||||
m.getFiltersTask = createObject("roSGNode", "GetFiltersTask")
|
||||
|
||||
'set inital counts for overhang before content is loaded.
|
||||
m.loadItemsTask.totalRecordCount = 0
|
||||
@ -126,6 +128,7 @@ sub loadInitialItems()
|
||||
|
||||
m.sortField = get_user_setting("display." + m.top.parentItem.Id + ".sortField")
|
||||
m.filter = get_user_setting("display." + m.top.parentItem.Id + ".filter")
|
||||
m.filterOptions = get_user_setting("display." + m.top.parentItem.Id + ".filterOptions")
|
||||
m.view = get_user_setting("display." + m.top.parentItem.Id + ".landing")
|
||||
sortAscendingStr = get_user_setting("display." + m.top.parentItem.Id + ".sortAscending")
|
||||
|
||||
@ -136,8 +139,11 @@ sub loadInitialItems()
|
||||
|
||||
if not isValid(m.sortField) then m.sortField = "SortName"
|
||||
if not isValid(m.filter) then m.filter = "All"
|
||||
if not isValid(m.filterOptions) then m.filterOptions = "{}"
|
||||
if not isValid(m.view) then m.view = "Movies"
|
||||
|
||||
m.filterOptions = ParseJson(m.filterOptions)
|
||||
|
||||
if sortAscendingStr = invalid or sortAscendingStr = "true"
|
||||
m.sortAscending = true
|
||||
else
|
||||
@ -165,6 +171,7 @@ sub loadInitialItems()
|
||||
m.loadItemsTask.sortField = m.sortField
|
||||
m.loadItemsTask.sortAscending = m.sortAscending
|
||||
m.loadItemsTask.filter = m.filter
|
||||
m.loadItemsTask.filterOptions = m.filterOptions
|
||||
m.loadItemsTask.startIndex = 0
|
||||
|
||||
' Load Item Types
|
||||
@ -216,7 +223,14 @@ sub loadInitialItems()
|
||||
m.loadItemsTask.observeField("content", "ItemDataLoaded")
|
||||
m.spinner.visible = true
|
||||
m.loadItemsTask.control = "RUN"
|
||||
SetUpOptions()
|
||||
|
||||
m.getFiltersTask.observeField("filters", "FilterDataLoaded")
|
||||
m.getFiltersTask.params = {
|
||||
userid: get_setting("active_user"),
|
||||
parentid: m.top.parentItem.Id,
|
||||
includeitemtypes: "Movie"
|
||||
}
|
||||
m.getFiltersTask.control = "RUN"
|
||||
end sub
|
||||
|
||||
' Set Movies view, sort, and filter options
|
||||
@ -291,12 +305,7 @@ function inStringArray(array, searchValue) as boolean
|
||||
end function
|
||||
|
||||
' Data to display when options button selected
|
||||
sub SetUpOptions()
|
||||
options = {}
|
||||
options.filter = []
|
||||
options.favorite = []
|
||||
|
||||
setMoviesOptions(options)
|
||||
sub setSelectedOptions(options)
|
||||
|
||||
' Set selected view option
|
||||
for each o in options.views
|
||||
@ -316,17 +325,76 @@ sub SetUpOptions()
|
||||
end if
|
||||
end for
|
||||
|
||||
' Set selected filter option
|
||||
' Set selected filter
|
||||
for each o in options.filter
|
||||
if o.Name = m.filter
|
||||
o.Selected = true
|
||||
m.options.filter = o.Name
|
||||
end if
|
||||
|
||||
' Select selected filter options
|
||||
if isValid(o.options) and isValid(m.filterOptions)
|
||||
if o.options.Count() > 0 and m.filterOptions.Count() > 0
|
||||
if LCase(o.Name) = LCase(m.filterOptions.keys()[0])
|
||||
selectedFilterOptions = m.filterOptions[m.filterOptions.keys()[0]].split(o.delimiter)
|
||||
checkedState = []
|
||||
|
||||
for each availableFilterOption in o.options
|
||||
matchFound = false
|
||||
|
||||
for each selectedFilterOption in selectedFilterOptions
|
||||
if LCase(toString(availableFilterOption).trim()) = LCase(selectedFilterOption.trim())
|
||||
matchFound = true
|
||||
end if
|
||||
end for
|
||||
|
||||
checkedState.push(matchFound)
|
||||
end for
|
||||
|
||||
o.checkedState = checkedState
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
end for
|
||||
|
||||
m.options.options = options
|
||||
end sub
|
||||
|
||||
'
|
||||
' Logo Image Loaded Event Handler
|
||||
sub FilterDataLoaded(msg)
|
||||
options = {}
|
||||
options.filter = []
|
||||
options.favorite = []
|
||||
|
||||
setMoviesOptions(options)
|
||||
|
||||
data = msg.GetData()
|
||||
m.getFiltersTask.unobserveField("filters")
|
||||
|
||||
if not isValid(data) then return
|
||||
|
||||
' Add Movie filters from the API data
|
||||
if LCase(m.loadItemsTask.view) = "movies"
|
||||
if isValid(data.genres)
|
||||
options.filter.push({ "Title": tr("Genres"), "Name": "Genres", "Options": data.genres, "Delimiter": "|", "CheckedState": [] })
|
||||
end if
|
||||
|
||||
if isValid(data.OfficialRatings)
|
||||
options.filter.push({ "Title": tr("Parental Ratings"), "Name": "OfficialRatings", "Options": data.OfficialRatings, "Delimiter": "|", "CheckedState": [] })
|
||||
end if
|
||||
|
||||
if isValid(data.Years)
|
||||
options.filter.push({ "Title": tr("Years"), "Name": "Years", "Options": data.Years, "Delimiter": ",", "CheckedState": [] })
|
||||
end if
|
||||
end if
|
||||
|
||||
setSelectedOptions(options)
|
||||
|
||||
m.options.options = options
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Logo Image Loaded Event Handler
|
||||
sub LogoImageLoaded(msg)
|
||||
@ -384,6 +452,10 @@ sub ItemDataLoaded(msg)
|
||||
m.itemGrid.setFocus(true)
|
||||
m.genreList.setFocus(false)
|
||||
|
||||
if m.data.getChildCount() = 0
|
||||
m.itemGrid.jumpToItem = 0
|
||||
end if
|
||||
|
||||
for each item in itemData
|
||||
m.data.appendChild(item)
|
||||
end for
|
||||
@ -709,6 +781,16 @@ sub optionsClosed()
|
||||
set_user_setting("display." + m.top.parentItem.Id + ".filter", m.options.filter)
|
||||
end if
|
||||
|
||||
if not isValid(m.options.filterOptions)
|
||||
m.filterOptions = {}
|
||||
end if
|
||||
|
||||
if not AssocArrayEqual(m.options.filterOptions, m.filterOptions)
|
||||
m.filterOptions = m.options.filterOptions
|
||||
reload = true
|
||||
set_user_setting("display." + m.top.parentItem.Id + ".filterOptions", FormatJson(m.options.filterOptions))
|
||||
end if
|
||||
|
||||
m.view = get_user_setting("display." + m.top.parentItem.Id + ".landing")
|
||||
|
||||
if m.options.view <> m.view
|
||||
@ -720,6 +802,7 @@ sub optionsClosed()
|
||||
m.loadItemsTask.NameStartsWith = " "
|
||||
m.loadItemsTask.searchTerm = ""
|
||||
m.filter = "All"
|
||||
m.filterOptions = {}
|
||||
m.sortField = "SortName"
|
||||
m.sortAscending = true
|
||||
|
||||
@ -727,6 +810,7 @@ sub optionsClosed()
|
||||
set_user_setting("display." + m.top.parentItem.Id + ".sortField", m.sortField)
|
||||
set_user_setting("display." + m.top.parentItem.Id + ".sortAscending", "true")
|
||||
set_user_setting("display." + m.top.parentItem.Id + ".filter", m.filter)
|
||||
set_user_setting("display." + m.top.parentItem.Id + ".filterOptions", FormatJson(m.filterOptions))
|
||||
|
||||
reload = true
|
||||
end if
|
||||
@ -845,6 +929,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||
m.top.alphaSelected = ""
|
||||
m.loadItemsTask.filter = "All"
|
||||
m.filter = "All"
|
||||
m.filterOptions = {}
|
||||
m.data = CreateObject("roSGNode", "ContentNode")
|
||||
m.itemGrid.content = m.data
|
||||
loadInitialItems()
|
||||
|
@ -63,4 +63,5 @@
|
||||
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
|
||||
<script type="text/brightscript" uri="MovieLibraryView.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
|
||||
</component>
|
||||
|
@ -101,6 +101,7 @@ sub updateTime()
|
||||
end sub
|
||||
|
||||
sub resetTime()
|
||||
if m.hideClock then return
|
||||
m.currentTimeTimer.control = "stop"
|
||||
m.currentTimeTimer.control = "start"
|
||||
updateTime()
|
||||
|
@ -18,9 +18,8 @@ sub init()
|
||||
end sub
|
||||
|
||||
sub updateSize()
|
||||
|
||||
image = invalid
|
||||
if m.top.itemContent <> invalid and m.top.itemContent.image <> invalid
|
||||
if isValid(m.top.itemContent) and isValid(m.top.itemContent.image)
|
||||
image = m.top.itemContent.image
|
||||
end if
|
||||
|
||||
@ -49,7 +48,6 @@ sub updateSize()
|
||||
|
||||
m.backdrop.width = m.poster.width
|
||||
m.backdrop.height = m.poster.height
|
||||
|
||||
end sub
|
||||
|
||||
sub itemContentChanged() as void
|
||||
@ -58,7 +56,7 @@ sub itemContentChanged() as void
|
||||
m.title.text = itemData.title
|
||||
|
||||
if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
|
||||
if itemData?.json?.UserData?.UnplayedItemCount <> invalid
|
||||
if isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
|
||||
if itemData.json.UserData.UnplayedItemCount > 0
|
||||
m.unplayedCount.visible = true
|
||||
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
|
||||
@ -66,12 +64,11 @@ sub itemContentChanged() as void
|
||||
end if
|
||||
end if
|
||||
|
||||
if itemData.json.lookup("Type") = "Episode" and itemData.json.IndexNumber <> invalid
|
||||
if itemData.json.lookup("Type") = "Episode" and isValid(itemData.json.IndexNumber)
|
||||
m.title.text = StrI(itemData.json.IndexNumber) + ". " + m.title.text
|
||||
|
||||
m.series.text = itemData.json.Series
|
||||
m.series.visible = true
|
||||
|
||||
else if itemData.json.lookup("Type") = "MusicAlbum"
|
||||
m.title.font = "font:SmallestSystemFont"
|
||||
m.staticTitle.font = "font:SmallestSystemFont"
|
||||
@ -83,8 +80,7 @@ sub itemContentChanged() as void
|
||||
imageUrl = itemData.posterURL
|
||||
|
||||
if get_user_setting("ui.tvshows.blurunwatched") = "true"
|
||||
|
||||
if itemData.json.lookup("Type") = "Episode" and itemData.json.userdata <> invalid
|
||||
if itemData.json.lookup("Type") = "Episode" and isValid(itemData.json.userdata)
|
||||
if not itemData.json.userdata.played
|
||||
imageUrl = imageUrl + "&blur=15"
|
||||
end if
|
||||
@ -99,25 +95,21 @@ end sub
|
||||
'
|
||||
' Enable title scrolling based on item Focus
|
||||
sub focusChanged()
|
||||
|
||||
if m.top.itemHasFocus = true
|
||||
m.title.repeatCount = -1
|
||||
m.series.repeatCount = -1
|
||||
m.staticTitle.visible = false
|
||||
m.title.visible = true
|
||||
|
||||
' text to speech for accessibility
|
||||
if m.deviceInfo.IsAudioGuideEnabled() = true
|
||||
txt2Speech = CreateObject("roTextToSpeech")
|
||||
txt2Speech.Flush()
|
||||
txt2Speech.Say(m.title.text)
|
||||
end if
|
||||
|
||||
else
|
||||
m.title.repeatCount = 0
|
||||
m.series.repeatCount = 0
|
||||
m.staticTitle.visible = true
|
||||
m.title.visible = false
|
||||
end if
|
||||
|
||||
end sub
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="ListPoster" extends="Group">
|
||||
<children>
|
||||
<Rectangle id="backdrop" />
|
||||
@ -12,10 +12,11 @@
|
||||
<Label id="staticTitle" horizAlign="center" font="font:SmallSystemFont" wrap="false" />
|
||||
</children>
|
||||
<interface>
|
||||
<field id="itemContent" type="node" onChange="itemContentChanged"/>
|
||||
<field id="itemContent" type="node" onChange="itemContentChanged" />
|
||||
<field id="itemWidth" type="integer" />
|
||||
<field id="itemHasFocus" type="boolean" onChange="focusChanged" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="ListPoster.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
</component>
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
</component>
|
8
components/data/GetFiltersTask.brs
Normal file
8
components/data/GetFiltersTask.brs
Normal file
@ -0,0 +1,8 @@
|
||||
sub init()
|
||||
m.top.functionName = "getFiltersTask"
|
||||
end sub
|
||||
|
||||
sub getFiltersTask()
|
||||
m.filters = api_API().items.getFilters(m.top.params)
|
||||
m.top.filters = m.filters
|
||||
end sub
|
11
components/data/GetFiltersTask.xml
Normal file
11
components/data/GetFiltersTask.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
|
||||
<component name="GetFiltersTask" extends="Task">
|
||||
<interface>
|
||||
<field id="params" type="assocarray" />
|
||||
<field id="filters" type="assocarray" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="GetFiltersTask.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
|
||||
</component>
|
@ -72,7 +72,7 @@ sub onPeopleLoaded()
|
||||
row = m.top.content.createChild("ContentNode")
|
||||
row.Title = tr("Cast & Crew")
|
||||
for each person in people
|
||||
if person.json.type = "Actor" and person.json.Role <> invalid
|
||||
if person.json.type = "Actor" and person.json.Role <> invalid and person.json.Role.ToStr().Trim() <> ""
|
||||
person.subTitle = "as " + person.json.Role
|
||||
else
|
||||
person.subTitle = person.json.Type
|
||||
|
@ -33,13 +33,13 @@ sub itemContentChanged()
|
||||
|
||||
m.backdrop.width = itemData.imageWidth
|
||||
|
||||
if itemData.iconUrl <> invalid
|
||||
if isValid(itemData.iconUrl)
|
||||
m.itemIcon.uri = itemData.iconUrl
|
||||
end if
|
||||
|
||||
if LCase(itemData.type) = "series"
|
||||
if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
|
||||
if itemData?.json?.UserData?.UnplayedItemCount <> invalid
|
||||
if isValid(itemData.json.UserData) and isValid(itemData.json.UserData.UnplayedItemCount)
|
||||
if itemData.json.UserData.UnplayedItemCount > 0
|
||||
m.unplayedCount.visible = true
|
||||
m.unplayedEpisodeCount.text = itemData.json.UserData.UnplayedItemCount
|
||||
@ -84,7 +84,7 @@ sub itemContentChanged()
|
||||
end if
|
||||
|
||||
' Set Episode title if available
|
||||
if itemData.json.EpisodeTitle <> invalid
|
||||
if isValid(itemData.json.EpisodeTitle)
|
||||
m.itemTextExtra.text = itemData.json.EpisodeTitle
|
||||
end if
|
||||
|
||||
@ -106,10 +106,10 @@ sub itemContentChanged()
|
||||
|
||||
' Set Series and Episode Number for Extra Text
|
||||
extraPrefix = ""
|
||||
if itemData.json.ParentIndexNumber <> invalid
|
||||
if isValid(itemData.json.ParentIndexNumber)
|
||||
extraPrefix = "S" + StrI(itemData.json.ParentIndexNumber).trim()
|
||||
end if
|
||||
if itemData.json.IndexNumber <> invalid
|
||||
if isValid(itemData.json.IndexNumber)
|
||||
extraPrefix = extraPrefix + "E" + StrI(itemData.json.IndexNumber).trim()
|
||||
end if
|
||||
if extraPrefix.len() > 0
|
||||
@ -136,10 +136,10 @@ sub itemContentChanged()
|
||||
|
||||
' Set Release Year and Age Rating for Extra Text
|
||||
textExtra = ""
|
||||
if itemData.json.ProductionYear <> invalid
|
||||
if isValid(itemData.json.ProductionYear)
|
||||
textExtra = StrI(itemData.json.ProductionYear).trim()
|
||||
end if
|
||||
if itemData.json.OfficialRating <> invalid
|
||||
if isValid(itemData.json.OfficialRating)
|
||||
if textExtra <> ""
|
||||
textExtra = textExtra + " - " + itemData.json.OfficialRating
|
||||
else
|
||||
@ -181,14 +181,14 @@ sub itemContentChanged()
|
||||
end if
|
||||
|
||||
textExtra = ""
|
||||
if itemData.json.ProductionYear <> invalid
|
||||
if isValid(itemData.json.ProductionYear)
|
||||
textExtra = StrI(itemData.json.ProductionYear).trim()
|
||||
end if
|
||||
|
||||
' Set Years Run for Extra Text
|
||||
if itemData.json.Status = "Continuing"
|
||||
textExtra = textExtra + " - Present"
|
||||
else if itemData.json.Status = "Ended" and itemData.json.EndDate <> invalid
|
||||
else if itemData.json.Status = "Ended" and isValid(itemData.json.EndDate)
|
||||
textExtra = textExtra + " - " + LEFT(itemData.json.EndDate, 4)
|
||||
end if
|
||||
m.itemTextExtra.text = textExtra
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="HomeItem" extends="Group">
|
||||
<children>
|
||||
<Rectangle id="backdrop" width="464" height="261" translation="[8,5]" />
|
||||
@ -26,4 +26,5 @@
|
||||
<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" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
</component>
|
@ -96,7 +96,7 @@ sub onLibrariesLoaded()
|
||||
m.LoadFavoritesTask.control = "RUN"
|
||||
|
||||
' validate library data
|
||||
if m.libraryData <> invalid and m.libraryData.count() > 0
|
||||
if isValid(m.libraryData) and m.libraryData.count() > 0
|
||||
userConfig = m.top.userConfig
|
||||
|
||||
' populate My Media row
|
||||
@ -163,7 +163,7 @@ sub updateFavoritesItems()
|
||||
rowIndex = getRowIndex("Favorites")
|
||||
|
||||
if itemData.count() < 1
|
||||
if rowIndex <> invalid
|
||||
if isValid(rowIndex)
|
||||
' remove the row
|
||||
deleteFromSizeArray(rowIndex)
|
||||
homeRows.removeChildIndex(rowIndex)
|
||||
@ -208,7 +208,7 @@ sub updateContinueItems()
|
||||
continueRowIndex = getRowIndex("Continue Watching")
|
||||
|
||||
if itemData.count() < 1
|
||||
if continueRowIndex <> invalid
|
||||
if isValid(continueRowIndex)
|
||||
' remove the row
|
||||
deleteFromSizeArray(continueRowIndex)
|
||||
homeRows.removeChildIndex(continueRowIndex)
|
||||
@ -219,7 +219,7 @@ sub updateContinueItems()
|
||||
row.title = tr("Continue Watching")
|
||||
itemSize = [464, 331]
|
||||
for each item in itemData
|
||||
if item.json?.UserData?.PlayedPercentage <> invalid
|
||||
if isValid(item.json) and isValid(item.json.UserData) and isValid(item.json.UserData.PlayedPercentage)
|
||||
item.PlayedPercentage = item.json.UserData.PlayedPercentage
|
||||
end if
|
||||
|
||||
@ -250,7 +250,7 @@ sub updateNextUpItems()
|
||||
nextUpRowIndex = getRowIndex("Next Up >")
|
||||
|
||||
if itemData.count() < 1
|
||||
if nextUpRowIndex <> invalid
|
||||
if isValid(nextUpRowIndex)
|
||||
' remove the row
|
||||
deleteFromSizeArray(nextUpRowIndex)
|
||||
homeRows.removeChildIndex(nextUpRowIndex)
|
||||
@ -269,7 +269,7 @@ sub updateNextUpItems()
|
||||
if nextUpRowIndex = invalid
|
||||
' insert new row under "Continue Watching"
|
||||
continueRowIndex = getRowIndex("Continue Watching")
|
||||
if continueRowIndex <> invalid
|
||||
if isValid(continueRowIndex)
|
||||
updateSizeArray(itemSize, continueRowIndex + 1)
|
||||
homeRows.insertChild(row, continueRowIndex + 1)
|
||||
else
|
||||
@ -305,7 +305,7 @@ sub updateLatestItems(msg)
|
||||
|
||||
if itemData.count() < 1
|
||||
' remove row
|
||||
if rowIndex <> invalid
|
||||
if isValid(rowIndex)
|
||||
deleteFromSizeArray(rowIndex)
|
||||
homeRows.removeChildIndex(rowIndex)
|
||||
end if
|
||||
@ -355,7 +355,7 @@ sub updateOnNowItems()
|
||||
onNowRowIndex = getRowIndex("On Now")
|
||||
|
||||
if itemData.count() < 1
|
||||
if onNowRowIndex <> invalid
|
||||
if isValid(onNowRowIndex)
|
||||
' remove the row
|
||||
deleteFromSizeArray(onNowRowIndex)
|
||||
homeRows.removeChildIndex(onNowRowIndex)
|
||||
@ -409,11 +409,11 @@ sub updateSizeArray(rowItemSize, rowIndex = invalid, action = "insert")
|
||||
newSizeArray.Push(rowItemSize)
|
||||
else if action = "insert"
|
||||
newSizeArray.Push(rowItemSize)
|
||||
if sizeArray[i] <> invalid
|
||||
if isValid(sizeArray[i])
|
||||
newSizeArray.Push(sizeArray[i])
|
||||
end if
|
||||
end if
|
||||
else if sizeArray[i] <> invalid
|
||||
else if isValid(sizeArray[i])
|
||||
newSizeArray.Push(sizeArray[i])
|
||||
end if
|
||||
end for
|
||||
@ -433,7 +433,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||
if press
|
||||
if key = "play"
|
||||
itemToPlay = m.top.content.getChild(m.top.rowItemFocused[0]).getChild(m.top.rowItemFocused[1])
|
||||
if itemToPlay <> invalid and (itemToPlay.type = "Movie" or itemToPlay.type = "Episode")
|
||||
if isValid(itemToPlay) and (itemToPlay.type = "Movie" or itemToPlay.type = "Episode")
|
||||
m.top.quickPlayNode = itemToPlay
|
||||
end if
|
||||
handled = true
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="HomeRows" extends="RowList">
|
||||
<interface>
|
||||
<field id="selectedItem" type="node" alwaysNotify="true" />
|
||||
@ -7,5 +7,6 @@
|
||||
<function name="updateHomeRows" />
|
||||
<function name="loadLibraries" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="HomeRows.brs"/>
|
||||
</component>
|
||||
<script type="text/brightscript" uri="HomeRows.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
</component>
|
@ -12,37 +12,43 @@ sub loadItems()
|
||||
url = Substitute("Users/{0}/Views/", get_setting("active_user"))
|
||||
resp = APIRequest(url)
|
||||
data = getJson(resp)
|
||||
for each item in data.Items
|
||||
' Skip Books for now as we don't support it (issue #525)
|
||||
if item.CollectionType <> "books"
|
||||
tmp = CreateObject("roSGNode", "HomeData")
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end if
|
||||
end for
|
||||
if isValid(data) and isValid(data.Items)
|
||||
for each item in data.Items
|
||||
' Skip Books for now as we don't support it (issue #525)
|
||||
if item.CollectionType <> "books"
|
||||
tmp = CreateObject("roSGNode", "HomeData")
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end if
|
||||
end for
|
||||
end if
|
||||
|
||||
' Load Latest Additions to Libraries
|
||||
else if m.top.itemsToLoad = "latest"
|
||||
activeUser = get_setting("active_user")
|
||||
if isValid(activeUser)
|
||||
url = Substitute("Users/{0}/Items/Latest", activeUser)
|
||||
params = {}
|
||||
params["Limit"] = 16
|
||||
params["ParentId"] = m.top.itemId
|
||||
params["EnableImageTypes"] = "Primary,Backdrop,Thumb"
|
||||
params["ImageTypeLimit"] = 1
|
||||
params["EnableTotalRecordCount"] = false
|
||||
|
||||
url = Substitute("Users/{0}/Items/Latest", get_setting("active_user"))
|
||||
params = {}
|
||||
params["Limit"] = 16
|
||||
params["ParentId"] = m.top.itemId
|
||||
params["EnableImageTypes"] = "Primary,Backdrop,Thumb"
|
||||
params["ImageTypeLimit"] = 1
|
||||
params["EnableTotalRecordCount"] = false
|
||||
resp = APIRequest(url, params)
|
||||
data = getJson(resp)
|
||||
|
||||
resp = APIRequest(url, params)
|
||||
data = getJson(resp)
|
||||
|
||||
for each item in data
|
||||
' Skip Books for now as we don't support it (issue #525)
|
||||
if item.Type <> "Book"
|
||||
tmp = CreateObject("roSGNode", "HomeData")
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
if isValid(data)
|
||||
for each item in data
|
||||
' Skip Books for now as we don't support it (issue #525)
|
||||
if item.Type <> "Book"
|
||||
tmp = CreateObject("roSGNode", "HomeData")
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end if
|
||||
end for
|
||||
end if
|
||||
end for
|
||||
end if
|
||||
|
||||
' Load Next Up
|
||||
else if m.top.itemsToLoad = "nextUp"
|
||||
@ -74,34 +80,39 @@ sub loadItems()
|
||||
|
||||
resp = APIRequest(url, params)
|
||||
data = getJson(resp)
|
||||
for each item in data.Items
|
||||
tmp = CreateObject("roSGNode", "HomeData")
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end for
|
||||
|
||||
' Load Continue Watching
|
||||
else if m.top.itemsToLoad = "continue"
|
||||
|
||||
url = Substitute("Users/{0}/Items/Resume", get_setting("active_user"))
|
||||
|
||||
params = {}
|
||||
params["recursive"] = true
|
||||
params["SortBy"] = "DatePlayed"
|
||||
params["SortOrder"] = "Descending"
|
||||
params["Filters"] = "IsResumable"
|
||||
params["EnableTotalRecordCount"] = false
|
||||
|
||||
resp = APIRequest(url, params)
|
||||
data = getJson(resp)
|
||||
for each item in data.Items
|
||||
' Skip Books for now as we don't support it (issue #558)
|
||||
if item.Type <> "Book"
|
||||
if isValid(data) and isValid(data.Items)
|
||||
for each item in data.Items
|
||||
tmp = CreateObject("roSGNode", "HomeData")
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end for
|
||||
end if
|
||||
' Load Continue Watching
|
||||
else if m.top.itemsToLoad = "continue"
|
||||
activeUser = get_setting("active_user")
|
||||
if isValid(activeUser)
|
||||
url = Substitute("Users/{0}/Items/Resume", activeUser)
|
||||
|
||||
params = {}
|
||||
params["recursive"] = true
|
||||
params["SortBy"] = "DatePlayed"
|
||||
params["SortOrder"] = "Descending"
|
||||
params["Filters"] = "IsResumable"
|
||||
params["EnableTotalRecordCount"] = false
|
||||
|
||||
resp = APIRequest(url, params)
|
||||
data = getJson(resp)
|
||||
if isValid(data) and isValid(data.Items)
|
||||
for each item in data.Items
|
||||
' Skip Books for now as we don't support it (issue #558)
|
||||
if item.Type <> "Book"
|
||||
tmp = CreateObject("roSGNode", "HomeData")
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end if
|
||||
end for
|
||||
end if
|
||||
end for
|
||||
end if
|
||||
|
||||
else if m.top.itemsToLoad = "favorites"
|
||||
|
||||
@ -116,14 +127,16 @@ sub loadItems()
|
||||
|
||||
resp = APIRequest(url, params)
|
||||
data = getJson(resp)
|
||||
for each item in data.Items
|
||||
' Skip Books for now as we don't support it (issue #558)
|
||||
if item.Type <> "Book"
|
||||
tmp = CreateObject("roSGNode", "HomeData")
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end if
|
||||
end for
|
||||
if isValid(data) and isValid(data.Items)
|
||||
for each item in data.Items
|
||||
' Skip Books for now as we don't support it (issue #558)
|
||||
if item.Type <> "Book"
|
||||
tmp = CreateObject("roSGNode", "HomeData")
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end if
|
||||
end for
|
||||
end if
|
||||
|
||||
else if m.top.itemsToLoad = "onNow"
|
||||
url = "LiveTv/Programs/Recommended"
|
||||
@ -138,12 +151,14 @@ sub loadItems()
|
||||
|
||||
resp = APIRequest(url, params)
|
||||
data = getJson(resp)
|
||||
for each item in data.Items
|
||||
tmp = CreateObject("roSGNode", "HomeData")
|
||||
item.ImageURL = ImageURL(item.Id)
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end for
|
||||
if isValid(data) and isValid(data.Items)
|
||||
for each item in data.Items
|
||||
tmp = CreateObject("roSGNode", "HomeData")
|
||||
item.ImageURL = ImageURL(item.Id)
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end for
|
||||
end if
|
||||
|
||||
' Extract array of persons from Views and download full metadata for each
|
||||
else if m.top.itemsToLoad = "people"
|
||||
@ -195,12 +210,14 @@ sub loadItems()
|
||||
url = Substitute("Items/{0}/Similar", m.top.itemId)
|
||||
resp = APIRequest(url, params)
|
||||
data = getJson(resp)
|
||||
for each item in data.items
|
||||
tmp = CreateObject("roSGNode", "ExtrasData")
|
||||
tmp.posterURL = ImageUrl(item.Id, "Primary", { "Tags": item.PrimaryImageTag })
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end for
|
||||
if isValid(data) and isValid(data.Items)
|
||||
for each item in data.items
|
||||
tmp = CreateObject("roSGNode", "ExtrasData")
|
||||
tmp.posterURL = ImageUrl(item.Id, "Primary", { "Tags": item.PrimaryImageTag })
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end for
|
||||
end if
|
||||
else if m.top.itemsToLoad = "personMovies"
|
||||
getPersonVideos("Movie", results, {})
|
||||
else if m.top.itemsToLoad = "personTVShows"
|
||||
|
@ -4,7 +4,6 @@ sub init()
|
||||
m.position = 0
|
||||
end sub
|
||||
|
||||
'
|
||||
' Clear all content from play queue
|
||||
sub clear()
|
||||
m.queue = []
|
||||
@ -12,72 +11,52 @@ sub clear()
|
||||
setPosition(0)
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Delete item from play queue at passed index
|
||||
sub deleteAtIndex(index)
|
||||
m.queue.Delete(index)
|
||||
m.queueTypes.Delete(index)
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Return the number of items in the play queue
|
||||
function getCount()
|
||||
return m.queue.count()
|
||||
end function
|
||||
|
||||
|
||||
'
|
||||
' Return the item currently in focus from the play queue
|
||||
function getCurrentItem()
|
||||
return getItemByIndex(m.position)
|
||||
end function
|
||||
|
||||
|
||||
'
|
||||
' Return the item in the passed index from the play queue
|
||||
function getItemByIndex(index)
|
||||
return m.queue[index]
|
||||
end function
|
||||
|
||||
|
||||
'
|
||||
' Returns current playback position within the queue
|
||||
function getPosition()
|
||||
return m.position
|
||||
end function
|
||||
|
||||
|
||||
'
|
||||
' Move queue position back one
|
||||
sub moveBack()
|
||||
m.position--
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Move queue position ahead one
|
||||
sub moveForward()
|
||||
m.position++
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Return the current play queue
|
||||
function getQueue()
|
||||
return m.queue
|
||||
end function
|
||||
|
||||
|
||||
'
|
||||
' Return the types of items in current play queue
|
||||
function getQueueTypes()
|
||||
return m.queueTypes
|
||||
end function
|
||||
|
||||
|
||||
'
|
||||
' Return the unique types of items in current play queue
|
||||
function getQueueUniqueTypes()
|
||||
itemTypes = []
|
||||
@ -91,15 +70,11 @@ function getQueueUniqueTypes()
|
||||
return itemTypes
|
||||
end function
|
||||
|
||||
|
||||
'
|
||||
' Return item at end of play queue without removing
|
||||
function peek()
|
||||
return m.queue.peek()
|
||||
end function
|
||||
|
||||
|
||||
'
|
||||
' Play items in queue
|
||||
sub playQueue()
|
||||
nextItem = getCurrentItem()
|
||||
@ -116,37 +91,28 @@ sub playQueue()
|
||||
end if
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Remove item at end of play queue
|
||||
sub pop()
|
||||
m.queue.pop()
|
||||
m.queueTypes.pop()
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Push new items to the play queue
|
||||
sub push(newItem)
|
||||
m.queue.push(newItem)
|
||||
m.queueTypes.push(getItemType(newItem))
|
||||
end sub
|
||||
|
||||
'
|
||||
' Set the queue position
|
||||
sub setPosition(newPosition)
|
||||
m.position = newPosition
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' Return the fitst item in the play queue
|
||||
function top()
|
||||
return getItemByIndex(0)
|
||||
end function
|
||||
|
||||
|
||||
'
|
||||
' Replace play queue with passed array
|
||||
sub set(items)
|
||||
setPosition(0)
|
||||
@ -157,10 +123,9 @@ sub set(items)
|
||||
end sub
|
||||
|
||||
function getItemType(item) as string
|
||||
|
||||
if isValid(item?.json?.mediatype) and item.json.mediatype <> ""
|
||||
if isValid(item) and isValid(item.json) and isValid(item.json.mediatype) and item.json.mediatype <> ""
|
||||
return LCase(item.json.mediatype)
|
||||
else if isValid(item?.type) and item.type <> ""
|
||||
else if isValid(item) and isValid(item.type) and item.type <> ""
|
||||
return LCase(item.type)
|
||||
end if
|
||||
|
||||
|
@ -1,7 +1,3 @@
|
||||
'
|
||||
' View Creators
|
||||
' ----------------
|
||||
|
||||
' Play Audio
|
||||
sub CreateAudioPlayerView()
|
||||
m.view = CreateObject("roSGNode", "AudioPlayerView")
|
||||
@ -26,14 +22,12 @@ sub CreateVideoPlayerView()
|
||||
m.global.sceneManager.callFunc("pushScene", m.view)
|
||||
end sub
|
||||
|
||||
|
||||
'
|
||||
' -----------------
|
||||
' Event Handlers
|
||||
' -----------------
|
||||
|
||||
' User requested subtitle selection popup
|
||||
sub onSelectSubtitlePressed()
|
||||
|
||||
' None is always first in the subtitle list
|
||||
subtitleData = {
|
||||
data: [{ "description": "None", "type": "subtitleselection" }]
|
||||
@ -80,9 +74,8 @@ end sub
|
||||
|
||||
' User requested playback info
|
||||
sub onSelectPlaybackInfoPressed()
|
||||
|
||||
' Check if we already have playback info and show it in a popup
|
||||
if isValid(m.playbackData?.playbackinfo)
|
||||
if isValid(m.playbackData) and isValid(m.playbackData.playbackinfo)
|
||||
m.global.sceneManager.callFunc("standardDialog", tr("Playback Info"), m.playbackData.playbackinfo)
|
||||
return
|
||||
end if
|
||||
@ -95,12 +88,11 @@ sub onPlaybackInfoLoaded()
|
||||
m.playbackData = m.getPlaybackInfoTask.data
|
||||
|
||||
' Check if we have playback info and show it in a popup
|
||||
if isValid(m.playbackData?.playbackinfo)
|
||||
if isValid(m.playbackData) and isValid(m.playbackData.playbackinfo)
|
||||
m.global.sceneManager.callFunc("standardDialog", tr("Playback Info"), m.playbackData.playbackinfo)
|
||||
end if
|
||||
end sub
|
||||
|
||||
|
||||
' Playback state change event handlers
|
||||
sub onStateChange()
|
||||
if LCase(m.view.state) = "finished"
|
||||
|
39
components/mediaPlayers/AudioPlayer.brs
Normal file
39
components/mediaPlayers/AudioPlayer.brs
Normal file
@ -0,0 +1,39 @@
|
||||
sub init()
|
||||
m.playReported = false
|
||||
m.top.observeField("state", "audioStateChanged")
|
||||
end sub
|
||||
|
||||
' State Change Event Handler
|
||||
sub audioStateChanged()
|
||||
currentState = LCase(m.top.state)
|
||||
|
||||
reportedPlaybackState = "update"
|
||||
|
||||
if currentState = "playing" and not m.playReported
|
||||
reportedPlaybackState = "start"
|
||||
m.playReported = true
|
||||
else if currentState = "stopped" or currentState = "finished"
|
||||
reportedPlaybackState = "stop"
|
||||
m.playReported = false
|
||||
end if
|
||||
|
||||
ReportPlayback(reportedPlaybackState)
|
||||
end sub
|
||||
|
||||
' Report playback to server
|
||||
sub ReportPlayback(state as string)
|
||||
|
||||
if not isValid(m.top.position) then return
|
||||
|
||||
params = {
|
||||
"ItemId": m.global.queueManager.callFunc("getCurrentItem").id,
|
||||
"PlaySessionId": m.top.content.id,
|
||||
"PositionTicks": int(m.top.position) * 10000000&, 'Ensure a LongInteger is used
|
||||
"IsPaused": (LCase(m.top.state) = "paused")
|
||||
}
|
||||
|
||||
' Report playstate via global task
|
||||
playstateTask = m.global.playstateTask
|
||||
playstateTask.setFields({ status: state, params: params })
|
||||
playstateTask.control = "RUN"
|
||||
end sub
|
5
components/mediaPlayers/AudioPlayer.xml
Normal file
5
components/mediaPlayers/AudioPlayer.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="AudioPlayer" extends="Audio">
|
||||
<script type="text/brightscript" uri="AudioPlayer.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
</component>
|
@ -13,7 +13,6 @@ sub init()
|
||||
m.shuffleEnabled = false
|
||||
m.loopMode = ""
|
||||
m.buttonCount = m.buttons.getChildCount()
|
||||
m.playReported = false
|
||||
|
||||
m.screenSaverTimeout = 300
|
||||
|
||||
@ -83,10 +82,9 @@ end sub
|
||||
|
||||
' Creates audio node used to play song(s)
|
||||
sub setupAudioNode()
|
||||
m.top.audio = createObject("RoSGNode", "Audio")
|
||||
m.top.audio.observeField("state", "audioStateChanged")
|
||||
m.top.audio.observeField("position", "audioPositionChanged")
|
||||
m.top.audio.observeField("bufferingStatus", "bufferPositionChanged")
|
||||
m.global.audioPlayer.observeField("state", "audioStateChanged")
|
||||
m.global.audioPlayer.observeField("position", "audioPositionChanged")
|
||||
m.global.audioPlayer.observeField("bufferingStatus", "bufferPositionChanged")
|
||||
end sub
|
||||
|
||||
' Setup playback buttons, default to Play button selected
|
||||
@ -120,10 +118,10 @@ sub setupInfoNodes()
|
||||
end sub
|
||||
|
||||
sub bufferPositionChanged()
|
||||
if not isValid(m.top.audio.bufferingStatus)
|
||||
if not isValid(m.global.audioPlayer.bufferingStatus)
|
||||
bufferPositionBarWidth = m.seekBar.width
|
||||
else
|
||||
bufferPositionBarWidth = m.seekBar.width * m.top.audio.bufferingStatus.percentage
|
||||
bufferPositionBarWidth = m.seekBar.width * m.global.audioPlayer.bufferingStatus.percentage
|
||||
end if
|
||||
|
||||
' Ensure position bar is never wider than the seek bar
|
||||
@ -137,16 +135,16 @@ sub bufferPositionChanged()
|
||||
end sub
|
||||
|
||||
sub audioPositionChanged()
|
||||
if m.top.audio.position = 0
|
||||
if m.global.audioPlayer.position = 0
|
||||
m.playPosition.width = 0
|
||||
end if
|
||||
|
||||
if not isValid(m.top.audio.position)
|
||||
if not isValid(m.global.audioPlayer.position)
|
||||
playPositionBarWidth = 0
|
||||
else if not isValid(m.songDuration)
|
||||
playPositionBarWidth = 0
|
||||
else
|
||||
songPercentComplete = m.top.audio.position / m.songDuration
|
||||
songPercentComplete = m.global.audioPlayer.position / m.songDuration
|
||||
playPositionBarWidth = m.seekBar.width * songPercentComplete
|
||||
end if
|
||||
|
||||
@ -202,25 +200,8 @@ end sub
|
||||
|
||||
sub audioStateChanged()
|
||||
|
||||
if m.top.audio.state = "playing"
|
||||
if m.playReported
|
||||
ReportPlayback()
|
||||
else
|
||||
ReportPlayback("start")
|
||||
m.playReported = true
|
||||
end if
|
||||
else if m.top.audio.state = "paused"
|
||||
ReportPlayback()
|
||||
else if m.top.audio.state = "stopped"
|
||||
ReportPlayback("stop")
|
||||
m.playReported = false
|
||||
else if m.top.audio.state = "finished"
|
||||
ReportPlayback("stop")
|
||||
m.playReported = false
|
||||
end if
|
||||
|
||||
' Song Finished, attempt to move to next song
|
||||
if m.top.audio.state = "finished"
|
||||
if m.global.audioPlayer.state = "finished"
|
||||
' User has enabled single song loop, play current song again
|
||||
if m.loopMode = "one"
|
||||
playAction()
|
||||
@ -246,18 +227,18 @@ sub audioStateChanged()
|
||||
end sub
|
||||
|
||||
function playAction() as boolean
|
||||
if m.top.audio.state = "playing"
|
||||
m.top.audio.control = "pause"
|
||||
if m.global.audioPlayer.state = "playing"
|
||||
m.global.audioPlayer.control = "pause"
|
||||
' Allow screen to go to real screensaver
|
||||
WriteAsciiFile("tmp:/scene.temp", "nowplaying-paused")
|
||||
MoveFile("tmp:/scene.temp", "tmp:/scene")
|
||||
else if m.top.audio.state = "paused"
|
||||
m.top.audio.control = "resume"
|
||||
else if m.global.audioPlayer.state = "paused"
|
||||
m.global.audioPlayer.control = "resume"
|
||||
' Write screen tracker for screensaver
|
||||
WriteAsciiFile("tmp:/scene.temp", "nowplaying")
|
||||
MoveFile("tmp:/scene.temp", "tmp:/scene")
|
||||
else if m.top.audio.state = "finished"
|
||||
m.top.audio.control = "play"
|
||||
else if m.global.audioPlayer.state = "finished"
|
||||
m.global.audioPlayer.control = "play"
|
||||
' Write screen tracker for screensaver
|
||||
WriteAsciiFile("tmp:/scene.temp", "nowplaying")
|
||||
MoveFile("tmp:/scene.temp", "tmp:/scene")
|
||||
@ -268,15 +249,15 @@ end function
|
||||
|
||||
function previousClicked() as boolean
|
||||
if m.playlistTypeCount > 1 then return false
|
||||
if m.global.queueManager.callFunc("getPosition") = 0 then return false
|
||||
|
||||
if m.top.audio.state = "playing"
|
||||
m.top.audio.control = "stop"
|
||||
if m.global.audioPlayer.state = "playing"
|
||||
m.global.audioPlayer.control = "stop"
|
||||
end if
|
||||
|
||||
if m.global.queueManager.callFunc("getPosition") > 0
|
||||
m.global.queueManager.callFunc("moveBack")
|
||||
pageContentChanged()
|
||||
end if
|
||||
m.global.queueManager.callFunc("moveBack")
|
||||
pageContentChanged()
|
||||
|
||||
|
||||
return true
|
||||
end function
|
||||
@ -363,8 +344,8 @@ function shuffleClicked() as boolean
|
||||
end function
|
||||
|
||||
sub LoadNextSong()
|
||||
if m.top.audio.state = "playing"
|
||||
m.top.audio.control = "stop"
|
||||
if m.global.audioPlayer.state = "playing"
|
||||
m.global.audioPlayer.control = "stop"
|
||||
end if
|
||||
|
||||
' Reset playPosition bar without animation
|
||||
@ -434,9 +415,9 @@ sub onAudioStreamLoaded()
|
||||
data = m.LoadAudioStreamTask.content[0]
|
||||
m.LoadAudioStreamTask.unobserveField("content")
|
||||
if data <> invalid and data.count() > 0
|
||||
m.top.audio.content = data
|
||||
m.top.audio.control = "none"
|
||||
m.top.audio.control = "play"
|
||||
m.global.audioPlayer.content = data
|
||||
m.global.audioPlayer.control = "none"
|
||||
m.global.audioPlayer.control = "play"
|
||||
end if
|
||||
end sub
|
||||
|
||||
@ -545,7 +526,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||
if key = "play"
|
||||
return playAction()
|
||||
else if key = "back"
|
||||
m.top.audio.control = "stop"
|
||||
m.global.audioPlayer.control = "stop"
|
||||
else if key = "rewind"
|
||||
return previousClicked()
|
||||
else if key = "fastforward"
|
||||
@ -587,21 +568,3 @@ sub OnScreenHidden()
|
||||
WriteAsciiFile("tmp:/scene.temp", "")
|
||||
MoveFile("tmp:/scene.temp", "tmp:/scene")
|
||||
end sub
|
||||
|
||||
' Report playback to server
|
||||
sub ReportPlayback(state = "update" as string)
|
||||
|
||||
if m.top.audio.position = invalid then return
|
||||
|
||||
params = {
|
||||
"ItemId": m.global.queueManager.callFunc("getCurrentItem").id,
|
||||
"PlaySessionId": m.top.audio.content.id,
|
||||
"PositionTicks": int(m.top.audio.position) * 10000000&, 'Ensure a LongInteger is used
|
||||
"IsPaused": (m.top.audio.state = "paused")
|
||||
}
|
||||
|
||||
' Report playstate via worker task
|
||||
playstateTask = m.global.playstateTask
|
||||
playstateTask.setFields({ status: state, params: params })
|
||||
playstateTask.control = "RUN"
|
||||
end sub
|
||||
|
@ -119,7 +119,6 @@
|
||||
<Poster width="0" height="0" uri="pkg:/images/icons/loopIndicator1-on.png" visible="false" />
|
||||
</children>
|
||||
<interface>
|
||||
<field id="audio" type="node" />
|
||||
<field id="state" type="string" />
|
||||
<field id="selectedButtonIndex" type="integer" />
|
||||
</interface>
|
||||
|
2
components/options/OptionNode.brs
Normal file
2
components/options/OptionNode.brs
Normal file
@ -0,0 +1,2 @@
|
||||
sub init()
|
||||
end sub
|
9
components/options/OptionNode.xml
Normal file
9
components/options/OptionNode.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="OptionNode" extends="ContentNode">
|
||||
<interface>
|
||||
<field id="name" type="string" />
|
||||
<field id="delimiter" type="string" />
|
||||
<field id="checkedState" type="array" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="OptionNode.brs" />
|
||||
</component>
|
@ -19,7 +19,7 @@ end sub
|
||||
|
||||
sub updateSeason()
|
||||
if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
|
||||
if m.top.seasonData?.UserData?.UnplayedItemCount <> invalid
|
||||
if isValid(m.top.seasonData) and isValid(m.top.seasonData.UserData) and isValid(m.top.seasonData.UserData.UnplayedItemCount)
|
||||
if m.top.seasonData.UserData.UnplayedItemCount > 0
|
||||
m.unplayedCount.visible = true
|
||||
m.unplayedEpisodeCount.text = m.top.seasonData.UserData.UnplayedItemCount
|
||||
@ -57,7 +57,6 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||
return true
|
||||
end if
|
||||
|
||||
|
||||
if key = "OK" or key = "play"
|
||||
if m.Random.hasFocus()
|
||||
randomEpisode = Rnd(m.rows.getChild(0).objects.items.count()) - 1
|
||||
@ -81,7 +80,6 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||
end if
|
||||
end if
|
||||
|
||||
|
||||
focusedChild = m.top.focusedChild.focusedChild
|
||||
if focusedChild.content = invalid then return handled
|
||||
|
||||
@ -94,7 +92,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
|
||||
if press and key = "play" or proceed = true
|
||||
m.top.lastFocus = focusedChild
|
||||
itemToPlay = focusedChild.content.getChild(focusedChild.rowItemFocused[0]).getChild(0)
|
||||
if itemToPlay <> invalid and itemToPlay.id <> ""
|
||||
if isValid(itemToPlay) and isValid(itemToPlay.id) and itemToPlay.id <> ""
|
||||
itemToPlay.type = "Episode"
|
||||
m.top.quickPlayNode = itemToPlay
|
||||
end if
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component name="TVEpisodes" extends="JFGroup">
|
||||
<children>
|
||||
<Poster id="seasonPoster" width="300" height="450" translation="[95,175]">
|
||||
@ -20,4 +20,5 @@
|
||||
<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>
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
|
||||
</component>
|
@ -18,7 +18,7 @@ end sub
|
||||
sub itemContentChanged()
|
||||
item = m.top.itemContent
|
||||
itemData = item.json
|
||||
if itemData.indexNumber <> invalid
|
||||
if isValid(itemData.indexNumber)
|
||||
indexNumber = itemData.indexNumber.toStr() + ". "
|
||||
else
|
||||
indexNumber = ""
|
||||
@ -26,7 +26,7 @@ sub itemContentChanged()
|
||||
m.title.text = indexNumber + item.title
|
||||
m.overview.text = item.overview
|
||||
|
||||
if itemData.PremiereDate <> invalid
|
||||
if isValid(itemData.PremiereDate)
|
||||
airDate = CreateObject("roDateTime")
|
||||
airDate.FromISO8601String(itemData.PremiereDate)
|
||||
m.top.findNode("aired").text = tr("Aired") + ": " + airDate.AsDateString("short-month-no-weekday")
|
||||
@ -70,12 +70,12 @@ sub itemContentChanged()
|
||||
end if
|
||||
|
||||
' Add checkmark in corner (if applicable)
|
||||
if isValid(itemData?.UserData?.Played) and itemData.UserData.Played = true
|
||||
if isValid(itemData.UserData) and isValid(itemData.UserData.Played) and itemData.UserData.Played = true
|
||||
m.playedIndicator.visible = true
|
||||
end if
|
||||
|
||||
' Add progress bar on bottom (if applicable)
|
||||
if isValid(itemData?.UserData?.PlayedPercentage) and itemData?.UserData?.PlayedPercentage > 0
|
||||
if isValid(itemData.UserData) and isValid(itemData.UserData.PlayedPercentage) and itemData.UserData.PlayedPercentage > 0
|
||||
m.progressBackground.width = m.poster.width
|
||||
m.progressBackground.visible = true
|
||||
progressWidthInPixels = int(m.progressBackground.width * itemData.UserData.PlayedPercentage / 100)
|
||||
@ -86,7 +86,7 @@ sub itemContentChanged()
|
||||
videoIdx = invalid
|
||||
audioIdx = invalid
|
||||
|
||||
if itemData.MediaStreams <> invalid
|
||||
if isValid(itemData.MediaStreams)
|
||||
for i = 0 to itemData.MediaStreams.Count() - 1
|
||||
if itemData.MediaStreams[i].Type = "Video" and videoIdx = invalid
|
||||
videoIdx = i
|
||||
@ -99,12 +99,12 @@ sub itemContentChanged()
|
||||
end if
|
||||
m.top.findNode("audio_codec").text = tr("Audio") + ": " + itemData.mediaStreams[audioIdx].DisplayTitle
|
||||
end if
|
||||
if videoIdx <> invalid and audioIdx <> invalid then exit for
|
||||
if isValid(videoIdx) and isValid(audioIdx) then exit for
|
||||
end for
|
||||
end if
|
||||
|
||||
m.top.findNode("video_codec").visible = videoIdx <> invalid
|
||||
if audioIdx <> invalid
|
||||
m.top.findNode("video_codec").visible = isValid(videoIdx)
|
||||
if isValid(audioIdx)
|
||||
m.top.findNode("audio_codec").visible = true
|
||||
DisplayAudioAvailable(itemData.mediaStreams)
|
||||
else
|
||||
@ -113,7 +113,6 @@ sub itemContentChanged()
|
||||
end sub
|
||||
|
||||
sub DisplayAudioAvailable(streams)
|
||||
|
||||
count = 0
|
||||
for i = 0 to streams.Count() - 1
|
||||
if streams[i].Type = "Audio"
|
||||
@ -124,7 +123,6 @@ sub DisplayAudioAvailable(streams)
|
||||
if count > 1
|
||||
m.top.findnode("audio_codec_count").text = "+" + stri(count - 1).trim()
|
||||
end if
|
||||
|
||||
end sub
|
||||
|
||||
function getRuntime() as integer
|
||||
|
@ -17,7 +17,7 @@ sub itemContentChanged()
|
||||
itemData = item.json
|
||||
|
||||
if get_user_setting("ui.tvshows.disableUnwatchedEpisodeCount", "false") = "false"
|
||||
if itemData?.UserData?.UnplayedItemCount <> invalid
|
||||
if isValid(itemData.UserData) and isValid(itemData.UserData.UnplayedItemCount)
|
||||
if itemData.UserData.UnplayedItemCount > 0
|
||||
m.unplayedCount.visible = true
|
||||
m.unplayedEpisodeCount.text = itemData.UserData.UnplayedItemCount
|
||||
@ -31,21 +31,21 @@ sub itemContentChanged()
|
||||
m.top.overhangTitle = itemData.name
|
||||
|
||||
'Check production year, if invalid remove label
|
||||
if itemData.productionYear <> invalid
|
||||
if isValid(itemData.productionYear)
|
||||
setFieldText("releaseYear", itemData.productionYear)
|
||||
else
|
||||
m.top.findNode("main_group").removeChild(m.top.findNode("releaseYear"))
|
||||
end if
|
||||
|
||||
'Check officialRating, if invalid remove label
|
||||
if itemData.officialRating <> invalid
|
||||
if isValid(itemData.officialRating)
|
||||
setFieldText("officialRating", itemData.officialRating)
|
||||
else
|
||||
m.top.findNode("main_group").removeChild(m.top.findNode("officialRating"))
|
||||
end if
|
||||
|
||||
'Check communityRating, if invalid remove label
|
||||
if itemData.communityRating <> invalid
|
||||
if isValid(itemData.communityRating)
|
||||
m.top.findNode("star").visible = true
|
||||
setFieldText("communityRating", int(itemData.communityRating * 10) / 10)
|
||||
else
|
||||
@ -134,7 +134,7 @@ function getHistory() as string
|
||||
|
||||
airdays = itemData.airdays
|
||||
airtime = itemData.airtime
|
||||
if airtime <> invalid and airdays.count() = 1
|
||||
if isValid(airtime) and airdays.count() = 1
|
||||
airwords = airdays[0] + " at " + airtime
|
||||
end if
|
||||
|
||||
@ -148,10 +148,10 @@ function getHistory() as string
|
||||
end if
|
||||
|
||||
words = verb
|
||||
if airwords <> invalid
|
||||
if isValid(airwords)
|
||||
words = words + " " + airwords
|
||||
end if
|
||||
if studio <> invalid
|
||||
if isValid(studio)
|
||||
words = words + " on " + studio
|
||||
end if
|
||||
|
||||
|
17
dictionary.txt
Normal file
17
dictionary.txt
Normal file
@ -0,0 +1,17 @@
|
||||
Jellyfin
|
||||
VSCode
|
||||
BrightScript
|
||||
sideload
|
||||
Sideload
|
||||
DEVGUIDE
|
||||
ing
|
||||
hardcode
|
||||
Hardcoding
|
||||
pre-release
|
||||
breakpoint
|
||||
repo
|
||||
Repo
|
||||
dev
|
||||
Dev
|
||||
assignees
|
||||
HTTPS
|
@ -6,7 +6,7 @@
|
||||
<name>default</name>
|
||||
<message>
|
||||
<source>192.168.1.100:8096 or https://example.com/jellyfin</source>
|
||||
<translation>default192.168.1.100:8096 or https://example.com/jellyfin</translation>
|
||||
<translation>192.168.1.100:8096 or https://example.com/jellyfin</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cancel</source>
|
||||
@ -764,8 +764,8 @@
|
||||
<extracomment>Name of codec used in settings menu</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
<source>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to trancoding if it fails.</source>
|
||||
<translation>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to trancoding if it fails.</translation>
|
||||
<source>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to transcoding if it fails.</source>
|
||||
<translation>Attempt Direct Play for HEVC media with unsupported profile levels before falling back to transcoding if it fails.</translation>
|
||||
<extracomment>Settings Menu - Description for option</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
@ -929,9 +929,14 @@
|
||||
<extracomment>Settings Menu - Title for option</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
<source>Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.</source>
|
||||
<translation>Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.</translation>
|
||||
<extracomment>Settings Menu - Description for option</extracomment>
|
||||
<source>Parental Ratings</source>
|
||||
<translation>Parental Ratings</translation>
|
||||
<extracomment>Used in Filter menu</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
<source>Years</source>
|
||||
<translation>Years</translation>
|
||||
<extracomment>Used in Filter menu</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
<source>Show What's New Popup</source>
|
||||
@ -965,14 +970,6 @@
|
||||
<translation>Default view for Movie Libraries.</translation>
|
||||
<extracomment>Settings Menu - Description for option</extracomment>
|
||||
</message>
|
||||
<message>
|
||||
<source>Movies (Presentation)</source>
|
||||
<translation>Movies (Presentation)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Movies (Grid)</source>
|
||||
<translation>Movies (Grid)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Item Titles</source>
|
||||
<translation>Item Titles</translation>
|
||||
@ -1128,4 +1125,4 @@
|
||||
<extracomment>Settings Menu - Description for option</extracomment>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
7272
package-lock.json
generated
7272
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -7,17 +7,18 @@
|
||||
"@rokucommunity/bslint": "0.8.1",
|
||||
"brighterscript": "0.61.3",
|
||||
"ropm": "0.10.11",
|
||||
"jsonlint-cli": "1.0.1",
|
||||
"markdownlint-cli": "0.33.0",
|
||||
"markdown-spellcheck": "1.3.1"
|
||||
"jshint": "^2.13.6",
|
||||
"markdownlint-cli2": "0.6.0",
|
||||
"spellchecker-cli": "6.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "npx ropm copy",
|
||||
"validate": "npx bsc --copy-to-staging=false --create-package=false",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"lint": "bslint",
|
||||
"lint-json": "jsonlint-cli **/*.json",
|
||||
"lint-markdown": "markdownlint **/*.md --ignore node_modules && mdspell --en-us -r **/*.md !**/node_modules/**/*.md",
|
||||
"lint-json": "jshint --extra-ext .json --verbose --exclude node_modules ./",
|
||||
"lint-markdown": "markdownlint-cli2 \"**/*.md\" \"#node_modules\"",
|
||||
"lint-spelling": "spellchecker -d dictionary.txt --files \"**/*.md\" \"**/.*/**/*.md\" \"!node_modules/**/*.md\"",
|
||||
"check-formatting": "npx bsfmt --check",
|
||||
"format": "npx bsfmt --write"
|
||||
},
|
||||
|
@ -63,7 +63,7 @@
|
||||
},
|
||||
{
|
||||
"title": "HEVC",
|
||||
"description": "Attempt Direct Play for HEVC media with unsupported profile levels before falling back to trancoding if it fails.",
|
||||
"description": "Attempt Direct Play for HEVC media with unsupported profile levels before falling back to transcoding if it fails.",
|
||||
"settingName": "playback.tryDirect.hevcProfileLevel",
|
||||
"type": "bool",
|
||||
"default": "true"
|
||||
|
106
source/Main.brs
106
source/Main.brs
@ -42,6 +42,7 @@ sub Main (args as dynamic) as void
|
||||
|
||||
m.global.addFields({ app_loaded: false, playstateTask: playstateTask, sceneManager: sceneManager })
|
||||
m.global.addFields({ queueManager: CreateObject("roSGNode", "QueueManager") })
|
||||
m.global.addFields({ audioPlayer: CreateObject("roSGNode", "AudioPlayer") })
|
||||
|
||||
app_start:
|
||||
' First thing to do is validate the ability to use the API
|
||||
@ -256,8 +257,10 @@ sub Main (args as dynamic) as void
|
||||
ptr = msg.getData()
|
||||
' ptr is for [row, col] of selected item... but we only have 1 row
|
||||
series = msg.getRoSGNode()
|
||||
node = series.seasonData.items[ptr[1]]
|
||||
group = CreateSeasonDetailsGroup(series.itemContent, node)
|
||||
if isValid(ptr) and ptr.count() >= 2 and isValid(ptr[1]) and isValid(series) and isValid(series.seasonData) and isValid(series.seasonData.items)
|
||||
node = series.seasonData.items[ptr[1]]
|
||||
group = CreateSeasonDetailsGroup(series.itemContent, node)
|
||||
end if
|
||||
else if isNodeEvent(msg, "musicAlbumSelected")
|
||||
' If you select a Music Album from ANYWHERE, follow this flow
|
||||
ptr = msg.getData()
|
||||
@ -401,33 +404,33 @@ sub Main (args as dynamic) as void
|
||||
' If a button is selected, we have some determining to do
|
||||
btn = getButton(msg)
|
||||
group = sceneManager.callFunc("getActiveScene")
|
||||
if btn <> invalid and btn.id = "play-button"
|
||||
if isValid(btn) and btn.id = "play-button"
|
||||
|
||||
' Check if a specific Audio Stream was selected
|
||||
audio_stream_idx = 1
|
||||
if group.selectedAudioStreamIndex <> invalid
|
||||
if isValid(group) and isValid(group.selectedAudioStreamIndex)
|
||||
audio_stream_idx = group.selectedAudioStreamIndex
|
||||
end if
|
||||
|
||||
' Check to see if a specific video "version" was selected
|
||||
mediaSourceId = invalid
|
||||
if group.selectedVideoStreamId <> invalid
|
||||
if isValid(group) and isValid(group.selectedVideoStreamId)
|
||||
mediaSourceId = group.selectedVideoStreamId
|
||||
end if
|
||||
video_id = group.id
|
||||
video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx)
|
||||
if video <> invalid and video.errorMsg <> "introaborted"
|
||||
if isValid(video) and video.errorMsg <> "introaborted"
|
||||
sceneManager.callFunc("pushScene", video)
|
||||
end if
|
||||
|
||||
if group.lastfocus.id = "main_group"
|
||||
if isValid(group) and isValid(group.lastFocus) and isValid(group.lastFocus.id) and group.lastFocus.id = "main_group"
|
||||
buttons = group.findNode("buttons")
|
||||
if isValid(buttons)
|
||||
group.lastfocus = group.findNode("buttons")
|
||||
group.lastFocus = group.findNode("buttons")
|
||||
end if
|
||||
end if
|
||||
|
||||
if group.lastFocus <> invalid
|
||||
if isValid(group) and isValid(group.lastFocus)
|
||||
group.lastFocus.setFocus(true)
|
||||
end if
|
||||
|
||||
@ -440,26 +443,31 @@ sub Main (args as dynamic) as void
|
||||
video_id = group.id
|
||||
|
||||
trailerData = api_API().users.getlocaltrailers(get_setting("active_user"), group.id)
|
||||
video = invalid
|
||||
|
||||
video_id = trailerData[0].id
|
||||
if isValid(trailerData) and isValid(trailerData[0]) and isValid(trailerData[0].id)
|
||||
video_id = trailerData[0].id
|
||||
video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx, false, false)
|
||||
end if
|
||||
|
||||
video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx, false, false)
|
||||
if video <> invalid and video.errorMsg <> "introaborted"
|
||||
if isValid(video) and video.errorMsg <> "introaborted"
|
||||
sceneManager.callFunc("pushScene", video)
|
||||
dialog.close = true
|
||||
end if
|
||||
|
||||
if group.lastFocus <> invalid
|
||||
if isValid(group) and isValid(group.lastFocus)
|
||||
group.lastFocus.setFocus(true)
|
||||
end if
|
||||
else if btn <> invalid and btn.id = "watched-button"
|
||||
movie = group.itemContent
|
||||
if movie.watched
|
||||
UnmarkItemWatched(movie.id)
|
||||
else
|
||||
MarkItemWatched(movie.id)
|
||||
if isValid(movie) and isValid(movie.watched) and isValid(movie.id)
|
||||
if movie.watched
|
||||
UnmarkItemWatched(movie.id)
|
||||
else
|
||||
MarkItemWatched(movie.id)
|
||||
end if
|
||||
movie.watched = not movie.watched
|
||||
end if
|
||||
movie.watched = not movie.watched
|
||||
else if btn <> invalid and btn.id = "favorite-button"
|
||||
movie = group.itemContent
|
||||
if movie.favorite
|
||||
@ -479,11 +487,11 @@ sub Main (args as dynamic) as void
|
||||
else if isNodeEvent(msg, "optionSelected")
|
||||
button = msg.getRoSGNode()
|
||||
group = sceneManager.callFunc("getActiveScene")
|
||||
if button.id = "goto_search"
|
||||
if button.id = "goto_search" and isValid(group)
|
||||
' Exit out of the side panel
|
||||
panel = group.findNode("options")
|
||||
panel.visible = false
|
||||
if group.lastFocus <> invalid
|
||||
if isValid(group.lastFocus)
|
||||
group.lastFocus.setFocus(true)
|
||||
else
|
||||
group.setFocus(true)
|
||||
@ -506,7 +514,7 @@ sub Main (args as dynamic) as void
|
||||
' Exit out of the side panel
|
||||
panel = group.findNode("options")
|
||||
panel.visible = false
|
||||
if group.lastFocus <> invalid
|
||||
if isValid(group) and isValid(group.lastFocus)
|
||||
group.lastFocus.setFocus(true)
|
||||
else
|
||||
group.setFocus(true)
|
||||
@ -529,41 +537,47 @@ sub Main (args as dynamic) as void
|
||||
end if
|
||||
else if isNodeEvent(msg, "state")
|
||||
node = msg.getRoSGNode()
|
||||
if m.selectedItemType = "TvChannel" and node.state = "finished"
|
||||
video = CreateVideoPlayerGroup(node.id)
|
||||
m.global.sceneManager.callFunc("pushScene", video)
|
||||
m.global.sceneManager.callFunc("deleteSceneAtIndex", 2)
|
||||
else if node.state = "finished"
|
||||
node.control = "stop"
|
||||
if isValid(node) and isValid(node.state)
|
||||
if m.selectedItemType = "TvChannel" and node.state = "finished"
|
||||
video = CreateVideoPlayerGroup(node.id)
|
||||
m.global.sceneManager.callFunc("pushScene", video)
|
||||
m.global.sceneManager.callFunc("deleteSceneAtIndex", 2)
|
||||
else if node.state = "finished"
|
||||
node.control = "stop"
|
||||
|
||||
' If node allows retrying using Transcode Url, give that shot
|
||||
if isValid(node.retryWithTranscoding) and node.retryWithTranscoding
|
||||
retryVideo = CreateVideoPlayerGroup(node.Id, invalid, node.audioIndex, true, false)
|
||||
m.global.sceneManager.callFunc("popScene")
|
||||
if retryVideo <> invalid
|
||||
m.global.sceneManager.callFunc("pushScene", retryVideo)
|
||||
end if
|
||||
else if node.showID = invalid
|
||||
sceneManager.callFunc("popScene")
|
||||
else
|
||||
if video.errorMsg = ""
|
||||
autoPlayNextEpisode(node.id, node.showID)
|
||||
else
|
||||
' If node allows retrying using Transcode Url, give that shot
|
||||
if isValid(node.retryWithTranscoding) and node.retryWithTranscoding
|
||||
retryVideo = CreateVideoPlayerGroup(node.Id, invalid, node.audioIndex, true, false)
|
||||
m.global.sceneManager.callFunc("popScene")
|
||||
if isValid(retryVideo)
|
||||
m.global.sceneManager.callFunc("pushScene", retryVideo)
|
||||
end if
|
||||
else if not isValid(node.showID)
|
||||
sceneManager.callFunc("popScene")
|
||||
else
|
||||
if video.errorMsg = ""
|
||||
autoPlayNextEpisode(node.id, node.showID)
|
||||
else
|
||||
sceneManager.callFunc("popScene")
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
else if type(msg) = "roDeviceInfoEvent"
|
||||
event = msg.GetInfo()
|
||||
group = sceneManager.callFunc("getActiveScene")
|
||||
|
||||
if event.exitedScreensaver = true
|
||||
sceneManager.callFunc("resetTime")
|
||||
if group.subtype() = "Home"
|
||||
currentTime = CreateObject("roDateTime").AsSeconds()
|
||||
group.timeLastRefresh = currentTime
|
||||
group.callFunc("refresh")
|
||||
group = sceneManager.callFunc("getActiveScene")
|
||||
if isValid(group) and isValid(group.subtype())
|
||||
' refresh the current view
|
||||
if group.subtype() = "Home"
|
||||
currentTime = CreateObject("roDateTime").AsSeconds()
|
||||
group.timeLastRefresh = currentTime
|
||||
group.callFunc("refresh")
|
||||
end if
|
||||
' todo: add other screens to be refreshed - movie detail, tv series, episode list etc.
|
||||
end if
|
||||
' todo: add other screens to be refreshed - movie detail, tv series, episode list etc.
|
||||
else
|
||||
print "Unhandled roDeviceInfoEvent:"
|
||||
print msg.GetInfo()
|
||||
|
@ -337,8 +337,8 @@ function CreateMovieDetailsGroup(movie)
|
||||
group.optionsAvailable = false
|
||||
m.global.sceneManager.callFunc("pushScene", group)
|
||||
|
||||
movie = ItemMetaData(movie.id)
|
||||
group.itemContent = movie
|
||||
movieMetaData = ItemMetaData(movie.id)
|
||||
group.itemContent = movieMetaData
|
||||
group.trailerAvailable = false
|
||||
|
||||
trailerData = api_API().users.getlocaltrailers(get_setting("active_user"), movie.id)
|
||||
@ -353,7 +353,7 @@ function CreateMovieDetailsGroup(movie)
|
||||
|
||||
extras = group.findNode("extrasGrid")
|
||||
extras.observeField("selectedItem", m.port)
|
||||
extras.callFunc("loadParts", movie.json)
|
||||
extras.callFunc("loadParts", movieMetaData.json)
|
||||
stopLoadingSpinner()
|
||||
return group
|
||||
end function
|
||||
|
@ -31,10 +31,10 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
|
||||
|
||||
' Special handling for "Programs" or "Vidoes" launched from "On Now" or elsewhere on the home screen...
|
||||
' basically anything that is a Live Channel.
|
||||
if isValid(meta?.json?.ChannelId)
|
||||
if meta.json.EpisodeTitle <> invalid
|
||||
if isValid(meta.json) and isValid(meta.json.ChannelId)
|
||||
if isValid(meta.json.EpisodeTitle)
|
||||
meta.title = meta.json.EpisodeTitle
|
||||
else if meta.json.Name <> invalid
|
||||
else if isValid(meta.json.Name)
|
||||
meta.title = meta.json.Name
|
||||
end if
|
||||
meta.showID = meta.json.id
|
||||
@ -47,7 +47,7 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
|
||||
end if
|
||||
|
||||
if m.videotype = "Episode" or m.videotype = "Series"
|
||||
if isValid(meta.json.RunTimeTicks)
|
||||
if isValid(meta.json) and isValid(meta.json.RunTimeTicks)
|
||||
video.runTime = (meta.json.RunTimeTicks / 10000000.0)
|
||||
end if
|
||||
video.content.contenttype = "episode"
|
||||
@ -243,11 +243,9 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
|
||||
video.content.SubtitleTracks = subtitles["text"]
|
||||
|
||||
' 'TODO: allow user selection of subtitle track before playback initiated, for now set to no subtitles
|
||||
|
||||
video.directPlaySupported = m.playbackInfo.MediaSources[0].SupportsDirectPlay
|
||||
fully_external = false
|
||||
|
||||
|
||||
' For h264/hevc video, Roku spec states that it supports specfic encoding levels
|
||||
' The device can decode content with a Higher Encoding level but may play it back with certain
|
||||
' artifacts. If the user preference is set, and the only reason the server says we need to
|
||||
@ -256,7 +254,7 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = -
|
||||
if m.playbackInfo.MediaSources[0].MediaStreams.Count() > 0 and meta.live = false
|
||||
tryDirectPlay = get_user_setting("playback.tryDirect.h264ProfileLevel") = "true" and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "h264"
|
||||
tryDirectPlay = tryDirectPlay or (get_user_setting("playback.tryDirect.hevcProfileLevel") = "true" and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "hevc")
|
||||
if tryDirectPlay and m.playbackInfo.MediaSources[0].TranscodingUrl <> invalid and forceTranscoding = false
|
||||
if tryDirectPlay and isValid(m.playbackInfo.MediaSources[0].TranscodingUrl) and forceTranscoding = false
|
||||
transcodingReasons = getTranscodeReasons(m.playbackInfo.MediaSources[0].TranscodingUrl)
|
||||
if transcodingReasons.Count() = 1 and transcodingReasons[0] = "VideoLevelNotSupported"
|
||||
video.directPlaySupported = true
|
||||
@ -324,7 +322,6 @@ end sub
|
||||
function PlayIntroVideo(video_id, audio_stream_idx) as boolean
|
||||
' Intro videos only play if user has cinema mode setting enabled
|
||||
if get_user_setting("playback.cinemamode") = "true"
|
||||
|
||||
' Check if server has intro videos setup and available
|
||||
introVideos = GetIntroVideos(video_id)
|
||||
|
||||
@ -362,7 +359,6 @@ end function
|
||||
' Extract array of Transcode Reasons from the content URL
|
||||
' @returns Array of Strings
|
||||
function getTranscodeReasons(url as string) as object
|
||||
|
||||
regex = CreateObject("roRegex", "&TranscodeReasons=([^&]*)", "")
|
||||
match = regex.Match(url)
|
||||
|
||||
@ -384,19 +380,18 @@ end function
|
||||
|
||||
function directPlaySupported(meta as object) as boolean
|
||||
devinfo = CreateObject("roDeviceInfo")
|
||||
if meta.json.MediaSources[0] <> invalid and meta.json.MediaSources[0].SupportsDirectPlay = false
|
||||
if isValid(meta.json.MediaSources[0]) and meta.json.MediaSources[0].SupportsDirectPlay = false
|
||||
return false
|
||||
end if
|
||||
|
||||
if meta.json.MediaStreams[0] = invalid
|
||||
if not isValid(meta.json.MediaSources[0])
|
||||
return false
|
||||
end if
|
||||
|
||||
streamInfo = { Codec: meta.json.MediaStreams[0].codec }
|
||||
if meta.json.MediaStreams[0].Profile <> invalid and meta.json.MediaStreams[0].Profile.len() > 0
|
||||
if isValid(meta.json.MediaStreams[0].Profile) and meta.json.MediaStreams[0].Profile.len() > 0
|
||||
streamInfo.Profile = LCase(meta.json.MediaStreams[0].Profile)
|
||||
end if
|
||||
if meta.json.MediaSources[0].container <> invalid and meta.json.MediaSources[0].container.len() > 0
|
||||
if isValid(meta.json.MediaSources[0].container) and meta.json.MediaSources[0].container.len() > 0
|
||||
'CanDecodeVideo() requires the .container to be format: “mp4”, “hls”, “mkv”, “ism”, “dash”, “ts” if its to direct stream
|
||||
if meta.json.MediaSources[0].container = "mov"
|
||||
streamInfo.Container = "mp4"
|
||||
@ -406,8 +401,7 @@ function directPlaySupported(meta as object) as boolean
|
||||
end if
|
||||
|
||||
decodeResult = devinfo.CanDecodeVideo(streamInfo)
|
||||
return decodeResult <> invalid and decodeResult.result
|
||||
|
||||
return isValid(decodeResult) and decodeResult.result
|
||||
end function
|
||||
|
||||
function getContainerType(meta as object) as string
|
||||
@ -455,12 +449,12 @@ sub autoPlayNextEpisode(videoID as string, showID as string)
|
||||
resp = APIRequest(url, urlParams)
|
||||
data = getJson(resp)
|
||||
|
||||
if data <> invalid and data.Items.Count() = 2
|
||||
if isValid(data) and data.Items.Count() = 2
|
||||
' setup new video node
|
||||
nextVideo = CreateVideoPlayerGroup(data.Items[1].Id, invalid, 1, false, false)
|
||||
' remove last videoplayer scene
|
||||
m.global.sceneManager.callFunc("clearPreviousScene")
|
||||
if nextVideo <> invalid
|
||||
if isValid(nextVideo)
|
||||
m.global.sceneManager.callFunc("pushScene", nextVideo)
|
||||
else
|
||||
m.global.sceneManager.callFunc("popScene")
|
||||
@ -478,7 +472,7 @@ end sub
|
||||
' In the future, with a custom playback info view, we can return an associated array.
|
||||
function GetPlaybackInfo()
|
||||
sessions = api_API().sessions.get()
|
||||
if sessions <> invalid and sessions.Count() > 0
|
||||
if isValid(sessions) and sessions.Count() > 0
|
||||
return GetTranscodingStats(sessions[0])
|
||||
end if
|
||||
|
||||
|
@ -38,7 +38,6 @@ function searchMedia(query as string)
|
||||
' This appears to be done differently on the web now
|
||||
' For each potential type, a separate query is done:
|
||||
' varying item types, and artists, and people
|
||||
|
||||
if query <> ""
|
||||
resp = APIRequest(Substitute("Search/Hints", get_setting("active_user")), {
|
||||
"searchTerm": query,
|
||||
@ -55,7 +54,6 @@ function searchMedia(query as string)
|
||||
"limit": 100
|
||||
})
|
||||
|
||||
|
||||
data = getJson(resp)
|
||||
results = []
|
||||
for each item in data.SearchHints
|
||||
@ -79,7 +77,7 @@ function ItemMetaData(id as string)
|
||||
|
||||
imgParams = {}
|
||||
if data.type <> "Audio"
|
||||
if data?.UserData?.PlayedPercentage <> invalid
|
||||
if data.UserData <> invalid and data.UserData.PlayedPercentage <> invalid
|
||||
param = { "PercentPlayed": data.UserData.PlayedPercentage }
|
||||
imgParams.Append(param)
|
||||
end if
|
||||
|
@ -21,7 +21,7 @@ end function
|
||||
' returns the server-side track index for the appriate subtitle
|
||||
function defaultSubtitleTrackFromVid(video_id) as integer
|
||||
meta = ItemMetaData(video_id)
|
||||
if meta?.json?.mediaSources <> invalid
|
||||
if isValid(meta) and isValid(meta.json) and isValid(meta.json.mediaSources)
|
||||
subtitles = sortSubtitles(meta.id, meta.json.MediaSources[0].MediaStreams)
|
||||
default_text_subs = defaultSubtitleTrack(subtitles["all"], true) ' Find correct subtitle track (forced text)
|
||||
if default_text_subs <> -1
|
||||
@ -130,7 +130,7 @@ function selectSubtitleTrackDialog(tracks, currentTrack = -1)
|
||||
default = ""
|
||||
if item.IsForced then forced = " [Forced]"
|
||||
if item.IsDefault then default = " - Default"
|
||||
if item.Track.Language <> invalid
|
||||
if isValid(item.Track.Language)
|
||||
language = iso6392.lookup(item.Track.Language)
|
||||
if language = invalid then language = item.Track.Language
|
||||
else
|
||||
@ -157,7 +157,7 @@ sub changeSubtitleDuringPlayback(newid)
|
||||
currentSubtitles = video.Subtitles[video.SelectedSubtitle]
|
||||
newSubtitles = video.Subtitles[newid]
|
||||
|
||||
if newSubtitles.IsEncoded or (currentSubtitles <> invalid and currentSubtitles.IsEncoded)
|
||||
if newSubtitles.IsEncoded or (isValid(currentSubtitles) and currentSubtitles.IsEncoded)
|
||||
' With encoded subtitles we need to stop/start playback
|
||||
video.control = "stop"
|
||||
AddVideoContent(video, video.mediaSourceId, video.audioIndex, newSubtitles.Index, video.position * 10000000)
|
||||
@ -195,7 +195,7 @@ function sortSubtitles(id as string, MediaStreams)
|
||||
if stream.type = "Subtitle"
|
||||
|
||||
url = ""
|
||||
if stream.DeliveryUrl <> invalid
|
||||
if isValid(stream.DeliveryUrl)
|
||||
url = buildURL(stream.DeliveryUrl)
|
||||
end if
|
||||
|
||||
|
@ -91,19 +91,23 @@ function get_dialog_result(dialog, port)
|
||||
end function
|
||||
|
||||
function lastFocusedChild(obj as object) as object
|
||||
if LCase(obj.focusedChild.focusedChild.subType()) = "tvepisodes"
|
||||
if isValid(obj?.focusedChild?.focusedChild?.lastFocus)
|
||||
return obj.focusedChild.focusedChild.lastFocus
|
||||
if isValid(obj)
|
||||
if isValid(obj.focusedChild) and isValid(obj.focusedChild.focusedChild) and LCase(obj.focusedChild.focusedChild.subType()) = "tvepisodes"
|
||||
if isValid(obj.focusedChild.focusedChild.lastFocus)
|
||||
return obj.focusedChild.focusedChild.lastFocus
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
|
||||
child = obj
|
||||
for i = 0 to obj.getChildCount()
|
||||
if obj.focusedChild <> invalid
|
||||
child = child.focusedChild
|
||||
end if
|
||||
end for
|
||||
return child
|
||||
child = obj
|
||||
for i = 0 to obj.getChildCount()
|
||||
if isValid(obj.focusedChild)
|
||||
child = child.focusedChild
|
||||
end if
|
||||
end for
|
||||
return child
|
||||
else
|
||||
return invalid
|
||||
end if
|
||||
end function
|
||||
|
||||
function show_dialog(message as string, options = [], defaultSelection = 0) as integer
|
||||
@ -282,14 +286,53 @@ function findNodeBySubtype(node, subtype)
|
||||
return foundNodes
|
||||
end function
|
||||
|
||||
' Search string array for search value. Return if it's found
|
||||
function inArray(array, searchValue) as boolean
|
||||
for each item in array
|
||||
if lcase(item) = lcase(searchValue) then return true
|
||||
function AssocArrayEqual(Array1 as object, Array2 as object) as boolean
|
||||
if not isValid(Array1) or not isValid(Array2)
|
||||
return false
|
||||
end if
|
||||
|
||||
if not Array1.Count() = Array2.Count()
|
||||
return false
|
||||
end if
|
||||
|
||||
for each key in Array1
|
||||
if not Array2.DoesExist(key)
|
||||
return false
|
||||
end if
|
||||
|
||||
if Array1[key] <> Array2[key]
|
||||
return false
|
||||
end if
|
||||
end for
|
||||
|
||||
return true
|
||||
end function
|
||||
|
||||
' Search string array for search value. Return if it's found
|
||||
function inArray(haystack, needle) as boolean
|
||||
valueToFind = needle
|
||||
|
||||
if LCase(type(valueToFind)) <> "rostring" and LCase(type(valueToFind)) <> "string"
|
||||
valueToFind = str(needle)
|
||||
end if
|
||||
|
||||
valueToFind = lcase(valueToFind)
|
||||
|
||||
for each item in haystack
|
||||
if lcase(item) = valueToFind then return true
|
||||
end for
|
||||
|
||||
return false
|
||||
end function
|
||||
|
||||
function toString(input) as string
|
||||
if LCase(type(input)) = "rostring" or LCase(type(input)) = "string"
|
||||
return input
|
||||
end if
|
||||
|
||||
return str(input)
|
||||
end function
|
||||
|
||||
sub startLoadingSpinner()
|
||||
m.spinner = createObject("roSGNode", "Spinner")
|
||||
m.spinner.translation = "[900, 450]"
|
||||
@ -309,7 +352,7 @@ sub stopLoadingSpinner()
|
||||
if isValid(m.spinner)
|
||||
m.spinner.visible = false
|
||||
end if
|
||||
if isValid(m.scene?.dialog)
|
||||
if isValid(m.scene) and isValid(m.scene.dialog)
|
||||
m.scene.dialog.close = true
|
||||
end if
|
||||
end sub
|
||||
|
Loading…
Reference in New Issue
Block a user